urbanairship 5.2.0 → 9.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +1 -1
  3. data/.github/ISSUE_TEMPLATE.md +2 -2
  4. data/.github/PULL_REQUEST_TEMPLATE.md +9 -4
  5. data/.github/SUPPORT.md +3 -3
  6. data/.github/workflows/ci.yaml +26 -0
  7. data/.github/workflows/release.yaml +30 -0
  8. data/.gitignore +2 -0
  9. data/CHANGELOG +198 -51
  10. data/LICENSE +1 -1
  11. data/README.rst +159 -57
  12. data/docs/ab_tests.rst +162 -0
  13. data/docs/attributes.rst +73 -0
  14. data/docs/automations.rst +212 -0
  15. data/docs/channel_uninstall.rst +1 -1
  16. data/docs/conf.py +11 -6
  17. data/docs/create_and_send.rst +551 -0
  18. data/docs/devices.rst +1 -1
  19. data/docs/examples.rst +10 -10
  20. data/docs/index.rst +23 -19
  21. data/docs/named_user.rst +27 -5
  22. data/docs/push.rst +37 -18
  23. data/docs/reports.rst +9 -9
  24. data/docs/segment.rst +5 -5
  25. data/docs/sms.rst +19 -0
  26. data/docs/static_lists.rst +4 -4
  27. data/docs/tag_lists.rst +76 -0
  28. data/docs/tags.rst +1 -1
  29. data/example/pusher.rb +3 -7
  30. data/lib/urbanairship/ab_tests/ab_test.rb +88 -0
  31. data/lib/urbanairship/ab_tests/experiment.rb +45 -0
  32. data/lib/urbanairship/ab_tests/variant.rb +34 -0
  33. data/lib/urbanairship/automations/automation.rb +105 -0
  34. data/lib/urbanairship/automations/pipeline.rb +52 -0
  35. data/lib/urbanairship/client.rb +57 -14
  36. data/lib/urbanairship/common.rb +110 -41
  37. data/lib/urbanairship/configuration.rb +3 -1
  38. data/lib/urbanairship/custom_events/custom_event.rb +60 -0
  39. data/lib/urbanairship/custom_events/payload.rb +89 -0
  40. data/lib/urbanairship/devices/attribute.rb +54 -0
  41. data/lib/urbanairship/devices/attributes.rb +53 -0
  42. data/lib/urbanairship/devices/channel_tags.rb +2 -2
  43. data/lib/urbanairship/devices/channel_uninstall.rb +10 -10
  44. data/lib/urbanairship/devices/create_and_send.rb +96 -0
  45. data/lib/urbanairship/devices/devicelist.rb +28 -7
  46. data/lib/urbanairship/devices/email.rb +33 -43
  47. data/lib/urbanairship/devices/email_notification.rb +92 -0
  48. data/lib/urbanairship/devices/mms_notification.rb +107 -0
  49. data/lib/urbanairship/devices/named_user.rb +22 -12
  50. data/lib/urbanairship/devices/open_channel.rb +105 -61
  51. data/lib/urbanairship/devices/segment.rb +10 -15
  52. data/lib/urbanairship/devices/sms.rb +51 -23
  53. data/lib/urbanairship/devices/sms_notification.rb +54 -0
  54. data/lib/urbanairship/devices/static_lists.rb +18 -19
  55. data/lib/urbanairship/devices/tag_lists.rb +82 -0
  56. data/lib/urbanairship/oauth.rb +129 -0
  57. data/lib/urbanairship/push/audience.rb +0 -61
  58. data/lib/urbanairship/push/payload.rb +78 -8
  59. data/lib/urbanairship/push/push.rb +23 -13
  60. data/lib/urbanairship/push/schedule.rb +9 -0
  61. data/lib/urbanairship/reports/response_statistics.rb +42 -31
  62. data/lib/urbanairship/version.rb +1 -1
  63. data/lib/urbanairship.rb +20 -1
  64. data/urbanairship.gemspec +8 -6
  65. metadata +74 -15
  66. data/.travis.yml +0 -4
  67. data/docs/location.rst +0 -127
  68. data/lib/urbanairship/push/location.rb +0 -103
