urbanairship 2.4.1 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CHANGELOG +13 -0
- data/Gemfile +3 -0
- data/Guardfile +14 -0
- data/LICENSE +10 -26
- data/README.md +96 -0
- data/Rakefile +6 -7
- data/bin/console +7 -0
- data/bin/setup +5 -0
- data/lib/ext/hash.rb +5 -0
- data/lib/ext/object.rb +5 -0
- data/lib/urbanairship.rb +16 -209
- data/lib/urbanairship/client.rb +84 -0
- data/lib/urbanairship/common.rb +85 -0
- data/lib/urbanairship/loggable.rb +18 -0
- data/lib/urbanairship/push/audience.rb +150 -0
- data/lib/urbanairship/push/payload.rb +144 -0
- data/lib/urbanairship/push/push.rb +192 -0
- data/lib/urbanairship/push/schedule.rb +23 -0
- data/lib/urbanairship/util.rb +15 -0
- data/lib/urbanairship/version.rb +3 -0
- data/urbanairship.gemspec +38 -0
- metadata +112 -45
- data/README.markdown +0 -369
- data/lib/urbanairship/response.rb +0 -33
- data/spec/response_spec.rb +0 -79
- data/spec/spec_helper.rb +0 -17
- data/spec/urbanairship_spec.rb +0 -1090
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'unirest'
|
2
|
+
require 'urbanairship/common'
|
3
|
+
require 'urbanairship/loggable'
|
4
|
+
|
5
|
+
module Urbanairship
|
6
|
+
class Client
|
7
|
+
attr_accessor :key, :secret
|
8
|
+
include Urbanairship::Common
|
9
|
+
include Urbanairship::Loggable
|
10
|
+
|
11
|
+
# set default client timeout to 5 seconds
|
12
|
+
Unirest.timeout(5)
|
13
|
+
|
14
|
+
# Initialize the Client
|
15
|
+
#
|
16
|
+
# @param [Object] key Application Key
|
17
|
+
# @param [Object] secret Application Secret
|
18
|
+
# @return [Object] Client
|
19
|
+
def initialize(key: required('key'), secret: required('secret'))
|
20
|
+
@key = key
|
21
|
+
@secret = secret
|
22
|
+
end
|
23
|
+
|
24
|
+
# Send a request to Urban Airship's API
|
25
|
+
#
|
26
|
+
# @param [Object] method HTTP Method
|
27
|
+
# @param [Object] body Request Body
|
28
|
+
# @param [Object] url Request URL
|
29
|
+
# @param [Object] content_type Content-Type
|
30
|
+
# @param [Object] version API Version
|
31
|
+
# @param [Object] params Parameters
|
32
|
+
# @return [Object] Push Response
|
33
|
+
def send_request(method: required('method'), body: required('body'), url: required('url'),
|
34
|
+
content_type: nil, version: nil, params: nil)
|
35
|
+
req_type = case method
|
36
|
+
when 'GET'
|
37
|
+
:get
|
38
|
+
when 'POST'
|
39
|
+
:post
|
40
|
+
when 'PUT'
|
41
|
+
:put
|
42
|
+
when 'DELETE'
|
43
|
+
:delete
|
44
|
+
else
|
45
|
+
fail 'Method was not "GET" "POST" "PUT" or "DELETE"'
|
46
|
+
end
|
47
|
+
|
48
|
+
logger.debug("Making #{method} request to #{url}. \n\tHeaders:\n\tcontent-type: #{content_type}\n\tversion=#{version.to_s}\nBody:\n\t#{body}")
|
49
|
+
|
50
|
+
response = Unirest.method(req_type).call(
|
51
|
+
url,
|
52
|
+
headers:{
|
53
|
+
"Content-type" => content_type,
|
54
|
+
"Accept" => "application/vnd.urbanairship+json; version=" + version.to_s
|
55
|
+
},
|
56
|
+
auth:{
|
57
|
+
:user=>@key,
|
58
|
+
:password=>@secret
|
59
|
+
},
|
60
|
+
parameters: body
|
61
|
+
)
|
62
|
+
|
63
|
+
logger.debug("Received #{response.code} response. Headers:\n\t#{response.headers}\nBody:\n\t#{response.body}")
|
64
|
+
|
65
|
+
Response.check_code(response.code, response)
|
66
|
+
|
67
|
+
{'body'=>response.body, 'code'=>response.code}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create a Push Object
|
71
|
+
#
|
72
|
+
# @return [Object] Push Object
|
73
|
+
def create_push
|
74
|
+
Push::Push.new(self)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create a Scheduled Push Object
|
78
|
+
#
|
79
|
+
# @return [Object] Scheduled Push Object
|
80
|
+
def create_scheduled_push
|
81
|
+
Push::ScheduledPush.new(self)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'urbanairship/loggable'
|
2
|
+
|
3
|
+
module Urbanairship
|
4
|
+
# Features mixed in to all classes
|
5
|
+
module Common
|
6
|
+
SERVER = 'go.urbanairship.com'
|
7
|
+
BASE_URL = 'https://go.urbanairship.com/api'
|
8
|
+
CHANNEL_URL = BASE_URL + '/channels/'
|
9
|
+
DEVICE_TOKEN_URL = BASE_URL + '/device_tokens/'
|
10
|
+
APID_URL = BASE_URL + '/apids/'
|
11
|
+
DEVICE_PIN_URL = BASE_URL + '/device_pins/'
|
12
|
+
PUSH_URL = BASE_URL + '/push/'
|
13
|
+
DT_FEEDBACK_URL = BASE_URL + '/device_tokens/feedback/'
|
14
|
+
APID_FEEDBACK_URL = BASE_URL + '/apids/feedback/'
|
15
|
+
SCHEDULES_URL = BASE_URL + '/schedules/'
|
16
|
+
TAGS_URL = BASE_URL + '/tags/'
|
17
|
+
SEGMENTS_URL = BASE_URL + '/segments/'
|
18
|
+
|
19
|
+
# Helper method for required keyword args in 2.0 that is compatible with 2.1+
|
20
|
+
# @example
|
21
|
+
# def say(greeting: required('greeting'))
|
22
|
+
# puts greeting
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# >> say
|
26
|
+
# >> test.rb:3:in `required': required parameter :greeting not passed to method say (ArgumentError)
|
27
|
+
# >> from test.rb:6:in `say'
|
28
|
+
# >> from test.rb:18:in `<main>'
|
29
|
+
# @param [Object] arg optional argument name
|
30
|
+
def required(arg=nil)
|
31
|
+
method = caller_locations(1,1)[0].label
|
32
|
+
raise ArgumentError.new("required parameter #{arg.to_sym.inspect + ' ' if arg}not passed to method #{method}")
|
33
|
+
end
|
34
|
+
|
35
|
+
class Unauthorized < StandardError
|
36
|
+
# raised when we get a 401 from server
|
37
|
+
end
|
38
|
+
|
39
|
+
class Forbidden < StandardError
|
40
|
+
# raised when we get a 403 from server
|
41
|
+
end
|
42
|
+
|
43
|
+
class AirshipFailure < StandardError
|
44
|
+
include Urbanairship::Loggable
|
45
|
+
# Raised when we get an error response from the server.
|
46
|
+
attr_accessor :error, :error_code, :details, :response
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@error = nil
|
50
|
+
@error_code = nil
|
51
|
+
@details = nil
|
52
|
+
@response = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# Instantiate a ValidationFailure from a Response object
|
56
|
+
def from_response(response)
|
57
|
+
|
58
|
+
payload = response.body
|
59
|
+
@error = payload['error']
|
60
|
+
@error_code = payload['error_code']
|
61
|
+
@details = payload['details']
|
62
|
+
@response = response
|
63
|
+
|
64
|
+
logger.error("Request failed with status #{response.code.to_s}: '#{@error_code} #{@error}': #{response.body}")
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
class Response
|
72
|
+
# Parse Response Codes and trigger appropriate actions.
|
73
|
+
def self.check_code(response_code, response)
|
74
|
+
if response_code == 401
|
75
|
+
raise Unauthorized, "Client is not authorized to make this request. The authorization credentials are incorrect or missing."
|
76
|
+
elsif response_code == 403
|
77
|
+
raise Forbidden, "Client is not forbidden from making this request. The application does not have the proper entitlement to access this feature."
|
78
|
+
elsif !((200...300).include?(response_code))
|
79
|
+
raise AirshipFailure.new.from_response(response)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Urbanairship
|
4
|
+
module Loggable
|
5
|
+
|
6
|
+
def logger
|
7
|
+
@logger ||= create_logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_logger
|
11
|
+
logger = Logger.new('urbanairship.log')
|
12
|
+
logger.datetime_format = '%Y-%m-%d %H:%M:%S'
|
13
|
+
logger.progname = 'Urbanairship'
|
14
|
+
logger
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'urbanairship/util'
|
2
|
+
require 'urbanairship/common'
|
3
|
+
|
4
|
+
module Urbanairship
|
5
|
+
module Push
|
6
|
+
module Audience
|
7
|
+
include Urbanairship::Common
|
8
|
+
UUID_PATTERN = /^\h{8}-\h{4}-\h{4}-\h{4}-\h{12}$/
|
9
|
+
DEVICE_TOKEN_PATTERN = /^\h{64}$/
|
10
|
+
DEVICE_PIN_PATTERN = /^\h{8}$/
|
11
|
+
DATE_TERMS = %i(minutes hours days weeks months years)
|
12
|
+
|
13
|
+
|
14
|
+
# Methods to select a single iOS Channel, Android Channel, Amazon Channel,
|
15
|
+
# Android APID, Windows 8 APID, or Windows Phone 8 APID respectively.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# ios_channel(<channel>) # ==>
|
19
|
+
# {:ios_channel=>"<channel>"}
|
20
|
+
%w(ios_channel android_channel amazon_channel apid wns mpns).each do |name|
|
21
|
+
define_method(name) do |uuid|
|
22
|
+
{ name.to_sym => cleanup(uuid) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Select a single iOS device token
|
27
|
+
def device_token(token)
|
28
|
+
Util.validate(token, 'device_token', DEVICE_TOKEN_PATTERN)
|
29
|
+
{ device_token: token.upcase.strip }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Select a single BlackBerry PIN
|
33
|
+
def device_pin(pin)
|
34
|
+
Util.validate(pin, 'pin', DEVICE_PIN_PATTERN)
|
35
|
+
{ device_pin: pin.downcase.strip }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Select a single tag
|
39
|
+
def tag(tag)
|
40
|
+
{ tag: tag }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Select a single alias
|
44
|
+
def alias(an_alias)
|
45
|
+
{ alias: an_alias }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Select a single segment using segment_id
|
49
|
+
def segment(segment)
|
50
|
+
{ segment: segment }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Select devices that match at least one of the given selectors.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# or(tag('sports'), tag('business')) # ==>
|
57
|
+
# {or: [{tag: 'sports'}, {tag: 'business'}]}
|
58
|
+
def or(*children)
|
59
|
+
{ or: children }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Select devices that match all of the given selectors.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# and(tag('sports'), tag('business')) # ==>
|
66
|
+
# {and: [{tag: 'sports'}, {tag: 'business'}]}
|
67
|
+
def and(*children)
|
68
|
+
{ and: children }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Select devices that do not match the given selectors.
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# not(and_(tag('sports'), tag('business'))) # ==>
|
75
|
+
# {not: {and: [{tag: 'sports'}, {tag: 'business'}]}}
|
76
|
+
def not(child)
|
77
|
+
{ not: child }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Select a recent date range for a location selector.
|
81
|
+
# Valid selectors are:
|
82
|
+
# :minutes :hours :days :weeks :months :years
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# recent_date(months: 6) # => { recent: { months: 6 }}
|
86
|
+
# recent_date(weeks: 3) # => { recent: { weeks: 3 }}
|
87
|
+
def recent_date(**params)
|
88
|
+
fail ArgumentError, 'Only one range allowed' if params.size != 1
|
89
|
+
k, v = params.first
|
90
|
+
unless DATE_TERMS.include?(k)
|
91
|
+
fail ArgumentError, "#{k} not in #{DATE_TERMS}"
|
92
|
+
end
|
93
|
+
{ recent: { k => v } }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Select an absolute date range for a location selector.
|
97
|
+
#
|
98
|
+
# @param resolution [Symbol] Time resolution specifier, one of
|
99
|
+
# :minutes :hours :days :weeks :months :years
|
100
|
+
# @param start [String] UTC start time in ISO 8601 format.
|
101
|
+
# @param the_end [String] UTC end time in ISO 8601 format.
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# absolute_date(resolution: :months, start: '2013-01', the_end: '2013-06')
|
105
|
+
# #=> {months: {end: '2013-06', start: '2013-01'}}
|
106
|
+
#
|
107
|
+
# absolute_date(resolution: :minutes, start: '2012-01-01 12:00',
|
108
|
+
# the_end: '2012-01-01 12:45')
|
109
|
+
# #=> {minutes: {end: '2012-01-01 12:45', start: '2012-01-01 12:00'}}
|
110
|
+
def absolute_date(resolution: required('resolution'), start: required('start'), the_end: required('the_end'))
|
111
|
+
unless DATE_TERMS.include?(resolution)
|
112
|
+
fail ArgumentError, "#{resolution} not in #{DATE_TERMS}"
|
113
|
+
end
|
114
|
+
{ resolution => { start: start, end: the_end } }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Select a location expression.
|
118
|
+
#
|
119
|
+
# Location selectors are made up of either an id or an alias and a date
|
120
|
+
# period specifier. Use a date specification function to generate the time
|
121
|
+
# period specifier.
|
122
|
+
#
|
123
|
+
# @example ID location
|
124
|
+
# location(id: '4oFkxX7RcUdirjtaenEQIV', date: recent_date(days: 4))
|
125
|
+
# #=> {location: {date: {recent: {days: 4}},
|
126
|
+
# id: '4oFkxX7RcUdirjtaenEQIV'}}
|
127
|
+
#
|
128
|
+
# @example Alias location
|
129
|
+
# location(us_zip: '94103', date: absolute_date(
|
130
|
+
# resolution: 'days', start: '2012-01-01', end: '2012-01-15'))
|
131
|
+
# #=> {location: {date: {days: {end: '2012-01-15',
|
132
|
+
# start: '2012-01-01'}}, us_zip: '94103'}}
|
133
|
+
def location(date: required('date'), **params)
|
134
|
+
unless params.size == 1
|
135
|
+
fail ArgumentError, 'One location specifier required'
|
136
|
+
end
|
137
|
+
params[:date] = date
|
138
|
+
{ location: params }
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# Clean up a UUID for use in the library
|
144
|
+
def cleanup(uuid)
|
145
|
+
Util.validate(uuid, 'UUID', UUID_PATTERN)
|
146
|
+
uuid.downcase.strip
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Urbanairship
|
2
|
+
module Push
|
3
|
+
module Payload
|
4
|
+
require 'ext/hash'
|
5
|
+
include Urbanairship::Common
|
6
|
+
|
7
|
+
# Notification Object for a Push Payload
|
8
|
+
def notification(alert: nil, ios: nil, android: nil, amazon: nil,
|
9
|
+
blackberry: nil, wns: nil, mpns: nil, actions: nil,
|
10
|
+
interactive: nil)
|
11
|
+
payload = {
|
12
|
+
alert: alert,
|
13
|
+
actions: actions,
|
14
|
+
ios: ios,
|
15
|
+
android: android,
|
16
|
+
amazon: amazon,
|
17
|
+
blackberry: blackberry,
|
18
|
+
wns: wns,
|
19
|
+
mpns: mpns,
|
20
|
+
interactive: interactive
|
21
|
+
}.compact
|
22
|
+
fail ArgumentError, 'Notification body is empty' if payload.empty?
|
23
|
+
payload
|
24
|
+
end
|
25
|
+
|
26
|
+
# iOS specific portion of Push Notification Object
|
27
|
+
def ios(alert: nil, badge: nil, sound: nil, extra: nil, expiry: nil,
|
28
|
+
category: nil, interactive: nil, content_available: nil)
|
29
|
+
{
|
30
|
+
alert: alert,
|
31
|
+
badge: badge,
|
32
|
+
sound: sound,
|
33
|
+
extra: extra,
|
34
|
+
expiry: expiry,
|
35
|
+
category: category,
|
36
|
+
interactive: interactive,
|
37
|
+
'content-available' => content_available
|
38
|
+
}.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
# Amazon specific portion of Push Notification Object
|
42
|
+
def amazon(alert: nil, consolidation_key: nil, expires_after: nil,
|
43
|
+
extra: nil, title: nil, summary: nil, interactive: nil)
|
44
|
+
{
|
45
|
+
alert: alert,
|
46
|
+
consolidation_key: consolidation_key,
|
47
|
+
expires_after: expires_after,
|
48
|
+
extra: extra,
|
49
|
+
title: title,
|
50
|
+
summary: summary,
|
51
|
+
interactive: interactive
|
52
|
+
}.compact
|
53
|
+
end
|
54
|
+
|
55
|
+
# Android specific portion of Push Notification Object
|
56
|
+
def android(alert: nil, collapse_key: nil, time_to_live: nil,
|
57
|
+
extra: nil, delay_while_idle: nil, interactive: nil)
|
58
|
+
{
|
59
|
+
alert: alert,
|
60
|
+
collapse_key: collapse_key,
|
61
|
+
time_to_live: time_to_live,
|
62
|
+
extra: extra,
|
63
|
+
delay_while_idle: delay_while_idle,
|
64
|
+
interactive: interactive
|
65
|
+
}.compact
|
66
|
+
end
|
67
|
+
|
68
|
+
# BlackBerry specific portion of Push Notification Object
|
69
|
+
def blackberry(alert: nil, body: nil, content_type: 'text/plain')
|
70
|
+
{ body: alert || body, content_type: content_type }
|
71
|
+
end
|
72
|
+
|
73
|
+
# WNS specific portion of Push Notification Object
|
74
|
+
def wns_payload(alert: nil, toast: nil, tile: nil, badge: nil)
|
75
|
+
payload = {
|
76
|
+
alert: alert,
|
77
|
+
toast: toast,
|
78
|
+
tile: tile,
|
79
|
+
badge: badge
|
80
|
+
}.compact
|
81
|
+
fail ArgumentError, 'Must specify one message type' if payload.size != 1
|
82
|
+
payload
|
83
|
+
end
|
84
|
+
|
85
|
+
# MPNS specific portion of Push Notification Object
|
86
|
+
def mpns_payload(alert: nil, toast: nil, tile: nil)
|
87
|
+
payload = {
|
88
|
+
alert: alert,
|
89
|
+
toast: toast,
|
90
|
+
tile: tile
|
91
|
+
}.compact
|
92
|
+
fail ArgumentError, 'Must specify one message type' if payload.size != 1
|
93
|
+
payload
|
94
|
+
end
|
95
|
+
|
96
|
+
# Rich Message specific portion of Push Notification Object
|
97
|
+
def message(title: required('title'), body: required('body'), content_type: nil, content_encoding: nil,
|
98
|
+
extra: nil, expiry: nil, icons: nil, options: nil)
|
99
|
+
{
|
100
|
+
title: title,
|
101
|
+
body: body,
|
102
|
+
content_type: content_type,
|
103
|
+
content_encoding: content_encoding,
|
104
|
+
extra: extra,
|
105
|
+
expiry: expiry,
|
106
|
+
icons: icons,
|
107
|
+
options: options
|
108
|
+
}.compact
|
109
|
+
end
|
110
|
+
|
111
|
+
# Interactive Notification portion of Push Notification Object
|
112
|
+
def interactive(type: required('type'), button_actions: nil)
|
113
|
+
fail ArgumentError, 'type must not be nil' if type.nil?
|
114
|
+
{ type: type, button_actions: button_actions }.compact
|
115
|
+
end
|
116
|
+
|
117
|
+
def all
|
118
|
+
'all'
|
119
|
+
end
|
120
|
+
|
121
|
+
# Target specified device types
|
122
|
+
def device_types(types)
|
123
|
+
types
|
124
|
+
end
|
125
|
+
|
126
|
+
# Expiry for a Rich Message
|
127
|
+
def options(expiry: required('expiry'))
|
128
|
+
{ expiry: expiry }
|
129
|
+
end
|
130
|
+
|
131
|
+
# Actions for a Push Notification Object
|
132
|
+
def actions(add_tag: nil, remove_tag: nil, open_: nil, share: nil,
|
133
|
+
app_defined: nil)
|
134
|
+
{
|
135
|
+
add_tag: add_tag,
|
136
|
+
remove_tag: remove_tag,
|
137
|
+
open: open_,
|
138
|
+
share: share,
|
139
|
+
app_defined: app_defined
|
140
|
+
}.compact
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|