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,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