@@ -0,0 +1,45 @@
1
+ require 'urbanairship'
2
+
3
+ module Urbanairship
4
+ module AbTests
5
+ class Experiment
6
+ include Urbanairship::Common
7
+ include Urbanairship::Loggable
8
+ attr_accessor :audience,
9
+ :campaigns,
10
+ :control,
11
+ :created_at,
12
+ :description,
13
+ :device_types,
14
+ :id,
15
+ :name,
16
+ :push_id,
17
+ :variants
18
+
19
+ def initialize(client: required('client'))
20
+ @client = client
21
+ @variants = []
22
+ end
23
+
24
+ def payload
25
+ fail ArgumentError, 'audience is required for experiment' if @audience.nil?
26
+ fail ArgumentError, 'device_types is required for experiment' if @device_types.nil?
27
+ fail ArgumentError, 'variant cannot be empty for experiment' if @variants.empty?
28
+
29
+ {
30
+ 'name': name,
31
+ 'description': description,
32
+ 'control': control,
33
+ 'audience': audience,
34
+ 'device_types': device_types,
35
+ 'campaigns': campaigns,
36
+ 'variants': variants,
37
+ 'id': id,
38
+ 'created_at': created_at,
39
+ 'push_id': push_id
40
+ }.delete_if {|key, value| value.nil?} #this removes the nil key value pairs
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ require 'urbanairship'
2
+
3
+ module Urbanairship
4
+ module AbTests
5
+ class Variant
6
+ include Urbanairship::Common
7
+ include Urbanairship::Loggable
8
+ attr_accessor :description,
9
+ :id,
10
+ :name,
11
+ :push,
12
+ :schedule,
13
+ :weight
14
+
15
+ def initialize(client: required('client'))
16
+ @client = client
17
+ end
18
+
19
+ def payload
20
+ fail ArgumentError, 'a push must be added to create a variant' if @push.nil?
21
+
22
+ {
23
+ 'description': description,
24
+ 'id': id,
25
+ 'name': name,
26
+ 'push': push,
27
+ 'schedule': schedule,
28
+ 'weight': weight
29
+ }.delete_if {|key, value| value.nil?} #this removes the nil key value pairs
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,105 @@
1
+ require 'uri'
2
+ require 'urbanairship'
3
+ require 'urbanairship/automations/pipeline'
4
+
5
+ module Urbanairship
6
+ module Automations
7
+ class Automation
8
+ include Urbanairship::Common
9
+ include Urbanairship::Loggable
10
+ attr_accessor :limit,
11
+ :enabled,
12
+ :offset,
13
+ :start,
14
+ :pipeline_id,
15
+ :pipeline_object
16
+
17
+ def initialize(client: required('client'))
18
+ @client = client
19
+ end
20
+
21
+ def create_automation
22
+ response = @client.send_request(
23
+ method: 'POST',
24
+ body: JSON.dump(pipeline_object),
25
+ path: pipelines_path,
26
+ content_type: 'application/json'
27
+ )
28
+ logger.info("Created Automation")
29
+ response
30
+ end
31
+
32
+ def list_automations
33
+ response = @client.send_request(
34
+ method: 'GET',
35
+ path: pipelines_path(format_url_with_params)
36
+ )
37
+ logger.info("Looking up automations for project")
38
+ response
39
+ end
40
+
41
+ def list_deleted_automations
42
+ response = @client.send_request(
43
+ method: 'GET',
44
+ path: pipelines_path('deleted' + format_url_with_params)
45
+ )
46
+ logger.info("Looking up deleted automations for project")
47
+ response
48
+ end
49
+
50
+ def validate_automation
51
+ response = @client.send_request(
52
+ method: 'POST',
53
+ body: JSON.dump(pipeline_object),
54
+ path: pipelines_path('validate'),
55
+ content_type: 'application/json'
56
+ )
57
+ logger.info("Validating Automation")
58
+ response
59
+ end
60
+
61
+ def lookup_automation
62
+ fail ArgumentError, 'pipeline_id must be set to lookup individual automation' if @pipeline_id.nil?
63
+ response = @client.send_request(
64
+ method: 'GET',
65
+ path: pipelines_path(pipeline_id)
66
+ )
67
+ logger.info("Looking up automation with id #{pipeline_id}")
68
+ response
69
+ end
70
+
71
+ def update_automation
72
+ fail ArgumentError, 'pipeline_id must be set to update individual automation' if @pipeline_id.nil?
73
+
74
+ response = @client.send_request(
75
+ method: 'PUT',
76
+ body: JSON.dump(pipeline_object),
77
+ path: pipelines_path(pipeline_id),
78
+ content_type: 'application/json'
79
+ )
80
+ logger.info("Validating Automation")
81
+ response
82
+ end
83
+
84
+ def delete_automation
85
+ fail ArgumentError, 'pipeline_id must be set to delete individual automation' if @pipeline_id.nil?
86
+ response = @client.send_request(
87
+ method: 'DELETE',
88
+ path: pipelines_path(pipeline_id)
89
+ )
90
+ logger.info("Deleting automation with id #{pipeline_id}")
91
+ response
92
+ end
93
+
94
+ def format_url_with_params
95
+ params = []
96
+ params << ['limit', limit] if limit
97
+ params << ['enabled', enabled] if enabled
98
+ params << ['offset', offset] if offset
99
+ params << ['start', start] if start
100
+ query = URI.encode_www_form(params)
101
+ '?' + query
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,52 @@
1
+ require 'urbanairship'
2
+
3
+ module Urbanairship
4
+ module Automations
5
+ class Pipeline
6
+ include Urbanairship::Common
7
+ include Urbanairship::Loggable
8
+ attr_accessor :activation_time,
9
+ :cancellation_trigger,
10
+ :condition,
11
+ :constraint,
12
+ :creation_time,
13
+ :deactivation_time,
14
+ :historical_trigger,
15
+ :immediate_trigger,
16
+ :last_modified_time,
17
+ :name,
18
+ :status,
19
+ :timing,
20
+ :url,
21
+ :enabled,
22
+ :outcome
23
+
24
+ def initialize(client: required('client'))
25
+ @client = client
26
+ end
27
+
28
+ def payload
29
+ fail ArgumentError, 'enabled must be set to create pipeline payload' if @enabled.nil?
30
+ fail ArgumentError, 'outcome must be set to create pipeline payload' if @outcome.nil?
31
+ {
32
+ activation_time: activation_time,
33
+ cancellation_trigger: cancellation_trigger,
34
+ condition: condition,
35
+ constraint: constraint,
36
+ creation_time: creation_time,
37
+ deactivation_time: deactivation_time,
38
+ enabled: enabled,
39
+ historical_trigger: historical_trigger,
40
+ immediate_trigger: immediate_trigger,
41
+ last_modified_time: last_modified_time,
42
+ name: name,
43
+ outcome: outcome,
44
+ status: status,
45
+ timing: timing,
46
+ url: url
47
+ }.delete_if {|key, value| value.nil?} #this removes the nil key value pairs
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -1,11 +1,12 @@
1
1
  require 'json'
