pushing 0.1.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +28 -0
  4. data/Appraisals +29 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +17 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +225 -0
  9. data/Rakefile +22 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/certs/apns_example_production.pem.enc +0 -0
  13. data/gemfiles/rails_42.gemfile +17 -0
  14. data/gemfiles/rails_50.gemfile +17 -0
  15. data/gemfiles/rails_51.gemfile +17 -0
  16. data/gemfiles/rails_edge.gemfile +20 -0
  17. data/lib/generators/pushing/USAGE +14 -0
  18. data/lib/generators/pushing/notifier_generator.rb +46 -0
  19. data/lib/generators/pushing/templates/application_notifier.rb +4 -0
  20. data/lib/generators/pushing/templates/initializer.rb +10 -0
  21. data/lib/generators/pushing/templates/notifier.rb +11 -0
  22. data/lib/generators/pushing/templates/template.json+apn.jbuilder +53 -0
  23. data/lib/generators/pushing/templates/template.json+fcm.jbuilder +75 -0
  24. data/lib/pushing.rb +16 -0
  25. data/lib/pushing/adapters.rb +43 -0
  26. data/lib/pushing/adapters/apn/apnotic_adapter.rb +86 -0
  27. data/lib/pushing/adapters/apn/houston_adapter.rb +33 -0
  28. data/lib/pushing/adapters/apn/lowdown_adapter.rb +70 -0
  29. data/lib/pushing/adapters/fcm/andpush_adapter.rb +47 -0
  30. data/lib/pushing/adapters/fcm/fcm_gem_adapter.rb +52 -0
  31. data/lib/pushing/adapters/test_adapter.rb +37 -0
  32. data/lib/pushing/base.rb +187 -0
  33. data/lib/pushing/delivery_job.rb +31 -0
  34. data/lib/pushing/log_subscriber.rb +44 -0
  35. data/lib/pushing/notification_delivery.rb +79 -0
  36. data/lib/pushing/platforms.rb +91 -0
  37. data/lib/pushing/railtie.rb +25 -0
  38. data/lib/pushing/rescuable.rb +28 -0
  39. data/lib/pushing/template_handlers.rb +15 -0
  40. data/lib/pushing/template_handlers/jbuilder_handler.rb +17 -0
  41. data/lib/pushing/version.rb +3 -0
  42. data/pushing.gemspec +30 -0
  43. metadata +211 -0
