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,33 @@
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
+ # @param options [Hash] optional. Options for GCMPusher. Currently supports:
13
+ # * open_timeout [Integer]: Number of seconds to wait for the connection to open. Defaults to 30.
14
+ # * read_timeout [Integer]: Number of seconds to wait for one block to be read. Defaults to 30.
15
+ # (http://msdn.microsoft.com/pt-br/library/windows/apps/ff941099)
16
+ def initialize(certificate = nil, options = {})
17
+ @certificate = certificate
18
+ @options = options
19
+ end
20
+
21
+ # Actually pushes the given notifications.
22
+ # Assigns every notification an array with the result of each
23
+ # individual notification.
24
+ #
25
+ # @param notifications [Array]. Array of MPNSNotification to send.
26
+ def push(notifications)
27
+ notifications.each do |notif|
28
+ notif.results = MPNSConnection.post notif, @certificate, @options
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,82 @@
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
+ alias_method :individual_results, :results
20
+
21
+ # Initializes the MPNSResponse and runs response parsing
22
+ #
23
+ # @param responses [Array]. Array with device_urls and http responses
24
+ def initialize(responses)
25
+ parse_response responses
26
+ end
27
+
28
+ def ==(other)
29
+ (other.is_a?(MPNSResponse) &&
30
+ success == other.success &&
31
+ failed == other.failed &&
32
+ results == other.results) || super(other)
33
+ end
34
+
35
+ private
36
+
37
+ # Parses the response extracting counts for successful, failed messages.
38
+ # Also creates the results array assigning a MPNSResult subclass for each
39
+ # device URL the notification was sent to.
40
+ #
41
+ # @param responses [Array]. Array of hash responses
42
+ def parse_response(responses)
43
+ @success = responses.count { |response| response[:code] == 200 }
44
+ @failed = responses.count { |response| response[:code] != 200 }
45
+ @results = responses.map do |response|
46
+ mpns_result_for response[:code],
47
+ response[:device_url],
48
+ response[:headers]
49
+ end
50
+ end
51
+
52
+ # Factory method that, for each MPNS result object assigns a MPNSResult
53
+ # subclass.
54
+ #
55
+ # @param code [Integer]. The HTTP status code received
56
+ # @param device_url [String]. The receiver's MPNS device url.
57
+ # @param headers [Hash]. The HTTP headers received.
58
+ # @return [MPNSResult]. Corresponding MPNSResult subclass
59
+ def mpns_result_for(code, device_url, headers)
60
+ case code
61
+ when 200
62
+ MPNSResultOK.new device_url, headers
63
+ when 400
64
+ MalformedMPNSResultError.new device_url
65
+ when 401
66
+ MPNSAuthError.new device_url
67
+ when 404
68
+ MPNSInvalidError.new device_url, headers
69
+ when 406
70
+ MPNSLimitError.new device_url, headers
71
+ when 412
72
+ MPNSPreConditionError.new device_url, headers
73
+ when 500..599
74
+ MPNSInternalError.new device_url
75
+ else
76
+ MPNSResultError.new device_url
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+ 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)
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,14 @@
1
+ module RubyPushNotifications
2
+ # This module contains the required behavior expected by particular notifications
3
+ #
4
+ # @author Carlos Alonso
5
+ module NotificationResultsManager
6
+ extend Forwardable
7
+
8
+ def_delegators :@results, :success, :failed, :individual_results
9
+
10
+ # The corresponding object with the results from sending this notification
11
+ # that also will respond to #success, #failed and #individual_results
12
+ attr_accessor :results
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ require 'ruby-push-notifications/wns/wns_access'
2
+ require 'ruby-push-notifications/wns/wns_connection'
3
+ require 'ruby-push-notifications/wns/wns_notification'
4
+ require 'ruby-push-notifications/wns/wns_pusher'
5
+ require 'ruby-push-notifications/wns/wns_result'
6
+ require 'ruby-push-notifications/wns/wns_response'
@@ -0,0 +1,82 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+ require 'json'
4
+ require 'ostruct'
5
+
6
+ module RubyPushNotifications
7
+ module WNS
8
+ # This class is responsible for get access auth token for sending pushes
9
+ #
10
+ class WNSAccess
11
+
12
+ # This class is responsible for structurize response from login WNS service
13
+ #
14
+ class Response
15
+ # @return [OpenStruct]. Return structurized response
16
+ attr_reader :response
17
+
18
+ def initialize(response)
19
+ @response = structurize(response)
20
+ end
21
+
22
+ private
23
+
24
+ def structurize(response)
25
+ body = response.body.to_s.empty? ? {} : JSON.parse(response.body)
26
+ OpenStruct.new(
27
+ status_code: response.code.to_i,
28
+ status: response.message,
29
+ error: body['error'],
30
+ error_description: body['error_description'],
31
+ access_token: body['access_token'],
32
+ token_ttl: body['expires_in']
33
+ )
34
+ end
35
+ end
36
+
37
+ # @private Grant type for getting access token
38
+ GRANT_TYPE = 'client_credentials'.freeze
39
+
40
+ # @private Scope for getting access token
41
+ SCOPE = 'notify.windows.com'.freeze
42
+
43
+ # @private Url for getting access token
44
+ ACCESS_TOKEN_URL = 'https://login.live.com/accesstoken.srf'.freeze
45
+
46
+ # @return [String]. Sid
47
+ attr_reader :sid
48
+
49
+ # @return [String]. Secret token
50
+ attr_reader :secret
51
+
52
+ # @param type [String]. Sid
53
+ # @param type [String]. Secret
54
+ #
55
+ # You can get it on https://account.live.com/developers/applications/index
56
+ def initialize(sid, secret)
57
+ @sid = sid
58
+ @secret = secret
59
+ end
60
+
61
+ # Get access auth token for sending pushes
62
+ #
63
+ # https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-windows-push-notification-services--wns--overview
64
+ def get_token
65
+ body = {
66
+ grant_type: GRANT_TYPE,
67
+ client_id: sid,
68
+ client_secret: secret,
69
+ scope: SCOPE
70
+ }
71
+
72
+ url = URI.parse ACCESS_TOKEN_URL
73
+ http = Net::HTTP.new url.host, url.port
74
+ http.use_ssl = true
75
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
76
+ response = http.post url.request_uri, URI.encode_www_form(body)
77
+
78
+ Response.new response
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,97 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ module RubyPushNotifications
5
+ module WNS
6
+ # Encapsulates a connection to the WNS service
7
+ # Responsible for final connection with the service.
8
+ #
9
+ class WNSConnection
10
+ # @private Content-Type HTTP Header type string
11
+ CONTENT_TYPE_HEADER = 'Content-Type'.freeze
12
+
13
+ # @private Content-Length HTTP Header type string
14
+ CONTENT_LENGTH_HEADER = 'Content-Length'.freeze
15
+
16
+ # @private WNS type string
17
+ X_WNS_TYPE_HEADER = 'X-WNS-Type'.freeze
18
+
19
+ # @private Authorization string
20
+ AUTHORIZATION_HEADER = 'Authorization'.freeze
21
+
22
+ # @private Request for status type boolean
23
+ REQUEST_FOR_STATUS_HEADER = 'X-WNS-RequestForStatus'.freeze
24
+
25
+ # @private Content-Type type string
26
+ CONTENT_TYPE = {
27
+ badge: 'text/xml',
28
+ toast: 'text/xml',
29
+ tile: 'text/xml',
30
+ raw: 'application/octet-stream'
31
+ }.freeze
32
+
33
+ # @private Windows Phone Target Types
34
+ WP_TARGETS = {
35
+ badge: 'wns/badge',
36
+ toast: 'wns/toast',
37
+ tile: 'wns/tile',
38
+ raw: 'wns/raw'
39
+ }.freeze
40
+
41
+ # Issues a POST request to the WNS send endpoint to
42
+ # submit the given notifications.
43
+ #
44
+ # @param notifications [WNSNotification]. The notifications object to POST
45
+ # @param access_token [String] required. Access token for send push
46
+ # @param options [Hash] optional. Options for GCMPusher. Currently supports:
47
+ # * open_timeout [Integer]: Number of seconds to wait for the connection to open. Defaults to 30.
48
+ # * read_timeout [Integer]: Number of seconds to wait for one block to be read. Defaults to 30.
49
+ # @return [Array]. The response of post
50
+ # (http://msdn.microsoft.com/pt-br/library/windows/apps/ff941099)
51
+ def self.post(notifications, access_token, options = {})
52
+ body = notifications.as_wns_xml
53
+ headers = build_headers(access_token, notifications.data[:type], body.length.to_s)
54
+ responses = []
55
+ notifications.each_device do |url|
56
+ http = Net::HTTP.new url.host, url.port
57
+ http.open_timeout = options.fetch(:open_timeout, 30)
58
+ http.read_timeout = options.fetch(:read_timeout, 30)
59
+ if url.scheme == 'https'
60
+ http.use_ssl = true
61
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
62
+ end
63
+ response = http.post url.request_uri, body, headers
64
+ responses << { device_url: url.to_s, headers: capitalize_headers(response), code: response.code.to_i }
65
+ end
66
+ WNSResponse.new responses
67
+ end
68
+
69
+ # Build Header based on type and delay
70
+ #
71
+ # @param type [String]. Access token
72
+ # @param type [Symbol]. The type of notification
73
+ # @param type [String]. Content length of body
74
+ # @return [Hash]. Correct delay based on notification type
75
+ # https://msdn.microsoft.com/en-us/library/windows/apps/hh465435.aspx#send_notification_request
76
+ def self.build_headers(access_token, type, body_length)
77
+ {
78
+ CONTENT_TYPE_HEADER => CONTENT_TYPE[type],
79
+ X_WNS_TYPE_HEADER => WP_TARGETS[type],
80
+ CONTENT_LENGTH_HEADER => body_length,
81
+ AUTHORIZATION_HEADER => "Bearer #{access_token}",
82
+ REQUEST_FOR_STATUS_HEADER => 'true'
83
+ }
84
+ end
85
+
86
+ # Extract headers from response
87
+ # @param response [Net::HTTPResponse]. HTTP response for request
88
+ #
89
+ # @return [Hash]. Hash with headers with case-insensitive keys and capitalized string values
90
+ def self.capitalize_headers(response)
91
+ headers = {}
92
+ response.each_header { |k, v| headers[k] = v.capitalize }
93
+ headers
94
+ end
95
+ end
96
+ end
97
+ end