2
2
  require 'rest-client'
3
3
  require 'urbanairship'
4
+ require 'jwt'
4
5
 
5
6
 
6
7
  module Urbanairship
7
8
  class Client
8
- attr_accessor :key, :secret
9
+ attr_accessor :key, :secret, :token
9
10
  include Urbanairship::Common
10
11
  include Urbanairship::Loggable
11
12
 
@@ -13,22 +14,35 @@ module Urbanairship
13
14
  #
14
15
  # @param [Object] key Application Key
15
16
  # @param [Object] secret Application Secret
17
+ # @param [String] server Airship server to use ("api.asnapieu.com" for EU or "api.asnapius.com" for US).
18
+ # Used only when the request is sent with a "path", not an "url".
19
+ # @param [String] token Application Auth Token
20
+ # @param [Object] oauth Oauth object
16
21
  # @return [Object] Client
17
- def initialize(key: required('key'), secret: required('secret'))
22
+ def initialize(key: required('key'), secret: nil, server: Urbanairship.configuration.server, token: nil, oauth: nil)
18
23
  @key = key
19
24
  @secret = secret
25
+ @server = server
26
+ @token = token
27
+ @oauth = oauth
28
+
29
+ if @oauth != nil && @token != nil
30
+ raise ArgumentError.new("oauth and token can't both be used at the same time.")
31
+ end
20
32
  end
