pushing 0.1.0

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