fcm-ruby-push-notifications 1.2.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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +37 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +19 -0
  5. data/CHANGELOG.md +32 -0
  6. data/Gemfile +6 -0
  7. data/LICENSE +22 -0
  8. data/README.md +82 -0
  9. data/Rakefile +12 -0
  10. data/examples/apns.rb +27 -0
  11. data/examples/gcm.rb +25 -0
  12. data/examples/mpns.rb +22 -0
  13. data/examples/wns.rb +25 -0
  14. data/gemfiles/Gemfile-legacy +8 -0
  15. data/lib/ruby-push-notifications.rb +7 -0
  16. data/lib/ruby-push-notifications/apns.rb +44 -0
  17. data/lib/ruby-push-notifications/apns/apns_connection.rb +70 -0
  18. data/lib/ruby-push-notifications/apns/apns_notification.rb +91 -0
  19. data/lib/ruby-push-notifications/apns/apns_pusher.rb +84 -0
  20. data/lib/ruby-push-notifications/apns/apns_results.rb +30 -0
  21. data/lib/ruby-push-notifications/fcm.rb +8 -0
  22. data/lib/ruby-push-notifications/fcm/fcm_connection.rb +60 -0
  23. data/lib/ruby-push-notifications/fcm/fcm_error.rb +17 -0
  24. data/lib/ruby-push-notifications/fcm/fcm_notification.rb +31 -0
  25. data/lib/ruby-push-notifications/fcm/fcm_pusher.rb +29 -0
  26. data/lib/ruby-push-notifications/fcm/fcm_request.rb +28 -0
  27. data/lib/ruby-push-notifications/fcm/fcm_response.rb +89 -0
  28. data/lib/ruby-push-notifications/fcm/fcm_result.rb +52 -0
  29. data/lib/ruby-push-notifications/gcm.rb +6 -0
  30. data/lib/ruby-push-notifications/gcm/gcm_connection.rb +54 -0
  31. data/lib/ruby-push-notifications/gcm/gcm_error.rb +17 -0
  32. data/lib/ruby-push-notifications/gcm/gcm_notification.rb +31 -0
  33. data/lib/ruby-push-notifications/gcm/gcm_pusher.rb +35 -0
  34. data/lib/ruby-push-notifications/gcm/gcm_response.rb +89 -0
  35. data/lib/ruby-push-notifications/gcm/gcm_result.rb +52 -0
  36. data/lib/ruby-push-notifications/mpns.rb +5 -0
  37. data/lib/ruby-push-notifications/mpns/mpns_connection.rb +87 -0
  38. data/lib/ruby-push-notifications/mpns/mpns_notification.rb +94 -0
  39. data/lib/ruby-push-notifications/mpns/mpns_pusher.rb +33 -0
  40. data/lib/ruby-push-notifications/mpns/mpns_response.rb +82 -0
  41. data/lib/ruby-push-notifications/mpns/mpns_result.rb +182 -0
  42. data/lib/ruby-push-notifications/notification_results_manager.rb +14 -0
  43. data/lib/ruby-push-notifications/wns.rb +6 -0
  44. data/lib/ruby-push-notifications/wns/wns_access.rb +82 -0
  45. data/lib/ruby-push-notifications/wns/wns_connection.rb +97 -0
  46. data/lib/ruby-push-notifications/wns/wns_notification.rb +101 -0
  47. data/lib/ruby-push-notifications/wns/wns_pusher.rb +32 -0
  48. data/lib/ruby-push-notifications/wns/wns_response.rb +83 -0
  49. data/lib/ruby-push-notifications/wns/wns_result.rb +208 -0
  50. data/ruby-push-notifications.gemspec +28 -0
  51. data/spec/factories.rb +17 -0
  52. data/spec/factories/notifications.rb +30 -0
  53. data/spec/ruby-push-notifications/apns/apns_connection_spec.rb +92 -0
  54. data/spec/ruby-push-notifications/apns/apns_notification_spec.rb +42 -0
  55. data/spec/ruby-push-notifications/apns/apns_pusher_spec.rb +295 -0
  56. data/spec/ruby-push-notifications/gcm/gcm_connection_spec.rb +46 -0
  57. data/spec/ruby-push-notifications/gcm/gcm_notification_spec.rb +37 -0
  58. data/spec/ruby-push-notifications/gcm/gcm_pusher_spec.rb +45 -0
  59. data/spec/ruby-push-notifications/gcm/gcm_response_spec.rb +82 -0
  60. data/spec/ruby-push-notifications/mpns/mpns_connection_spec.rb +46 -0
  61. data/spec/ruby-push-notifications/mpns/mpns_notification_spec.rb +53 -0
  62. data/spec/ruby-push-notifications/mpns/mpns_pusher_spec.rb +59 -0
  63. data/spec/ruby-push-notifications/mpns/mpns_response_spec.rb +64 -0
  64. data/spec/ruby-push-notifications/wns/wns_access_spec.rb +76 -0
  65. data/spec/ruby-push-notifications/wns/wns_connection_spec.rb +53 -0
  66. data/spec/ruby-push-notifications/wns/wns_notification_spec.rb +177 -0
  67. data/spec/ruby-push-notifications/wns/wns_pusher_spec.rb +58 -0
  68. data/spec/ruby-push-notifications/wns/wns_response_spec.rb +65 -0
  69. data/spec/spec_helper.rb +23 -0
  70. data/spec/support/dummy.pem +44 -0
  71. data/spec/support/factory_girl.rb +5 -0
  72. data/spec/support/results_shared_examples.rb +31 -0
  73. metadata +249 -0