21
33
 
22
- # Send a request to Urban Airship's API
34
+ # Send a request to Airship's API
23
35
  #
24
36
  # @param [Object] method HTTP Method
25
37
  # @param [Object] body Request Body
38
+ # @param [Object] path Request path
26
39
  # @param [Object] url Request URL
27
40
  # @param [Object] content_type Content-Type
28
- # @param [Object] version API Version
41
+ # @param [Object] encoding Encoding
42
+ # @param [Symbol] auth_type (:basic|:bearer)
29
43
  # @return [Object] Push Response
30
- def send_request(method: required('method'), url: required('url'), body: nil,
31
- content_type: nil, encoding: nil)
44
+ def send_request(method: required('method'), path: nil, url: nil, body: nil,
45
+ content_type: nil, encoding: nil, auth_type: :basic)
32
46
  req_type = case method
33
47
  when 'GET'
34
48
  :get
@@ -42,13 +56,36 @@ module Urbanairship
42
56
  fail 'Method was not "GET" "POST" "PUT" or "DELETE"'
43
57
  end
44
58
 
45
- headers = {'User-agent' => 'UARubyLib/' + Urbanairship::VERSION}
59
+ raise ArgumentError.new("path and url can't be both nil") if path.nil? && url.nil?
60
+
61
+ headers = {'User-Agent' => 'UARubyLib/' + Urbanairship::VERSION + ' ' + @key}
46
62
  headers['Accept'] = 'application/vnd.urbanairship+json; version=3'
47
- headers['Content-type'] = content_type unless content_type.nil?
63
+ headers['Content-Type'] = content_type unless content_type.nil?
48
64
  headers['Content-Encoding'] = encoding unless encoding.nil?
49
65
 
50
- debug = "Making #{method} request to #{url}.\n"+
51
- "\tHeaders:\n"
66
+ unless @oauth.nil?
67
+ begin
68
+ @token = @oauth.get_token
69
+ rescue RestClient::Exception => e
70
+ new_error = RestClient::Exception.new(e.response, e.response.code)
71
+ new_error.message = "error while getting oauth token: #{e.message}"
72
+ raise new_error
73
+ end
74
+ end
75
+
76
+ if @token != nil
77
+ auth_type = :bearer
78
+ end
79
+
80
+ if auth_type == :bearer
81
+ raise ArgumentError.new('token must be provided as argument if auth_type=bearer') if @token.nil?
82
+ headers['X-UA-Appkey'] = @key
83
+ headers['Authorization'] = "Bearer #{@token}"
84
+ end
85
+
86
+ url = "https://#{@server}/api#{path}" unless path.nil?
87
+
88
+ debug = "Making #{method} request to #{url}.\n"+ "\tHeaders:\n"
52
89
  debug += "\t\tcontent-type: #{content_type}\n" unless content_type.nil?
53
90
  debug += "\t\tcontent-encoding: gzip\n" unless encoding.nil?
54
91
  debug += "\t\taccept: application/vnd.urbanairship+json; version=3\n"
@@ -56,15 +93,21 @@ module Urbanairship
56
93
 
57
94
  logger.debug(debug)
58
95
 
59
- response = RestClient::Request.execute(
96
+ params = {
60
97
  method: method,
61
98
  url: url,
62
99
  headers: headers,
63
- user: @key,
64
- password: @secret,
65
100
  payload: body,
66
101
  timeout: Urbanairship.configuration.timeout
67
- )
102
+ }
103
+
104
+ if auth_type == :basic
105
+ raise ArgumentError.new('secret must be provided as argument if auth_type=basic') if @secret.nil?
106
+ params[:user] = @key
107
+ params[:password] = @secret
108
+ end
109
+
110
+ response = RestClient::Request.execute(params)
68
111
 
69
112
  logger.debug("Received #{response.code} response. Headers:\n\t#{response.headers}\nBody:\n\t#{response.body}")
70
113
  Response.check_code(response.code, response)
@@ -4,21 +4,67 @@ require 'urbanairship/loggable'
4
4
  module Urbanairship
