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,91 @@
1
+ require 'json'
2
+
3
+ module RubyPushNotifications
4
+ module APNS
5
+ # Represents a APNS Notification.
6
+ # Manages the conversion of the notification to APNS binary format for
7
+ # each of the destinations.
8
+ # By default sets maximum expiration date (4 weeks).
9
+ #
10
+ # @author Carlos Alonso
11
+ class APNSNotification
12
+ include RubyPushNotifications::NotificationResultsManager
13
+
14
+ # @private. 4 weeks in seconds
15
+ WEEKS_4 = 2419200
16
+
17
+ # Initializes the APNS Notification
18
+ #
19
+ # @param [Array]. Array containing all destinations for the notification
20
+ # @param [Hash]. Hash with the data to use as payload.
21
+ def initialize(tokens, data)
22
+ @tokens = tokens
23
+ @data = data
24
+ end
25
+
26
+ # Method that yields the notification's binary for each of the receivers.
27
+ #
28
+ # @param starting_id [Integer]. Every notification encodes a unique ID for
29
+ # further reference. This parameter represents the first id the first
30
+ # notification of this group should use.
31
+ # @yieldparam [String]. APNS binary's representation of this notification.
32
+ # Consisting of:
33
+ # Notification = 2(1), FrameLength(4), items(FrameLength)
34
+ # Item = ItemID(1), ItemLength(2), data(ItemLength)
35
+ # Items:
36
+ # Device Token => Id: 1, length: 32, data: binary device token
37
+ # Payload => Id: 2, length: ??, data: json formatted payload
38
+ # Notification ID => Id: 3, length: 4, data: notif id as int
39
+ # Expiration Date => Id: 4, length: 4, data: Unix timestamp as int
40
+ # Priority => Id: 5, length: 1, data: 10 as 1 byte int
41
+ # (https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4)
42
+ def each_message(starting_id)
43
+ @tokens.each_with_index do |token, i|
44
+ bytes = device_token(token) + payload + notification_id(starting_id + i) + expiration_date + priority
45
+ yield [2, bytes.bytesize, bytes].pack 'cNa*'
46
+ end
47
+ end
48
+
49
+ # @return [Integer]. The number of binaries this notification will send.
50
+ # One for each receiver.
51
+ def count
52
+ @tokens.count
53
+ end
54
+
55
+ private
56
+
57
+ # @param [String]. The device token to encode.
58
+ # @return [String]. Binary representation of the device token field.
59
+ def device_token(token)
60
+ [1, 32, token].pack 'cnH64'
61
+ end
62
+
63
+ # Generates the APNS's binary representation of the notification's payload.
64
+ # Caches the value in an instance variable.
65
+ #
66
+ # @return [String]. Binary representation of the notification's payload.
67
+ def payload
68
+ @encoded_payload ||= -> {
69
+ json = (@data.respond_to?(:to_json) ? @data.to_json : JSON.dump(@data)).force_encoding 'ascii-8bit'
70
+ [2, json.bytesize, json].pack 'cna*'
71
+ }.call
72
+ end
73
+
74
+ # @param [Integer]. The unique ID for this notification.
75
+ # @return [String]. Binary representation of the notification id field.
76
+ def notification_id(id)
77
+ [3, 4, id].pack 'cnN'
78
+ end
79
+
80
+ # @return [String]. Binary representation of the expiration date field.
81
+ def expiration_date
82
+ [4, 4, (Time.now + WEEKS_4).to_i].pack 'cnN'
83
+ end
84
+
85
+ # @return [String]. Binary representation of the priority field.
86
+ def priority
87
+ [5, 1, 10].pack 'cnc'
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,84 @@
1
+
2
+ module RubyPushNotifications
3
+ module APNS
4
+ # This class coordinates the process of sending notifications.
5
+ # It takes care of reopening closed APNSConnections and seeking back to
6
+ # the failed notification to keep writing.
7
+ #
8
+ # Remember that APNS doesn't confirm successful notification, it just
9
+ # notifies when one went wrong and closes the connection. Therefore, this
10
+ # APNSPusher reconnects and rewinds the array until the notification that
11
+ # Apple rejected.
12
+ #
13
+ # @author Carlos Alonso
14
+ class APNSPusher
15
+
16
+ # @param certificate [String]. The PEM encoded APNS certificate.
17
+ # @param sandbox [Boolean]. Whether the certificate is an APNS sandbox or not.
18
+ # @param options [Hash] optional. Options for APNSPusher. Currently supports:
19
+ # * host [String]: Hostname of the APNS environment. Defaults to the official APNS hostname.
20
+ # * connect_timeout [Integer]: Number of seconds to wait for the connection to open. Defaults to 30.
21
+ def initialize(certificate, sandbox, password = nil, options = {})
22
+ @certificate = certificate
23
+ @pass = password
24
+ @sandbox = sandbox
25
+ @options = options
26
+ end
27
+
28
+ # Pushes the notifications.
29
+ # Builds an array with all the binaries (one for each notification and receiver)
30
+ # and pushes them sequentially to APNS monitoring the response.
31
+ # If an error is received, the connection is reopened and the process
32
+ # continues at the next notification after the failed one (pointed by the response error)
33
+ #
34
+ # For each notification assigns an array with the results of each submission.
35
+ #
36
+ # @param notifications [Array]. All the APNSNotifications to be sent.
37
+ def push(notifications)
38
+ conn = APNSConnection.open @certificate, @sandbox, @pass, @options
39
+
40
+ binaries = notifications.each_with_object([]) do |notif, binaries|
41
+ notif.each_message(binaries.count) do |msg|
42
+ binaries << msg
43
+ end
44
+ end
45
+
46
+ results = []
47
+ i = 0
48
+ while i < binaries.count
49
+ conn.write binaries[i]
50
+
51
+ if i == binaries.count-1
52
+ conn.flush
53
+ rs, = IO.select([conn], nil, nil, 2)
54
+ else
55
+ rs, = IO.select([conn], [conn])
56
+ end
57
+ if rs && rs.any?
58
+ packed = rs[0].read 6
59
+ if packed.nil? && i == 0
60
+ # The connection wasn't properly open
61
+ # Probably because of wrong certificate/sandbox? combination
62
+ results << UNKNOWN_ERROR_STATUS_CODE
63
+ else
64
+ err = packed.unpack 'ccN'
65
+ results.slice! err[2]..-1
66
+ results << err[1]
67
+ i = err[2]
68
+ conn = APNSConnection.open @certificate, @sandbox, @pass, @options
69
+ end
70
+ else
71
+ results << NO_ERROR_STATUS_CODE
72
+ end
73
+ i += 1
74
+ end
75
+
76
+ conn.close
77
+
78
+ notifications.each do |notif|
79
+ notif.results = APNSResults.new(results.slice! 0, notif.count)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,30 @@
1
+ module RubyPushNotifications
2
+ module APNS
3
+ # This class is responsible for holding and
4
+ # managing result of a pushed notification.
5
+ #
6
+ # @author Carlos Alonso
7
+ class APNSResults
8
+
9
+ # @return [Array] of each destination's individual result.
10
+ attr_reader :individual_results
11
+
12
+ # Initializes the result
13
+ #
14
+ # @param [Array] containing each destination's individual result.
15
+ def initialize(results)
16
+ @individual_results = results
17
+ end
18
+
19
+ # @return [Integer] numer of successfully pushed notifications
20
+ def success
21
+ @success ||= individual_results.count { |r| r == NO_ERROR_STATUS_CODE }
22
+ end
23
+
24
+ # @return [Integer] number of failed notifications
25
+ def failed
26
+ @failed ||= individual_results.count { |r| r != NO_ERROR_STATUS_CODE }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ require 'ruby-push-notifications/fcm/fcm_request'
2
+ require 'ruby-push-notifications/fcm/fcm_connection'
3
+ require 'ruby-push-notifications/fcm/fcm_notification'
4
+ require 'ruby-push-notifications/fcm/fcm_pusher'
5
+ require 'ruby-push-notifications/fcm/fcm_response'
6
+ require 'ruby-push-notifications/fcm/fcm_error'
7
+ require 'ruby-push-notifications/fcm/fcm_result'
8
+
@@ -0,0 +1,60 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ require 'httparty'
5
+ require 'cgi'
6
+ require 'json'
7
+
8
+ module RubyPushNotifications
9
+ module FCM
10
+ # Encapsulates a connection to the FCM service
11
+ # Responsible for final connection with the service.
12
+ #
13
+ # @author Carlos Alonso
14
+ class FCMConnection
15
+
16
+ # @private The URL of the Android FCM endpoint
17
+ FCM_URL = 'https://fcm.googleapis.com/fcm'
18
+
19
+ # @private Content-Type HTTP Header string
20
+ CONTENT_TYPE_HEADER = 'Content-Type'
21
+
22
+ # @private Application/JSON content type
23
+ JSON_CONTENT_TYPE = 'application/json'
24
+
25
+ # @private Authorization HTTP Header String
26
+ AUTHORIZATION_HEADER = 'Authorization'
27
+ GROUP_NOTIFICATION_BASE_URI = 'https://android.googleapis.com/gcm'
28
+
29
+ # Issues a POST request to the FCM send endpoint to
30
+ # submit the given notifications.
31
+ #
32
+ # @param notification [String]. The text to POST
33
+ # @param key [String]. The FCM sender id to use
34
+ # (https://developer.android.com/google/gcm/gcm.html#senderid)
35
+ # @return [FCMResponse]. The FCMResponse that encapsulates the received response
36
+ def self.post(notification, key)
37
+ headers = {
38
+ CONTENT_TYPE_HEADER => JSON_CONTENT_TYPE,
39
+ AUTHORIZATION_HEADER => "key=#{key}"
40
+ }
41
+ # TODO: remove to_json if causing error
42
+ params = {
43
+ body: notification.to_json,
44
+ headers: {
45
+ AUTHORIZATION_HEADER => "key=#{key}",
46
+ CONTENT_TYPE_HEADER => JSON_CONTENT_TYPE
47
+ }
48
+ }
49
+
50
+ fcm_request = FCMRequest.new(params)
51
+ response = fcm_request.make_request
52
+ # puts '*' * 10
53
+ # puts response
54
+ # puts '*' * 10
55
+ FCMResponse.new response.code.to_i, response.body
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+
2
+ module RubyPushNotifications
3
+ module FCM
4
+ # Base class for all FCM related errors
5
+ #
6
+ # @author Carlos Alonso
7
+ class FCMError < StandardError ; end
8
+
9
+ class MalformedFCMJSONError < FCMError ; end
10
+
11
+ class FCMAuthError < FCMError ; end
12
+
13
+ class FCMInternalError < FCMError ; end
14
+
15
+ class UnexpectedFCMResponseError < FCMError ; end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+
2
+ module RubyPushNotifications
3
+ module FCM
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 FCMNotification
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_fcm_json
24
+ JSON.dump(
25
+ registration_ids: @registration_ids,
26
+ data: @data
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+
2
+ module RubyPushNotifications
3
+ module FCM
4
+
5
+ # This class is responsible for sending notifications to the FCM service.
6
+ #
7
+ # @author Carlos Alonso
8
+ class FCMPusher
9
+
10
+ # Initializes the FCMPusher
11
+ #
12
+ # @param key [String]. FCM sender id to use
13
+ def initialize(key)
14
+ @key = key
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 FCMNotification to send.
22
+ def push(notifications)
23
+ notifications.each do |notif|
24
+ notif.results = FCMConnection.post notif.as_fcm_json, @key
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ require 'httparty'
2
+ require 'cgi'
3
+ require 'json'
4
+
5
+ module RubyPushNotifications
6
+ module FCM
7
+ # Encapsulates a connection to the FCM service
8
+ # Responsible for final connection with the service.
9
+ #
10
+ # @author Carlos Alonso
11
+ class FCMRequest
12
+ include HTTParty
13
+ default_timeout 30
14
+ format :json
15
+
16
+ base_uri 'https://fcm.googleapis.com/fcm'
17
+
18
+ def initialize(params = {})
19
+ @params = params
20
+ end
21
+ def make_request
22
+ puts '********** make_request *************'
23
+ puts @params
24
+ response = self.class.post('/send', @params)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,89 @@
1
+
2
+ module RubyPushNotifications
3
+ module FCM
4
+
5
+ # This class encapsulates a response received from the FCM service
6
+ # and helps parsing and understanding the received meesages/codes.
7
+ #
8
+ # @author Carlos Alonso
9
+ class FCMResponse
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 MalformedFCMJSONError, body
40
+ when 401
41
+ raise FCMAuthError, body
42
+ when 500..599
43
+ raise FCMInternalError, body
44
+ else
45
+ raise UnexpectedFCMResponseError, code
46
+ end
47
+ end
48
+
49
+ def ==(other)
50
+ (other.is_a?(FCMResponse) &&
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| fcm_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 fcm_result_for(result)
79
+ if canonical_id = result[:registration_id]
80
+ FCMCanonicalIDResult.new canonical_id
81
+ elsif error = result[:error]
82
+ FCMResultError.new error
83
+ else
84
+ FCMResultOK.new
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end