@@ -0,0 +1,52 @@
1
+
2
+ module RubyPushNotifications
3
+ module FCM
4
+ # Class that encapsulates the result of a single sent notification to a single
5
+ # Registration ID
6
+ # (https://developer.android.com/google/gcm/server-ref.html#table4)
7
+ # @author Carlos Alonso
8
+ class FCMResult ; end
9
+
10
+ # Indicates that the notification was successfully sent to the corresponding
11
+ # registration ID
12
+ class FCMResultOK < FCMResult
13
+
14
+ def ==(other)
15
+ other.is_a?(FCMResultOK) || super(other)
16
+ end
17
+ end
18
+
19
+ # Indicates that the notification was successfully sent to the corresponding
20
+ # registration ID but GCM sent a canonical ID for it, so the received canonical
21
+ # ID should be used as registration ID ASAP.
22
+ class FCMCanonicalIDResult < FCMResult
23
+
24
+ # @return [String]. The suggested canonical ID from GCM
25
+ attr_reader :canonical_id
26
+
27
+ def initialize(canonical_id)
28
+ @canonical_id = canonical_id
29
+ end
30
+
31
+ def ==(other)
32
+ (other.is_a?(FCMCanonicalIDResult) && @canonical_id == other.canonical_id) ||
33
+ super(other)
34
+ end
35
+ end
36
+
37
+ # An error occurred sending the notification to the registration ID.
38
+ class FCMResultError < FCMResult
39
+
40
+ # @return [String]. The error sent by GCM
41
+ attr_accessor :error
42
+
43
+ def initialize(error)
44
+ @error = error
45
+ end
46
+
47
+ def ==(other)
48
+ (other.is_a?(FCMResultError) && @error == other.error) || super(other)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,6 @@
1
+ require 'ruby-push-notifications/gcm/gcm_connection'
2
+ require 'ruby-push-notifications/gcm/gcm_notification'
3
+ require 'ruby-push-notifications/gcm/gcm_pusher'
4
+ require 'ruby-push-notifications/gcm/gcm_response'
5
+ require 'ruby-push-notifications/gcm/gcm_error'
6
+ require 'ruby-push-notifications/gcm/gcm_result'
@@ -0,0 +1,54 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ module RubyPushNotifications
5
+ module GCM
6
+ # Encapsulates a connection to the GCM service
7
+ # Responsible for final connection with the service.
8
+ #
9
+ # @author Carlos Alonso
10
+ class GCMConnection
11
+
12
+ # @private The URL of the Android GCM endpoint
13
+ GCM_URL = 'https://android.googleapis.com/gcm/send'
14
+
15
+ # @private Content-Type HTTP Header string
16
+ CONTENT_TYPE_HEADER = 'Content-Type'
17
+
18
+ # @private Application/JSON content type
19
+ JSON_CONTENT_TYPE = 'application/json'
20
+
21
+ # @private Authorization HTTP Header String
22
+ AUTHORIZATION_HEADER = 'Authorization'
23
+
24
+ # Issues a POST request to the GCM send endpoint to
25
+ # submit the given notifications.
26
+ #
27
+ # @param notification [String]. The text to POST
28
+ # @param key [String]. The GCM sender id to use
29
+ # (https://developer.android.com/google/gcm/gcm.html#senderid)
30
+ # @param options [Hash] optional. Options for #post. Currently supports:
31
+ # * url [String]: URL of the GCM endpoint. Defaults to the official GCM URL.
32
+ # * open_timeout [Integer]: Number of seconds to wait for the connection to open. Defaults to 30.
33
+ # * read_timeout [Integer]: Number of seconds to wait for one block to be read. Defaults to 30.
34
+ # @return [GCMResponse]. The GCMResponse that encapsulates the received response
35
+ def self.post(notification, key, options = {})
36
+ headers = {
37
+ CONTENT_TYPE_HEADER => JSON_CONTENT_TYPE,
38
+ AUTHORIZATION_HEADER => "key=#{key}"
39
+ }
40
+
41
+ url = URI.parse options.fetch(:url, GCM_URL)
42
+ http = Net::HTTP.new url.host, url.port
43
+ http.use_ssl = url.scheme == 'https'
44
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
45
+ http.open_timeout = options.fetch(:open_timeout, 30)
46
+ http.read_timeout = options.fetch(:read_timeout, 30)
47
+
48
+ response = http.post url.path, notification, headers
49
+
50
+ GCMResponse.new response.code.to_i, response.body
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+ # Base class for all GCM related errors
5
+ #
6
+ # @author Carlos Alonso
7
+ class GCMError < StandardError ; end
8
+
9
+ class MalformedGCMJSONError < GCMError ; end
10
+
11
+ class GCMAuthError < GCMError ; end
12
+
13
+ class GCMInternalError < GCMError ; end
14
+
15
+ class UnexpectedGCMResponseError < GCMError ; end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+ # Encapsulates a GCM Notification.
5
+ # By default only Required fields are set.
6
+ # (https://developer.android.com/google/gcm/server-ref.html#send-downstream)
7
+ #
8
+ # @author Carlos Alonso
9
+ class GCMNotification
10
+ include RubyPushNotifications::NotificationResultsManager
11
+
12
+ # Initializes the notification
13
+ #
14
+ # @param [Array]. Array with the receiver's GCM registration ids.
15
+ # @param [Hash]. Payload to send.
16
+ def initialize(registration_ids, data)
17
+ @registration_ids = registration_ids
18
+ @data = data
19
+ end
20
+
21
+ # @return [String]. The GCM's JSON format for the payload to send.
22
+ # (https://developer.android.com/google/gcm/server-ref.html#send-downstream)
23
+ def as_gcm_json
24
+ JSON.dump(
25
+ registration_ids: @registration_ids,
26
+ data: @data
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+
5
+ # This class is responsible for sending notifications to the GCM service.
6
+ #
7
+ # @author Carlos Alonso
8
+ class GCMPusher
9
+
10
+ # Initializes the GCMPusher
11
+ #
12
+ # @param key [String]. GCM sender id to use
13
+ # ((https://developer.android.com/google/gcm/gcm.html#senderid))
14
+ # @param options [Hash] optional. Options for GCMPusher. Currently supports:
15
+ # * url [String]: URL of the GCM endpoint. Defaults to the official GCM URL.
16
+ # * open_timeout [Integer]: Number of seconds to wait for the connection to open. Defaults to 30.
17
+ # * read_timeout [Integer]: Number of seconds to wait for one block to be read. Defaults to 30.
18
+ def initialize(key, options = {})
19
+ @key = key
20
+ @options = options
21
+ end
22
+
23
+ # Actually pushes the given notifications.
24
+ # Assigns every notification an array with the result of each
25
+ # individual notification.
26
+ #
27
+ # @param notifications [Array]. Array of GCMNotification to send.
28
+ def push(notifications)
29
+ notifications.each do |notif|
30
+ notif.results = GCMConnection.post notif.as_gcm_json, @key, @options
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,89 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+
5
+ # This class encapsulates a response received from the GCM service
6
+ # and helps parsing and understanding the received meesages/codes.
7
+ #
8
+ # @author Carlos Alonso
9
+ class GCMResponse
10
+
11
+ # @return [Integer] the number of successfully sent notifications
12
+ attr_reader :success
13
+
14
+ # @return [Integer] the number of failed notifications
15
+ attr_reader :failed
16
+
17
+ # @return [Integer] the number of received canonical IDS
18
+ # (https://developer.android.com/google/gcm/server-ref.html#table4)
19
+ attr_reader :canonical_ids
20
+
21
+ # @return [Array] Array of a GCMResult for every receiver of the notification
22
+ # sent indicating the result of the operation.
23
+ attr_reader :results
24
+ alias_method :individual_results, :results
25
+
26
+ # Initializes the GCMResponse and runs response parsing
27
+ #
28
+ # @param code [Integer]. The HTTP status code received
29
+ # @param body [String]. The response body received.
30
+ # @raise MalformedGCMJsonError if code == 400 Bad Request
31
+ # @raise GCMAuthError if code == 401 Unauthorized
32
+ # @raise GCMInternalError if code == 5xx
33
+ # @raise UnexpectedGCMResponseError if code != 200
34
+ def initialize(code, body)
35
+ case code
36
+ when 200
37
+ parse_response body
38
+ when 400
39
+ raise MalformedGCMJSONError, body
40
+ when 401
41
+ raise GCMAuthError, body
42
+ when 500..599
43
+ raise GCMInternalError, body
44
+ else
45
+ raise UnexpectedGCMResponseError, code
46
+ end
47
+ end
48
+
49
+ def ==(other)
50
+ (other.is_a?(GCMResponse) &&
51
+ success == other.success &&
52
+ failed == other.failed &&
53
+ canonical_ids == other.canonical_ids &&
54
+ results == other.results) || super(other)
55
+ end
56
+
57
+ private
58
+
59
+ # Parses the response extracting counts for successful, failed and
60
+ # containing canonical ID messages.
61
+ # Also creates the results array assigning a GCMResult subclass for each
62
+ # registration ID the notification was sent to.
63
+ #
64
+ # @param body [String]. The response body
65
+ def parse_response(body)
66
+ json = JSON.parse body, symbolize_names: true
67
+ @success = json[:success]
68
+ @failed = json[:failure]
69
+ @canonical_ids = json[:canonical_ids]
70
+ @results = (json[:results] || []).map { |result| gcm_result_for result }
71
+ end
72
+
73
+ # Factory method that, for each GCM result object assigns a GCMResult
74
+ # subclass.
75
+ #
76
+ # @param result [Hash]. GCM Result parsed hash
77
+ # @return [GCMResult]. Corresponding GCMResult subclass
78
+ def gcm_result_for(result)
79
+ if canonical_id = result[:registration_id]
80
+ GCMCanonicalIDResult.new canonical_id
81
+ elsif error = result[:error]
82
+ GCMResultError.new error
83
+ else
84
+ GCMResultOK.new
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,52 @@
1
+
2
+ module RubyPushNotifications
3
+ module GCM
4
+ # Class that encapsulates the result of a single sent notification to a single
5
+ # Registration ID
6
+ # (https://developer.android.com/google/gcm/server-ref.html#table4)
7
+ # @author Carlos Alonso
8
+ class GCMResult ; end
9
+
10
+ # Indicates that the notification was successfully sent to the corresponding
11
+ # registration ID
12
+ class GCMResultOK < GCMResult
13
+
14
+ def ==(other)
15
+ other.is_a?(GCMResultOK) || super(other)
16
+ end
17
+ end
18
+
19
+ # Indicates that the notification was successfully sent to the corresponding
20
+ # registration ID but GCM sent a canonical ID for it, so the received canonical
21
+ # ID should be used as registration ID ASAP.
22
+ class GCMCanonicalIDResult < GCMResult
23
+
24
+ # @return [String]. The suggested canonical ID from GCM
25
+ attr_reader :canonical_id
26
+
27
+ def initialize(canonical_id)
28
+ @canonical_id = canonical_id
29
+ end
30
+
31
+ def ==(other)
32
+ (other.is_a?(GCMCanonicalIDResult) && @canonical_id == other.canonical_id) ||
33
+ super(other)
34
+ end
35
+ end
36
+
37
+ # An error occurred sending the notification to the registration ID.
38
+ class GCMResultError < GCMResult
39
+
40
+ # @return [String]. The error sent by GCM
41
+ attr_accessor :error
42
+
43
+ def initialize(error)
44
+ @error = error
45
+ end
46
+
47
+ def ==(other)
48
+ (other.is_a?(GCMResultError) && @error == other.error) || super(other)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ require 'ruby-push-notifications/mpns/mpns_connection'
2
+ require 'ruby-push-notifications/mpns/mpns_notification'
3
+ require 'ruby-push-notifications/mpns/mpns_pusher'
4
+ require 'ruby-push-notifications/mpns/mpns_response'
5
+ require 'ruby-push-notifications/mpns/mpns_result'
@@ -0,0 +1,87 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ module RubyPushNotifications
5
+ module MPNS
6
+ # Encapsulates a connection to the MPNS service
7
+ # Responsible for final connection with the service.
8
+ #
9
+ class MPNSConnection
10
+
11
+ # @private Content-Type HTTP Header string
12
+ CONTENT_TYPE_HEADER = 'Content-Type'
13
+
14
+ # @private text/xml content type
15
+ X_NOTIFICATION_CLASS = 'X-NotificationClass'
16
+
17
+ # @private Windows Phone Target
18
+ X_WINDOWSPHONE_TARGET = 'X-WindowsPhone-Target'
19
+
20
+ # @private text/xml content type
21
+ XML_CONTENT_TYPE = 'text/xml'
22
+
23
+ # @private Enumators for notification types
24
+ BASEBATCH = { tile: 1, toast: 2, raw: 3 }
25
+
26
+ # @private Enumators for delay
27
+ BATCHADDS = { delay450: 10, delay900: 20 }
28
+
29
+ # @private Windows Phone Target Types
30
+ WP_TARGETS = { toast: 'toast', tile: 'token' }
31
+
32
+ # Issues a POST request to the MPNS send endpoint to
33
+ # submit the given notifications.
34
+ #
35
+ # @param n [MPNSNotification]. The notification object to POST
36
+ # @param cert [String] optional. Contents of the PEM encoded certificate.
37
+ # @param options [Hash] optional. Options for GCMPusher. Currently supports:
38
+ # * open_timeout [Integer]: Number of seconds to wait for the connection to open. Defaults to 30.
39
+ # * read_timeout [Integer]: Number of seconds to wait for one block to be read. Defaults to 30.
40
+ # @return [Array]. The response of post
41
+ # (http://msdn.microsoft.com/pt-br/library/windows/apps/ff941099)
42
+ def self.post(n, cert = nil, options = {})
43
+ headers = build_headers(n.data[:type], n.data[:delay])
44
+ body = n.as_mpns_xml
45
+ responses = []
46
+ n.each_device do |url|
47
+ http = Net::HTTP.new url.host, url.port
48
+ http.open_timeout = options.fetch(:open_timeout, 30)
49
+ http.read_timeout = options.fetch(:read_timeout, 30)
50
+ if cert && url.scheme == 'https'
51
+ http.use_ssl = true
52
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
53
+ http.ca_file = cert
54
+ end
55
+ response = http.post url.path, body, headers
56
+ responses << { device_url: url.to_s, headers: extract_headers(response), code: response.code.to_i }
57
+ end
58
+ MPNSResponse.new responses
59
+ end
60
+
61
+ # Build Header based on type and delay
62
+ #
63
+ # @param type [Symbol]. The type of notification
64
+ # @param delay [Symbol]. The delay to be used
65
+ # @return [Hash]. Correct delay based on notification type
66
+ def self.build_headers(type, delay)
67
+ headers = {
68
+ CONTENT_TYPE_HEADER => XML_CONTENT_TYPE,
69
+ X_NOTIFICATION_CLASS => "#{(BASEBATCH[type] + (BATCHADDS[delay] || 0))}"
70
+ }
71
+ headers[X_WINDOWSPHONE_TARGET] = WP_TARGETS[type] unless type == :raw
72
+ headers
73
+ end
74
+
75
+ # Extract headers from response
76
+ # @param response [Net::HTTPResponse]. HTTP response for request
77
+ #
78
+ # @return [Hash]. Hash with headers with case-insensitive keys and string values
79
+ def self.extract_headers(response)
80
+ headers = {}
81
+ response.each_header { |k, v| headers[k] = v }
82
+ headers
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,94 @@
1
+ require 'builder'
2
+
3
+ module RubyPushNotifications
4
+ module MPNS
5
+ # Encapsulates a MPNS Notification.
6
+ # Actually support for raw, toast, tiles notifications
7
+ # (http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202945)
8
+ #
9
+ class MPNSNotification
10
+ include RubyPushNotifications::NotificationResultsManager
11
+
12
+ # @return [Hash]. Payload to send.
13
+ # Toast :title => a bold message
14
+ # :message => the small message
15
+ # :param => a string parameter that is passed to the app
16
+ # Tile :image => a new image for the tile
17
+ # :count => a number to show on the tile
18
+ # :title => the new title of the tile
19
+ # :back_image => an image for the back of the tile
20
+ # :back_title => a title on the back of the tile
21
+ # :back_content => some content (text) for the back
22
+ # Raw :message => the full Hash message body
23
+ attr_reader :data
24
+
25
+ # @return [Array]. Array with the receiver's MPNS device URLs.
26
+ attr_reader :device_urls
27
+
28
+ # Initializes the notification
29
+ #
30
+ # @param [Array]. Array with the receiver's device urls.
31
+ # @param [Hash]. Payload to send.
32
+ # Toast :title => a bold message
33
+ # :message => the small message
34
+ # :param => a string parameter that is passed to the app
35
+ # Tile :image => a new image for the tile
36
+ # :count => a number to show on the tile
37
+ # :title => the new title of the tile
38
+ # :back_image => an image for the back of the tile
39
+ # :back_title => a title on the back of the tile
40
+ # :back_content => some content (text) for the back
41
+ # Raw :message => the full XML message body
42
+ def initialize(device_urls, data)
43
+ @device_urls = device_urls
44
+ @data = data
45
+ @data[:type] ||= :raw
46
+ end
47
+
48
+
49
+ # @return [String]. The GCM's XML format for the payload to send.
50
+ # (http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202945)
51
+ def as_mpns_xml
52
+ xml = Builder::XmlMarkup.new
53
+ xml.instruct!
54
+ if data[:type] != :raw
55
+ xml.tag!('wp:Notification', 'xmlns:wp' => 'WPNotification') do
56
+ case data[:type]
57
+ when :toast
58
+ xml.tag!('wp:Toast') do
59
+ xml.tag!('wp:Text1') { xml.text!(data[:title]) }
60
+ xml.tag!('wp:Text2') { xml.text!(data[:message]) }
61
+ xml.tag!('wp:Param') { xml.text!(data[:param]) } if data[:param]
62
+ end
63
+ when :tile
64
+ xml.tag!('wp:Tile') do
65
+ xml.tag!('wp:BackgroundImage') { xml.text!(data[:image]) } if data[:image]
66
+ xml.tag!('wp:Count') { xml.text!(data[:count].to_s) } if data[:count]
67
+ xml.tag!('wp:Title') { xml.text!(data[:title]) } if data[:title]
68
+ xml.tag!('wp:BackBackgroundImage') { xml.text!(data[:back_image]) } if data[:back_image]
69
+ xml.tag!('wp:BackTitle') { xml.text!(data[:back_title]) } if data[:back_title]
70
+ xml.tag!('wp:BackContent') { xml.text!(data[:back_content]) } if data[:back_content]
71
+ end
72
+ end
73
+ end
74
+ else
75
+ xml.root { build_hash(xml, data[:message]) }
76
+ end
77
+ xml.target!
78
+ end
79
+
80
+ def each_device
81
+ @device_urls.each do |url|
82
+ yield(URI.parse url)
83
+ end
84
+ end
85
+
86
+ def build_hash(xml, options)
87
+ options.each do |k, v|
88
+ xml.tag!(k.to_s) { v.is_a?(Hash) ? build_hash(xml, v) : xml.text!(v.to_s) }
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+ end