@@ -0,0 +1,33 @@
1
+ require 'houston'
2
+
3
+ module Pushing
4
+ module Adapters
5
+ class HoustonAdapter
6
+ attr_reader :certificate_path, :environment, :client
7
+
8
+ def initialize(apn_settings)
9
+ @certificate_path = apn_settings.certificate_path
10
+ @environment = apn_settings.environment
11
+
12
+ @client = {
13
+ production: Houston::Client.production,
14
+ development: Houston::Client.development
15
+ }
16
+ @client[:production].certificate = @client[:development].certificate = File.read(certificate_path)
17
+ end
18
+
19
+ def push!(notification)
20
+ payload = notification.payload
21
+ aps = payload.delete(:aps)
22
+ aps[:device] = notification.device_token
23
+
24
+ houston_notification = Houston::Notification.new(payload.merge(aps))
25
+ client[notification.environment || environment].push(houston_notification)
26
+ rescue => cause
27
+ error = Pushing::ApnDeliveryError.new("Error while trying to send push notification: #{cause.message}", nil, notification)
28
+
29
+ raise error, error.message, cause.backtrace
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,70 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'json'
4
+
5
+ module Pushing
6
+ module Adapters
7
+ class LowdownAdapter
8
+ attr_reader :environment, :topic, :clients
9
+
10
+ def initialize(apn_settings)
11
+ @environment = apn_settings.environment.to_sym
12
+ @topic = apn_settings.topic
13
+
14
+ # Don't load lowdown earlier as it may load Celluloid (and start it)
15
+ # before daemonizing the workers spun up by a gem (e,g, delayed_job).
16
+ require 'lowdown' unless defined?(Lodwown)
17
+
18
+ cert = File.read(apn_settings.certificate_path)
19
+ @clients = {
20
+ development: Lowdown::Client.production(false, certificate: cert, keep_alive: true),
21
+ production: Lowdown::Client.production(true, certificate: cert, keep_alive: true)
22
+ }
23
+ end
24
+
25
+ def push!(notification)
26
+ if notification.headers[:'apns-id']
27
+ warn("The lowdown gem does not allow for overriding `apns_id'.")
28
+ end
29
+
30
+ if notification.headers[:'apns-collapse-id']
31
+ warn("The lowdown gem does not allow for overriding `apns-collapse-id'.")
32
+ end
33
+
34
+ lowdown_notification = Lowdown::Notification.new(token: notification.device_token)
35
+ lowdown_notification.payload = notification.payload
36
+
37
+ lowdown_notification.expiration = notification.headers[:'apns-expiration'].to_i if notification.headers[:'apns-expiration']
38
+ lowdown_notification.priority = notification.headers[:'apns-priority']
39
+ lowdown_notification.topic = notification.headers[:'apns-topic'] || topic
40
+
41
+ response = nil
42
+ clients[notification.environment || environment].group do |group|
43
+ group.send_notification(lowdown_notification) do |_response|
44
+ response = _response
45
+ end
46
+ end
47
+
48
+ raise response.raw_body if !response.success?
49
+ ApnResponse.new(response)
50
+ rescue => cause
51
+ response = response ? ApnResponse.new(response) : nil
52
+ error = Pushing::ApnDeliveryError.new("Error while trying to send push notification: #{cause.message}", response, notification)
53
+
54
+ raise error, error.message, cause.backtrace
55
+ end
56
+
57
+ class ApnResponse < SimpleDelegator
58
+ def code
59
+ __getobj__.status
60
+ end
61
+
62
+ def json
63
+ @json ||= JSON.parse(__getobj__.raw_body, symbolize_names: true) if __getobj__.raw_body
64
+ end
65
+ end
66
+
67
+ private_constant :ApnResponse
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,47 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'andpush'
4
+ require 'active_support/core_ext/hash/transform_values'
5
+
6
+ module Pushing
7
+ module Adapters
8
+ class AndpushAdapter
9
+ attr_reader :server_key
10
+
11
+ def initialize(fcm_settings)
12
+ @server_key = fcm_settings.server_key
13
+ end
14
+
15
+ def push!(notification)
16
+ FcmResponse.new(client.push(notification.payload))
17
+ rescue => e
18
+ response = e.respond_to?(:response) ? FcmResponse.new(e.response) : nil
19
+ error = Pushing::FcmDeliveryError.new("Error while trying to send push notification: #{e.message}", response, notification)
20
+
21
+ raise error, error.message, e.backtrace
22
+ end
23
+
24
+ private
25
+
26
+ def client
27
+ @client ||= Andpush.build(server_key)
28
+ end
29
+
30
+ class FcmResponse < SimpleDelegator
31
+ def json
32
+ @json ||= __getobj__.json
33
+ end
34
+
35
+ def code
36
+ __getobj__.code.to_i
37
+ end
38
+
39
+ def headers
40
+ __getobj__.headers.transform_values {|value| value.join(", ") }
41
+ end
42
+ end
43
+
44
+ private_constant :FcmResponse
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,52 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'json'
4
+ require 'fcm'
5
+ require 'active_support/core_ext/hash/slice'
6
+
7
+ module Pushing
8
+ module Adapters
9
+ class FcmGemAdapter
10
+ SUCCESS_CODES = (200..299).freeze
11
+
12
+ attr_reader :server_key
13
+
14
+ def initialize(fcm_settings)
15
+ @server_key = fcm_settings.server_key
16
+ end
17
+
18
+ def push!(notification)
19
+ json = notification.payload
20
+ ids = json.delete(:registration_ids) || Array(json.delete(:to))
21
+ response = FCM.new(server_key).send(ids, json)
22
+
23
+ if SUCCESS_CODES.include?(response[:status_code])
24
+ FcmResponse.new(response.slice(:body, :headers, :status_code).merge(raw_response: response))
25
+ else
26
+ raise "#{response[:response]} (response body: #{response[:body]})"
27
+ end
28
+ rescue => cause
29
+ resopnse = FcmResponse.new(response.slice(:body, :headers, :status_code).merge(raw_response: response)) if response
30
+ error = Pushing::FcmDeliveryError.new("Error while trying to send push notification: #{cause.message}", resopnse, notification)
31
+
32
+ raise error, error.message, cause.backtrace
33
+ end
34
+
35
+ class FcmResponse
36
+ attr_reader :body, :headers, :status_code, :raw_response
37
+
38
+ alias code status_code
39
+
40
+ def initialize(body: , headers: , status_code: , raw_response: )
41
+ @body, @headers, @status_code, @raw_response = body, headers, status_code, raw_response
42
+ end
43
+
44
+ def json
45
+ @json ||= JSON.parse(body, symbolize_names: true) if body.is_a?(String)
46
+ end
47
+ end
48
+
49
+ private_constant :FcmResponse
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,37 @@
1
+ require 'active_support/core_ext/module/attribute_accessors'
2
+
3
+ module Pushing
4
+ module Adapters
5
+ class TestAdapter
6
+ class Deliveries
7
+ include Enumerable
8
+
9
+ def initialize
10
+ @deliveries = []
11
+ end
12
+
13
+ delegate :each, :empty?, :clear, :<<, :length, :size, to: :@deliveries
14
+
15
+ def apn
16
+ select {|delivery| delivery.is_a?(Platforms::ApnPayload) }
17
+ end
18
+
19
+ def fcm
20
+ select {|delivery| delivery.is_a?(Platforms::FcmPayload) }
21
+ end
22
+ end
23
+
24
+ private_constant :Deliveries
25
+ cattr_accessor :deliveries
26
+ self.deliveries = Deliveries.new
27
+
28
+ def initialize(*)
29
+ end
30
+
31
+ def push!(notification)
32
+ self.class.deliveries << notification if notification
33
+ notification
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,187 @@
1
+ # frozen-string-literal: true
2
+
3
+ require "abstract_controller"
4
+ require 'active_support/core_ext/module/attribute_accessors'
5
+ require 'active_support/core_ext/object/blank'
6
+
7
+ require 'pushing/log_subscriber'
8
+ require 'pushing/rescuable'
9
+ require 'pushing/platforms'
10
+ require 'pushing/template_handlers'
11
+
12
+ module Pushing
13
+ class Base < AbstractController::Base
14
+ include Rescuable
15
+
16
+ abstract!
17
+
18
+ include AbstractController::Rendering
19
+ include AbstractController::Logger
20
+ include AbstractController::Helpers
21
+ include AbstractController::Translation
22
+ include AbstractController::AssetPaths
23
+ include AbstractController::Callbacks
24
+ begin
25
+ include AbstractController::Caching
26
+ rescue NameError
27
+ # AbstractController::Caching does not exist in rails 4.2. No-op.
28
+ end
29
+
30
+ include ActionView::Rendering
31
+
32
+ PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
33
+
34
+ def _protected_ivars # :nodoc:
35
+ PROTECTED_IVARS
36
+ end
37
+
38
+ cattr_accessor :deliver_later_queue_name
39
+ self.deliver_later_queue_name = :notifiers
40
+
41
+ cattr_reader :delivery_notification_observers
42
+ @@delivery_notification_observers = []
43
+
44
+ cattr_reader :delivery_interceptors
45
+ @@delivery_interceptors = []
46
+
47
+ class << self
48
+ delegate :deliveries, :deliveries=, to: Pushing::Adapters::TestAdapter
49
+
50
+ # Register one or more Observers which will be notified when notification is delivered.
51
+ def register_observers(*observers)
52
+ observers.flatten.compact.each { |observer| register_observer(observer) }
53
+ end
54
+
55
+ # Register one or more Interceptors which will be called before notification is sent.
56
+ def register_interceptors(*interceptors)
57
+ interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
58
+ end
59
+
60
+ # Register an Observer which will be notified when notification is delivered.
61
+ # Either a class, string or symbol can be passed in as the Observer.
62
+ # If a string or symbol is passed in it will be camelized and constantized.
63
+ def register_observer(observer)
64
+ unless delivery_notification_observers.include?(observer)
65
+ delivery_notification_observers << observer
66
+ end
67
+ end
68
+
69
+ # Register an Interceptor which will be called before notification is sent.
70
+ # Either a class, string or symbol can be passed in as the Interceptor.
71
+ # If a string or symbol is passed in it will be camelized and constantized.
72
+ def register_interceptor(interceptor)
73
+ unless delivery_interceptors.include?(interceptor)
74
+ delivery_interceptors << interceptor
75
+ end
76
+ end
77
+
78
+ def inform_observers(notification, response)
79
+ delivery_notification_observers.each do |observer|
80
+ observer.delivered_notification(notification, response)
81
+ end
82
+ end
83
+
84
+ def inform_interceptors(notification)
85
+ delivery_interceptors.each do |interceptor|
86
+ interceptor.delivering_notification(notification)
87
+ end
88
+ end
89
+
90
+ def notifier_name
91
+ @notifier_name ||= anonymous? ? "anonymous" : name.underscore
92
+ end
93
+ # Allows to set the name of current notifier.
94
+ attr_writer :notifier_name
95
+ alias :controller_path :notifier_name
96
+
97
+ # Wraps a notification delivery inside of <tt>ActiveSupport::Notifications</tt> instrumentation.
98
+ def deliver_notification(notification) #:nodoc:
99
+ ActiveSupport::Notifications.instrument("deliver.push_notification") do |payload|
100
+ set_payload_for_notification(payload, notification)
101
+ yield # Let NotificationDelivery do the delivery actions
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def set_payload_for_notification(payload, notification)
108
+ payload[:notifier] = name
109
+ payload[:notification] = notification.message.to_h
110
+ end
111
+
112
+ def method_missing(method_name, *args)
113
+ if action_methods.include?(method_name.to_s)
114
+ NotificationDelivery.new(self, method_name, *args)
115
+ else
116
+ super
117
+ end
118
+ end
119
+
120
+ def respond_to_missing?(method, include_all = false)
121
+ action_methods.include?(method.to_s) || super
122
+ end
123
+ end
124
+
125
+ def process(method_name, *args) #:nodoc:
126
+ payload = {
127
+ notifier: self.class.name,
128
+ action: method_name,
129
+ args: args
130
+ }
131
+
132
+ ActiveSupport::Notifications.instrument("process.push_notification", payload) do
133
+ super
134
+ @_notification ||= NullNotification.new
135
+ end
136
+ end
137
+
138
+ class NullNotification #:nodoc:
139
+ def respond_to?(string, include_all = false)
140
+ true
141
+ end
142
+
143
+ def method_missing(*args)
144
+ nil
145
+ end
146
+ end
147
+
148
+ attr_internal :notification
149
+
150
+ def push(headers)
151
+ return notification if notification && headers.blank?
152
+
153
+ payload = {}
154
+ headers.each do |platform, options|
155
+ payload_class = ::Pushing::Platforms.lookup(platform)
156
+
157
+ if payload_class.should_render?(options)
158
+ json = render_json(platform, headers)
159
+ payload[platform] = payload_class.new(json, options)
160
+ end
161
+ end
162
+
163
+ # TODO: Do not use OpenStruct
164
+ @_notification = OpenStruct.new(payload)
165
+ end
166
+
167
+ private
168
+
169
+ def render_json(platform, headers)
170
+ templates_path = headers[:template_path] || self.class.notifier_name
171
+ templates_name = headers[:template_name] || action_name
172
+
173
+ lookup_context.variants = platform
174
+ template = lookup_context.find(templates_name, Array(templates_path))
175
+
176
+ unless template.instance_variable_get(:@compiled)
177
+ engine = File.extname(template.identifier).tr!(".", "")
178
+ handler = ::Pushing::TemplateHandlers.lookup(engine)
179
+ template.instance_variable_set(:@handler, handler)
180
+ end
181
+
182
+ view_renderer.render_template(view_context, template: template)
183
+ end
184
+
185
+ ActiveSupport.run_load_hooks(:pushing, self)
186
+ end
187
+ end
@@ -0,0 +1,31 @@
1
+ require 'active_job'
2
+
3
+ module Pushing
4
+ class DeliveryJob < ActiveJob::Base # :nodoc:
5
+ queue_as { Pushing::Base.deliver_later_queue_name }
6
+
7
+ if ActiveSupport::VERSION::MAJOR > 4
8
+ rescue_from StandardError, with: :handle_exception_with_notifier_class
9
+ end
10
+
11
+ def perform(notifier, mail_method, delivery_method, *args) #:nodoc:
12
+ notifier.constantize.public_send(mail_method, *args).send(delivery_method)
13
+ end
14
+
15
+ private
16
+
17
+ def notifier_class
18
+ if notifier = Array(@serialized_arguments).first || Array(arguments).first
19
+ notifier.constantize
20
+ end
21
+ end
22
+
23
+ def handle_exception_with_notifier_class(exception)
24
+ if klass = notifier_class
25
+ klass.handle_exception exception
26
+ else
27
+ raise exception
28
+ end
29
+ end
30
+ end
31
+ end