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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +28 -0
- data/Appraisals +29 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +225 -0
- data/Rakefile +22 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/certs/apns_example_production.pem.enc +0 -0
- data/gemfiles/rails_42.gemfile +17 -0
- data/gemfiles/rails_50.gemfile +17 -0
- data/gemfiles/rails_51.gemfile +17 -0
- data/gemfiles/rails_edge.gemfile +20 -0
- data/lib/generators/pushing/USAGE +14 -0
- data/lib/generators/pushing/notifier_generator.rb +46 -0
- data/lib/generators/pushing/templates/application_notifier.rb +4 -0
- data/lib/generators/pushing/templates/initializer.rb +10 -0
- data/lib/generators/pushing/templates/notifier.rb +11 -0
- data/lib/generators/pushing/templates/template.json+apn.jbuilder +53 -0
- data/lib/generators/pushing/templates/template.json+fcm.jbuilder +75 -0
- data/lib/pushing.rb +16 -0
- data/lib/pushing/adapters.rb +43 -0
- data/lib/pushing/adapters/apn/apnotic_adapter.rb +86 -0
- data/lib/pushing/adapters/apn/houston_adapter.rb +33 -0
- data/lib/pushing/adapters/apn/lowdown_adapter.rb +70 -0
- data/lib/pushing/adapters/fcm/andpush_adapter.rb +47 -0
- data/lib/pushing/adapters/fcm/fcm_gem_adapter.rb +52 -0
- data/lib/pushing/adapters/test_adapter.rb +37 -0
- data/lib/pushing/base.rb +187 -0
- data/lib/pushing/delivery_job.rb +31 -0
- data/lib/pushing/log_subscriber.rb +44 -0
- data/lib/pushing/notification_delivery.rb +79 -0
- data/lib/pushing/platforms.rb +91 -0
- data/lib/pushing/railtie.rb +25 -0
- data/lib/pushing/rescuable.rb +28 -0
- data/lib/pushing/template_handlers.rb +15 -0
- data/lib/pushing/template_handlers/jbuilder_handler.rb +17 -0
- data/lib/pushing/version.rb +3 -0
- data/pushing.gemspec +30 -0
- 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
|
data/lib/pushing/base.rb
ADDED
@@ -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
|