5
5
  # Features mixed in to all classes
6
6
  module Common
7
- SERVER = 'go.urbanairship.com'
8
- BASE_URL = 'https://go.urbanairship.com/api'
9
- CHANNEL_URL = BASE_URL + '/channels/'
10
- OPEN_CHANNEL_URL = BASE_URL + '/channels/open/'
11
- DEVICE_TOKEN_URL = BASE_URL + '/device_tokens/'
12
- APID_URL = BASE_URL + '/apids/'
13
- PUSH_URL = BASE_URL + '/push/'
14
- SCHEDULES_URL = BASE_URL + '/schedules/'
15
- SEGMENTS_URL = BASE_URL + '/segments/'
16
- NAMED_USER_URL = BASE_URL + '/named_users/'
17
- REPORTS_URL = BASE_URL + '/reports/'
18
- LISTS_URL = BASE_URL + '/lists/'
19
- PIPELINES_URL = BASE_URL + '/pipelines/'
20
- FEEDS_URL = BASE_URL + '/feeds/'
21
- LOCATION_URL = BASE_URL + '/location/'
7
+ CONTENT_TYPE = 'application/json'
8
+
9
+ def apid_path(path='')
10
+ "/apids/#{path}"
11
+ end
12
+
13
+ def channel_path(path='')
14
+ "/channels/#{path}"
15
+ end
16
+
17
+ def create_and_send_path(path='')
18
+ "/create-and-send/#{path}"
19
+ end
20
+
21
+ def custom_events_path(path='')
22
+ "/custom-events/#{path}"
23
+ end
24
+
25
+ def device_token_path(path='')
26
+ "/device_tokens/#{path}"
27
+ end
28
+
29
+ def experiments_path(path='')
30
+ "/experiments/#{path}"
31
+ end
32
+
33
+ def lists_path(path='')
34
+ "/lists/#{path}"
35
+ end
36
+
37
+ def named_users_path(path='')
38
+ "/named_users/#{path}"
39
+ end
40
+
41
+ def open_channel_path(path='')
42
+ "/open/#{path}"
43
+ end
44
+
45
+ def pipelines_path(path='')
46
+ "/pipelines/#{path}"
47
+ end
48
+
49
+ def push_path(path='')
50
+ "/push/#{path}"
51
+ end
52
+
53
+ def reports_path(path='')
54
+ "/reports/#{path}"
55
+ end
56
+
57
+ def schedules_path(path='')
58
+ "/schedules/#{path}"
59
+ end
60
+
61
+ def segments_path(path='')
62
+ "/segments/#{path}"
63
+ end
64
+
65
+ def tag_lists_path(path='')
66
+ "/tag-lists/#{path}"
67
+ end
22
68
 
23
69
  # Helper method for required keyword args in Ruby 2.0 that is compatible with 2.1+
24
70
  # @example
@@ -113,36 +159,18 @@ module Urbanairship
113
159
 
114
160
  def initialize(client: required('client'))
115
161
  @client = client
116
- @next_page = nil
117
- @data_list = nil
118
- @data_attribute = nil
119
162
  @count = 0
120
- end
121
-
122
- def load_page
123
- return false unless @next_page
124
- response = @client.send_request(
125
- method: 'GET',
126
- url: @next_page
127
- )
128
- logger.info("Retrieving data from: #{@next_page}")
129
- check_next_page = response['body']['next_page']
130
- if check_next_page != @next_page
131
- @next_page = check_next_page
132
- elsif check_next_page
133
- # if check_page = next_page, we have repeats in the response.
134
- # and we don't want to load them
135
- return false
136
- else
137
- @next_page = nil
138
- end
139
- @data_list = response['body'][@data_attribute]
140
- true
163
+ @data_attribute = nil
164
+ @data_list = nil
165
+ @next_page_path = nil
166
+ @next_page_url = nil
141
167
  end
142
168
 
143
169
  def each
144
- while load_page
145
- @data_list.each do | value |
170
+ while @next_page_path || @next_page_url
171
+ load_page
172
+
173
+ @data_list.each do |value|
146
174
  @count += 1
147
175
  yield value
148
176
  end
