ruby-push-notifications 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.hound.yml +2 -0
- data/.ruby-style.yml +1063 -0
- data/.travis.yml +7 -0
- data/Gemfile.lock +2 -0
- data/README.md +6 -2
- data/examples/mpns.rb +17 -0
- data/lib/ruby-push-notifications/mpns/mpns_connection.rb +82 -0
- data/lib/ruby-push-notifications/mpns/mpns_notification.rb +96 -0
- data/lib/ruby-push-notifications/mpns/mpns_pusher.rb +29 -0
- data/lib/ruby-push-notifications/mpns/mpns_response.rb +81 -0
- data/lib/ruby-push-notifications/mpns/mpns_result.rb +182 -0
- data/lib/ruby-push-notifications/mpns.rb +5 -0
- data/lib/ruby-push-notifications/version.rb +1 -1
- data/lib/ruby-push-notifications.rb +1 -0
- data/ruby-push-notifications.gemspec +2 -0
- data/spec/factories/notifications.rb +8 -0
- data/spec/factories.rb +5 -0
- data/spec/ruby-push-notifications/mpns/mpns_connection_spec.rb +46 -0
- data/spec/ruby-push-notifications/mpns/mpns_notification_spec.rb +43 -0
- data/spec/ruby-push-notifications/mpns/mpns_pusher_spec.rb +59 -0
- data/spec/ruby-push-notifications/mpns/mpns_response_spec.rb +64 -0
- metadata +33 -2
data/.travis.yml
CHANGED
@@ -5,3 +5,10 @@ rvm:
|
|
5
5
|
- "2.0.0"
|
6
6
|
- "1.9.3"
|
7
7
|
env: CODECLIMATE_REPO_TOKEN=efdb12c380287a25b2b26362aa710a9b59020122cbe4edecf0d353bf50e0046a
|
8
|
+
notifications:
|
9
|
+
webhooks:
|
10
|
+
urls:
|
11
|
+
- https://webhooks.gitter.im/e/b740ebfeb7e0dba10293
|
12
|
+
on_success: change # options: [always|never|change] default: always
|
13
|
+
on_failure: always # options: [always|never|change] default: always
|
14
|
+
on_start: false # default: false
|
data/Gemfile.lock
CHANGED
@@ -2,6 +2,7 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
ruby-push-notifications (0.1.0)
|
5
|
+
builder (~> 3.2)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
@@ -13,6 +14,7 @@ GEM
|
|
13
14
|
thread_safe (~> 0.3, >= 0.3.4)
|
14
15
|
tzinfo (~> 1.1)
|
15
16
|
addressable (2.3.8)
|
17
|
+
builder (3.2.2)
|
16
18
|
codeclimate-test-reporter (0.4.7)
|
17
19
|
simplecov (>= 0.7.1, < 1.0.0)
|
18
20
|
crack (0.4.2)
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Ruby Push Notifications
|
2
2
|
|
3
|
-
[![Build Status](https://travis-ci.org/calonso/ruby-push-notifications.svg)](https://travis-ci.org/calonso/ruby-push-notifications) [![Dependency Status](https://gemnasium.com/calonso/ruby-push-notifications.svg)](https://gemnasium.com/calonso/ruby-push-notifications) [![Code Climate](https://codeclimate.com/github/calonso/ruby-push-notifications/badges/gpa.svg)](https://codeclimate.com/github/calonso/ruby-push-notifications) [![Test Coverage](https://codeclimate.com/github/calonso/ruby-push-notifications/badges/coverage.svg)](https://codeclimate.com/github/calonso/ruby-push-notifications) [![Gem Version](https://badge.fury.io/rb/ruby-push-notifications.svg)](http://badge.fury.io/rb/ruby-push-notifications)
|
3
|
+
[![Build Status](https://travis-ci.org/calonso/ruby-push-notifications.svg)](https://travis-ci.org/calonso/ruby-push-notifications) [![Dependency Status](https://gemnasium.com/calonso/ruby-push-notifications.svg)](https://gemnasium.com/calonso/ruby-push-notifications) [![Code Climate](https://codeclimate.com/github/calonso/ruby-push-notifications/badges/gpa.svg)](https://codeclimate.com/github/calonso/ruby-push-notifications) [![Test Coverage](https://codeclimate.com/github/calonso/ruby-push-notifications/badges/coverage.svg)](https://codeclimate.com/github/calonso/ruby-push-notifications) [![Gem Version](https://badge.fury.io/rb/ruby-push-notifications.svg)](http://badge.fury.io/rb/ruby-push-notifications) [![Join the chat at https://gitter.im/calonso/ruby-push-notifications](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/calonso/ruby-push-notifications?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
4
|
|
5
|
-
###iOS and
|
5
|
+
###iOS, Android and Windows Phone Push Notifications made easy!
|
6
6
|
|
7
7
|
## Features
|
8
8
|
|
@@ -38,6 +38,7 @@ For completely detailed examples:
|
|
38
38
|
|
39
39
|
1. [Apple iOS example](https://github.com/calonso/ruby-push-notifications/tree/master/examples/apns.rb)
|
40
40
|
2. [Google Android example](https://github.com/calonso/ruby-push-notifications/tree/master/examples/gcm.rb)
|
41
|
+
3. [Windows Phone example](https://github.com/calonso/ruby-push-notifications/tree/master/examples/mpns.rb)
|
41
42
|
|
42
43
|
## Pending tasks
|
43
44
|
|
@@ -49,6 +50,9 @@ Feel free to contribute!!
|
|
49
50
|
* Validate GCM notifications format and max size
|
50
51
|
* Split GCM notifications in parts if more than 1000 destinations are given (currently raising exception)
|
51
52
|
* Integrate with APNS Feedback service
|
53
|
+
* Validate MPNS notifications format
|
54
|
+
* Validate MPNS data
|
55
|
+
* Add other platforms (Amazon Fire...)
|
52
56
|
|
53
57
|
## Contributing
|
54
58
|
|
data/examples/mpns.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
4
|
+
|
5
|
+
require 'ruby-push-notifications'
|
6
|
+
|
7
|
+
device_urls = [
|
8
|
+
'First device url here',
|
9
|
+
'Second device url here'
|
10
|
+
]
|
11
|
+
|
12
|
+
# Notification with toast type
|
13
|
+
notification = RubyPushNotifications::MPNS::MPNSNotification.new device_urls, { title: 'Title', message: 'Hello MPNS World!', type: :toast }
|
14
|
+
|
15
|
+
pusher = RubyPushNotifications::MPNS::MPNSPusher.new
|
16
|
+
pusher.push [notification]
|
17
|
+
p notification.results
|
@@ -0,0 +1,82 @@
|
|
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 optional cert [String]. Contents of the PEM encoded certificate
|
37
|
+
# @return [Array]. The response of post
|
38
|
+
# (http://msdn.microsoft.com/pt-br/library/windows/apps/ff941099)
|
39
|
+
def self.post(n, cert = nil)
|
40
|
+
headers = build_headers(n.data[:type], n.data[:delay])
|
41
|
+
body = n.as_mpns_xml
|
42
|
+
responses = []
|
43
|
+
n.each_device do |url|
|
44
|
+
http = Net::HTTP.new url.host, url.port
|
45
|
+
if cert && url.scheme == 'https'
|
46
|
+
http.use_ssl = true
|
47
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
48
|
+
http.ca_file = cert
|
49
|
+
end
|
50
|
+
response = http.post url.path, body, headers
|
51
|
+
responses << { device_url: url.to_s, headers: extract_headers(response), code: response.code.to_i }
|
52
|
+
end
|
53
|
+
MPNSResponse.new responses
|
54
|
+
end
|
55
|
+
|
56
|
+
# Build Header based on type and delay
|
57
|
+
#
|
58
|
+
# @param type [Symbol]. The type of notification
|
59
|
+
# @param delay [Symbol]. The delay to be used
|
60
|
+
# @return [Hash]. Correct delay based on notification type
|
61
|
+
def self.build_headers(type, delay)
|
62
|
+
headers = {
|
63
|
+
CONTENT_TYPE_HEADER => XML_CONTENT_TYPE,
|
64
|
+
X_NOTIFICATION_CLASS => "#{(BASEBATCH[type] + (BATCHADDS[delay] || 0))}"
|
65
|
+
}
|
66
|
+
headers[X_WINDOWSPHONE_TARGET] = WP_TARGETS[type] unless type == :raw
|
67
|
+
headers
|
68
|
+
end
|
69
|
+
|
70
|
+
# Extract headers from response
|
71
|
+
# @param response [Net::HTTPResponse]. HTTP response for request
|
72
|
+
#
|
73
|
+
# @return [Hash]. Hash with headers with case-insensitive keys and string values
|
74
|
+
def self.extract_headers(response)
|
75
|
+
headers = {}
|
76
|
+
response.each_header { |k, v| headers[k] = v }
|
77
|
+
headers
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,96 @@
|
|
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
|
+
|
11
|
+
# @return [MPNSResponse]. MPNSResponse with the results from sending this notification.
|
12
|
+
attr_accessor :results
|
13
|
+
|
14
|
+
# @return [Hash]. Payload to send.
|
15
|
+
# Toast :title => a bold message
|
16
|
+
# :message => the small message
|
17
|
+
# :param => a string parameter that is passed to the app
|
18
|
+
# Tile :image => a new image for the tile
|
19
|
+
# :count => a number to show on the tile
|
20
|
+
# :title => the new title of the tile
|
21
|
+
# :back_image => an image for the back of the tile
|
22
|
+
# :back_title => a title on the back of the tile
|
23
|
+
# :back_content => some content (text) for the back
|
24
|
+
# Raw :message => the full Hash message body
|
25
|
+
attr_reader :data
|
26
|
+
|
27
|
+
# @return [Array]. Array with the receiver's MPNS device URLs.
|
28
|
+
attr_reader :device_urls
|
29
|
+
|
30
|
+
# Initializes the notification
|
31
|
+
#
|
32
|
+
# @param [Array]. Array with the receiver's device urls.
|
33
|
+
# @param [Hash]. Payload to send.
|
34
|
+
# Toast :title => a bold message
|
35
|
+
# :message => the small message
|
36
|
+
# :param => a string parameter that is passed to the app
|
37
|
+
# Tile :image => a new image for the tile
|
38
|
+
# :count => a number to show on the tile
|
39
|
+
# :title => the new title of the tile
|
40
|
+
# :back_image => an image for the back of the tile
|
41
|
+
# :back_title => a title on the back of the tile
|
42
|
+
# :back_content => some content (text) for the back
|
43
|
+
# Raw :message => the full XML message body
|
44
|
+
def initialize(device_urls, data)
|
45
|
+
@device_urls = device_urls
|
46
|
+
@data = data
|
47
|
+
@data[:type] ||= :raw
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# @return [String]. The GCM's XML format for the payload to send.
|
52
|
+
# (http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202945)
|
53
|
+
def as_mpns_xml
|
54
|
+
xml = Builder::XmlMarkup.new
|
55
|
+
xml.instruct!
|
56
|
+
if data[:type] != :raw
|
57
|
+
xml.tag!('wp:Notification', 'xmlns:wp' => 'WPNotification') do
|
58
|
+
case data[:type]
|
59
|
+
when :toast
|
60
|
+
xml.tag!('wp:Toast') do
|
61
|
+
xml.tag!('wp:Text1') { xml.text!(data[:title]) }
|
62
|
+
xml.tag!('wp:Text2') { xml.text!(data[:message]) }
|
63
|
+
xml.tag!('wp:Param') { xml.text!(data[:param]) } if data[:param]
|
64
|
+
end
|
65
|
+
when :tile
|
66
|
+
xml.tag!('wp:Tile') do
|
67
|
+
xml.tag!('wp:BackgroundImage') { xml.text!(data[:image]) } if data[:image]
|
68
|
+
xml.tag!('wp:Count') { xml.text!(data[:count].to_s) } if data[:count]
|
69
|
+
xml.tag!('wp:Title') { xml.text!(data[:title]) } if data[:title]
|
70
|
+
xml.tag!('wp:BackBackgroundImage') { xml.text!(data[:back_image]) } if data[:back_image]
|
71
|
+
xml.tag!('wp:BackTitle') { xml.text!(data[:back_title]) } if data[:back_title]
|
72
|
+
xml.tag!('wp:BackContent') { xml.text!(data[:back_content]) } if data[:back_content]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
else
|
77
|
+
xml.root { build_hash(xml, data[:message]) }
|
78
|
+
end
|
79
|
+
xml.target!
|
80
|
+
end
|
81
|
+
|
82
|
+
def each_device
|
83
|
+
@device_urls.each do |url|
|
84
|
+
yield(URI.parse url)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_hash(xml, options)
|
89
|
+
options.each do |k, v|
|
90
|
+
xml.tag!(k.to_s) { v.is_a?(Hash) ? build_hash(xml, v) : xml.text!(v.to_s) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
module RubyPushNotifications
|
3
|
+
module MPNS
|
4
|
+
|
5
|
+
# This class is responsible for sending notifications to the MPNS service.
|
6
|
+
#
|
7
|
+
class MPNSPusher
|
8
|
+
|
9
|
+
# Initializes the MPNSPusher
|
10
|
+
#
|
11
|
+
# @param certificate [String]. The PEM encoded MPNS certificate.
|
12
|
+
# (http://msdn.microsoft.com/pt-br/library/windows/apps/ff941099)
|
13
|
+
def initialize(certificate = nil)
|
14
|
+
@certificate = certificate
|
15
|
+
end
|
16
|
+
|
17
|
+
# Actually pushes the given notifications.
|
18
|
+
# Assigns every notification an array with the result of each
|
19
|
+
# individual notification.
|
20
|
+
#
|
21
|
+
# @param notifications [Array]. Array of MPNSNotification to send.
|
22
|
+
def push(notifications)
|
23
|
+
notifications.each do |notif|
|
24
|
+
notif.results = MPNSConnection.post notif, @certificate
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
module RubyPushNotifications
|
3
|
+
module MPNS
|
4
|
+
|
5
|
+
# This class encapsulates a response received from the MPNS service
|
6
|
+
# and helps parsing and understanding the received messages/codes.
|
7
|
+
#
|
8
|
+
class MPNSResponse
|
9
|
+
|
10
|
+
# @return [Integer] the number of successfully sent notifications
|
11
|
+
attr_reader :success
|
12
|
+
|
13
|
+
# @return [Integer] the number of failed notifications
|
14
|
+
attr_reader :failed
|
15
|
+
|
16
|
+
# @return [Array] Array of a MPNSResult for every receiver of the notification
|
17
|
+
# sent indicating the result of the operation.
|
18
|
+
attr_reader :results
|
19
|
+
|
20
|
+
# Initializes the MPNSResponse and runs response parsing
|
21
|
+
#
|
22
|
+
# @param responses [Array]. Array with device_urls and http responses
|
23
|
+
def initialize(responses)
|
24
|
+
parse_response responses
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
(other.is_a?(MPNSResponse) &&
|
29
|
+
success == other.success &&
|
30
|
+
failed == other.failed &&
|
31
|
+
results == other.results) || super(other)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Parses the response extracting counts for successful, failed messages.
|
37
|
+
# Also creates the results array assigning a MPNSResult subclass for each
|
38
|
+
# device URL the notification was sent to.
|
39
|
+
#
|
40
|
+
# @param responses [Array]. Array of hash responses
|
41
|
+
def parse_response(responses)
|
42
|
+
@success = responses.count { |response| response[:code] == 200 }
|
43
|
+
@failed = responses.count { |response| response[:code] != 200 }
|
44
|
+
@results = responses.map do |response|
|
45
|
+
mpns_result_for response[:code],
|
46
|
+
response[:device_url],
|
47
|
+
response[:headers]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Factory method that, for each MPNS result object assigns a MPNSResult
|
52
|
+
# subclass.
|
53
|
+
#
|
54
|
+
# @param code [Integer]. The HTTP status code received
|
55
|
+
# @param device_url [String]. The receiver's MPNS device url.
|
56
|
+
# @param headers [Hash]. The HTTP headers received.
|
57
|
+
# @return [MPNSResult]. Corresponding MPNSResult subclass
|
58
|
+
def mpns_result_for(code, device_url, headers)
|
59
|
+
case code
|
60
|
+
when 200
|
61
|
+
MPNSResultOK.new device_url, headers
|
62
|
+
when 400
|
63
|
+
MalformedMPNSResultError.new device_url
|
64
|
+
when 401
|
65
|
+
MPNSAuthError.new device_url
|
66
|
+
when 404
|
67
|
+
MPNSInvalidError.new device_url, headers
|
68
|
+
when 406
|
69
|
+
MPNSLimitError.new device_url, headers
|
70
|
+
when 412
|
71
|
+
MPNSPreConditionError.new device_url, headers
|
72
|
+
when 500..599
|
73
|
+
MPNSInternalError.new device_url
|
74
|
+
else
|
75
|
+
MPNSResultError.new device_url
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
|
2
|
+
module RubyPushNotifications
|
3
|
+
module MPNS
|
4
|
+
# Class that encapsulates the result of a single sent notification to a single
|
5
|
+
# Device URL
|
6
|
+
# (https://msdn.microsoft.com/en-us/library/windows/apps/ff941100%28v=vs.105%29.aspx)
|
7
|
+
class MPNSResult
|
8
|
+
# @return [String]. Receiver MPNS device URL.
|
9
|
+
attr_accessor :device_url
|
10
|
+
|
11
|
+
# @private X-NotificationStatus HTTP Header string
|
12
|
+
X_NOTIFICATION_STATUS = 'x-notificationstatus'
|
13
|
+
|
14
|
+
# @private X-DeviceConnectionStatus HTTP Header string
|
15
|
+
X_DEVICE_CONNECTION_STATUS = 'x-deviceconnectionstatus'
|
16
|
+
|
17
|
+
# @private X-SubscriptionStatus HTTP Header string
|
18
|
+
X_SUBSCRIPTION_STATUS = 'x-subscriptionstatus'
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
# Indicates that the notification was successfully sent to the corresponding
|
23
|
+
# device URL
|
24
|
+
class MPNSResultOK < MPNSResult
|
25
|
+
# @return [String]. The status of the notification received
|
26
|
+
# by the Microsoft Push Notification Service.
|
27
|
+
attr_accessor :notification_status
|
28
|
+
|
29
|
+
# @return [String]. The connection status of the device.
|
30
|
+
attr_accessor :device_connection_status
|
31
|
+
|
32
|
+
# @return [String]. The subscription status.
|
33
|
+
attr_accessor :subscription_status
|
34
|
+
|
35
|
+
def initialize(device_url, headers = nil)
|
36
|
+
@device_url = device_url
|
37
|
+
@notification_status = headers[X_NOTIFICATION_STATUS]
|
38
|
+
@device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
|
39
|
+
@subscription_status = headers[X_SUBSCRIPTION_STATUS]
|
40
|
+
end
|
41
|
+
|
42
|
+
def ==(other)
|
43
|
+
(other.is_a?(MPNSResultOK) &&
|
44
|
+
device_url == other.device_url &&
|
45
|
+
notification_status == other.notification_status &&
|
46
|
+
device_connection_status == other.device_connection_status &&
|
47
|
+
subscription_status == other.subscription_status) || super(other)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# This error occurs when the cloud service sends a notification
|
52
|
+
# request with a bad XML document or malformed notification URI.
|
53
|
+
class MalformedMPNSResultError < MPNSResult
|
54
|
+
def initialize(device_url)
|
55
|
+
@device_url = device_url
|
56
|
+
end
|
57
|
+
|
58
|
+
def ==(other)
|
59
|
+
(other.is_a?(MalformedMPNSResultError) &&
|
60
|
+
device_url == other.device_url) || super(other)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sending this notification is unauthorized.
|
65
|
+
class MPNSAuthError < MPNSResult
|
66
|
+
def initialize(device_url)
|
67
|
+
@device_url = device_url
|
68
|
+
end
|
69
|
+
|
70
|
+
def ==(other)
|
71
|
+
(other.is_a?(MPNSAuthError) &&
|
72
|
+
device_url == other.device_url) || super(other)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# The subscription is invalid and is not present on the Push Notification Service.
|
77
|
+
class MPNSInvalidError < MPNSResult
|
78
|
+
# @return [String]. The status of the notification received
|
79
|
+
# by the Microsoft Push Notification Service.
|
80
|
+
attr_accessor :notification_status
|
81
|
+
|
82
|
+
# @return [String]. The connection status of the device.
|
83
|
+
attr_accessor :device_connection_status
|
84
|
+
|
85
|
+
# @return [String]. The subscription status.
|
86
|
+
attr_accessor :subscription_status
|
87
|
+
|
88
|
+
def initialize(device_url, headers)
|
89
|
+
@device_url = device_url
|
90
|
+
@notification_status = headers[X_NOTIFICATION_STATUS]
|
91
|
+
@device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
|
92
|
+
@subscription_status = headers[X_SUBSCRIPTION_STATUS]
|
93
|
+
end
|
94
|
+
|
95
|
+
def ==(other)
|
96
|
+
(other.is_a?(MPNSInvalidError) &&
|
97
|
+
device_url == other.device_url &&
|
98
|
+
notification_status == other.notification_status &&
|
99
|
+
device_connection_status == other.device_connection_status &&
|
100
|
+
subscription_status == other.subscription_status) || super(other)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# This error occurs when an unauthenticated cloud service has reached
|
105
|
+
# the per-day throttling limit for a subscription,
|
106
|
+
# or when a cloud service (authenticated or unauthenticated)
|
107
|
+
# has sent too many notifications per second.
|
108
|
+
class MPNSLimitError < MPNSResult
|
109
|
+
# @return [String]. The status of the notification received
|
110
|
+
# by the Microsoft Push Notification Service.
|
111
|
+
attr_accessor :notification_status
|
112
|
+
|
113
|
+
# @return [String]. The connection status of the device.
|
114
|
+
attr_accessor :device_connection_status
|
115
|
+
|
116
|
+
# @return [String]. The subscription status.
|
117
|
+
attr_accessor :subscription_status
|
118
|
+
|
119
|
+
def initialize(device_url, headers)
|
120
|
+
@device_url = device_url
|
121
|
+
@notification_status = headers[X_NOTIFICATION_STATUS]
|
122
|
+
@device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
|
123
|
+
@subscription_status = headers[X_SUBSCRIPTION_STATUS]
|
124
|
+
end
|
125
|
+
|
126
|
+
def ==(other)
|
127
|
+
(other.is_a?(MPNSLimitError) &&
|
128
|
+
device_url == other.device_url &&
|
129
|
+
notification_status == other.notification_status &&
|
130
|
+
device_connection_status == other.device_connection_status &&
|
131
|
+
subscription_status == other.subscription_status) || super(other)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# The device is in a disconnected state.
|
136
|
+
class MPNSPreConditionError < MPNSResult
|
137
|
+
# @return [String]. The status of the notification received
|
138
|
+
# by the Microsoft Push Notification Service.
|
139
|
+
attr_accessor :notification_status
|
140
|
+
|
141
|
+
# @return [String]. The connection status of the device.
|
142
|
+
attr_accessor :device_connection_status
|
143
|
+
|
144
|
+
def initialize(device_url, headers)
|
145
|
+
@device_url = device_url
|
146
|
+
@notification_status = headers[X_NOTIFICATION_STATUS]
|
147
|
+
@device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
|
148
|
+
end
|
149
|
+
|
150
|
+
def ==(other)
|
151
|
+
(other.is_a?(MPNSPreConditionError) &&
|
152
|
+
device_url == other.device_url &&
|
153
|
+
notification_status == other.notification_status &&
|
154
|
+
device_connection_status == other.device_connection_status) || super(other)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# The Push Notification Service is unable to process the request.
|
159
|
+
class MPNSInternalError < MPNSResult
|
160
|
+
def initialize(device_url)
|
161
|
+
@device_url = device_url
|
162
|
+
end
|
163
|
+
|
164
|
+
def ==(other)
|
165
|
+
(other.is_a?(MPNSInternalError) &&
|
166
|
+
device_url == other.device_url) || super(other)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Unknow Error
|
171
|
+
class MPNSResultError < MPNSResult
|
172
|
+
def initialize(device_url)
|
173
|
+
@device_url = device_url
|
174
|
+
end
|
175
|
+
|
176
|
+
def ==(other)
|
177
|
+
(other.is_a?(MPNSResultError) &&
|
178
|
+
device_url == other.device_url) || super(other)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
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'
|
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
21
|
spec.require_paths = ['lib']
|
22
22
|
|
23
|
+
spec.add_dependency 'builder', '~> 3.2'
|
24
|
+
|
23
25
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
24
26
|
spec.add_development_dependency 'rake', '~> 10.4'
|
25
27
|
spec.add_development_dependency 'rspec', '~> 3.2'
|
@@ -12,4 +12,12 @@ FactoryGirl.define do
|
|
12
12
|
|
13
13
|
initialize_with { new registration_ids, data }
|
14
14
|
end
|
15
|
+
|
16
|
+
factory :mpns_notification, class: 'RubyPushNotifications::MPNS::MPNSNotification' do
|
17
|
+
device_urls { [generate(:mpns_device_url)] }
|
18
|
+
data message: { value1: 'hello' }
|
19
|
+
|
20
|
+
initialize_with { new device_urls, data }
|
21
|
+
end
|
22
|
+
|
15
23
|
end
|
data/spec/factories.rb
CHANGED
@@ -7,4 +7,9 @@ FactoryGirl.define do
|
|
7
7
|
sequence :gcm_registration_id do |i|
|
8
8
|
"APA91bHPRgkF3JUikC4ENAHEeMrd41Zxv3hVZjC9KtT8OvPVGJ-hQMRKRrZuJAEcl7B338qju59zJMjw2DELjzEvxwYv7hH5Ynpc1ODQ0aT4U4OFEeco8ohsN5PjL1iC2dNtk2BAokeMCg2ZXKqpc8FXKmhX94kIxQ#{i}"
|
9
9
|
end
|
10
|
+
|
11
|
+
sequence :mpns_device_url do |i|
|
12
|
+
"http://s.notify.live.net/u/1/bn1/HmQAAACP-0esPuxBSkzBNNXH4W0lV3lK-stEw6eRfpXX39uYbM7IwehXOTO9pRBjaaGECWOdD_7x5j5U4w4iXG4hGxer/d2luZG93c3Bob25lZGVmYXVsdA/EMDhx32Q5BG0DWnZpuVX1g/kRFAu0-jnhMQ-HG94rXzrbb0wQk#{i}"
|
13
|
+
end
|
14
|
+
|
10
15
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
module RubyPushNotifications
|
3
|
+
module MPNS
|
4
|
+
describe MPNSConnection do
|
5
|
+
|
6
|
+
describe '::post' do
|
7
|
+
|
8
|
+
let(:body) { 'abc' }
|
9
|
+
let(:headers) {
|
10
|
+
{
|
11
|
+
'x-notificationstatus' => 'Received',
|
12
|
+
'x-deviceconnectionstatus' => 'Connected',
|
13
|
+
'x-subscriptionstatus' => 'Active'
|
14
|
+
}
|
15
|
+
}
|
16
|
+
let(:device_urls) { [generate(:mpns_device_url)] }
|
17
|
+
let(:toast_data) { { title: 'Title', message: 'Hello MPNS World!', type: :toast } }
|
18
|
+
let(:toast_notification) { build :mpns_notification, device_urls: device_urls, data: toast_data }
|
19
|
+
|
20
|
+
before do
|
21
|
+
stub_request(:post, %r{s.notify.live.net}).
|
22
|
+
to_return status: [200, 'OK'], body: body, headers: headers
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'runs the right request' do
|
26
|
+
MPNSConnection.post toast_notification
|
27
|
+
|
28
|
+
expect(WebMock).
|
29
|
+
to have_requested(:post, toast_notification.device_urls[0]).
|
30
|
+
with(body: toast_notification.as_mpns_xml, headers: { 'Content-Type' => 'text/xml', 'X-NotificationClass' => '2', 'X-WindowsPhone-Target' => :toast}).
|
31
|
+
once
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns the response encapsulated in a Hash object' do
|
35
|
+
responses = [
|
36
|
+
{ device_url: toast_notification.device_urls[0],
|
37
|
+
headers: headers,
|
38
|
+
code: 200
|
39
|
+
}
|
40
|
+
]
|
41
|
+
expect(MPNSConnection.post toast_notification).to eq MPNSResponse.new(responses)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|