fcm-ruby-push-notifications 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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