@@ -152,6 +180,47 @@ module Urbanairship
152
180
  def count
153
181
  @count
154
182
  end
183
+
184
+ private
185
+
186
+ def load_page
187
+ logger.info("Retrieving data from: #{@next_page_url || @next_page_path}")
188
+ params = {
189
+ method: 'GET',
190
+ path: @next_page_path,
191
+ url: @next_page_url
192
+ }.select { |k, v| !v.nil? }
193
+ response = @client.send_request(params)
194
+
195
+ @data_list = get_new_data(response)
196
+ @next_page_url = get_next_page_url(response)
197
+ @next_page_path = nil
198
+ end
199
+
200
+ def extract_next_page_url(response)
201
+ response['body']['next_page']
202
+ end
203
+
204
+ def get_new_data(response)
205
+ potential_next_page_url = extract_next_page_url(response)
206
+
207
+ # if potential_next_page_url is the same as the current page, we have
208
+ # repeats in the response and we don't want to load them
209
+ return [] if potential_next_page_url && get_next_page_url(response).nil?
210
+
211
+ response['body'][@data_attribute]
212
+ end
213
+
214
+ def get_next_page_url(response)
215
+ potential_next_page_url = extract_next_page_url(response)
216
+ return nil if potential_next_page_url.nil?
217
+
218
+ # if potential_next_page_url is the same as the current page, we have
219
+ # repeats in the response and we don't want to check the next pages
220
+ return potential_next_page_url if @next_page_url && potential_next_page_url != @next_page_url
221
+ return potential_next_page_url if @next_page_path && !potential_next_page_url.end_with?(@next_page_path)
222
+ nil
223
+ end
155
224
  end
156
225
  end
157
226
  end
@@ -1,8 +1,10 @@
1
1
  module Urbanairship
2
2
  class Configuration
3
- attr_accessor :custom_logger, :log_path, :log_level, :timeout
3
+ attr_accessor :custom_logger, :log_path, :log_level, :server, :oauth_server, :timeout
4
4
 
5
5
  def initialize
6
+ @server = 'api.asnapius.com'
7
+ @oauth_server = 'oauth2.asnapius.com'
6
8
  @custom_logger = nil
7
9
  @log_path = nil
8
10
  @log_level = Logger::INFO
@@ -0,0 +1,60 @@
1
+ require 'uri'
2
+ require 'urbanairship'
3
+ require 'urbanairship/common'
4
+ require 'urbanairship/loggable'
5
+
6
+ module Urbanairship
7
+ module CustomEvents
8
+ class CustomEvent
9
+ include Urbanairship::Common
10
+ include Urbanairship::Loggable
11
+
12
+ attr_accessor :events
13
+
14
+ def initialize(client: required('client'))
15
+ @client = client
16
+ end
17
+
18
+ def create
19
+ fail ArgumentError, 'events must be an array of custom events' unless events.is_a?(Array)
20
+
21
+ response = @client.send_request(
22
+ auth_type: :bearer,
23
+ body: JSON.dump(events),
24
+ content_type: 'application/json',
25
+ method: 'POST',
26
+ path: custom_events_path
27
+ )
28
+ cer = CustomEventResponse.new(body: response['body'], code: response['code'])
29
+ logger.info { cer.format }
30
+
31
+ cer
32
+ end
33
+ end
34
+
35
+ # Response to a successful custom event creation.
36
+ class CustomEventResponse
37
+ attr_reader :ok, :operation_id, :payload, :status_code
38
+
39
+ def initialize(body: nil, code: nil)
40
+ @payload = (body.nil? || body.empty?) ? {} : body
41
+ @ok = payload['ok']
42
+ @operation_id = payload['operationId']
43
+ @status_code = code
44
+ end
45
+
46
+ # String Formatting of the CustomEventResponse
47
+ #
48
+ # @return [Object] String Formatted CustomEventResponse
49
+ def format
50
+ "Received [#{status_code}] response code.\nBody:\n#{formatted_body}"
51
+ end
52
+
53
+ def formatted_body
54
+ payload
55
+ .map { |key, value| "#{key}:\t#{value.to_s || 'None'}" }
56
+ .join("\n")
57
+ end
58
+ end
59
+ end
60
+ end