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
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pushing"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "houston", require: false
6
+ gem "apnotic", require: false
7
+ gem "lowdown", require: false
8
+ gem "andpush", require: false
9
+ gem "fcm", require: false
10
+ gem "pry"
11
+ gem "pry-byebug", platforms: :mri
12
+ gem "rails", "~> 4.2.0"
13
+ gem "actionpack", "~> 4.2.0"
14
+ gem "actionview", "~> 4.2.0"
15
+ gem "activejob", "~> 4.2.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "houston", require: false
6
+ gem "apnotic", require: false
7
+ gem "lowdown", require: false
8
+ gem "andpush", require: false
9
+ gem "fcm", require: false
10
+ gem "pry"
11
+ gem "pry-byebug", platforms: :mri
12
+ gem "railties", "~> 5.0.0"
13
+ gem "actionpack", "~> 5.0.0"
14
+ gem "actionview", "~> 5.0.0"
15
+ gem "activejob", "~> 5.0.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "houston", require: false
6
+ gem "apnotic", require: false
7
+ gem "lowdown", require: false
8
+ gem "andpush", require: false
9
+ gem "fcm", require: false
10
+ gem "pry"
11
+ gem "pry-byebug", platforms: :mri
12
+ gem "railties", "~> 5.1.0"
13
+ gem "actionpack", "~> 5.1.0"
14
+ gem "actionview", "~> 5.1.0"
15
+ gem "activejob", "~> 5.1.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,20 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git "git://github.com/rails/rails.git" do
6
+ gem "railties"
7
+ gem "actionpack"
8
+ gem "actionview"
9
+ gem "activejob"
10
+ end
11
+
12
+ gem "houston", require: false
13
+ gem "apnotic", require: false
14
+ gem "lowdown", require: false
15
+ gem "andpush", require: false
16
+ gem "fcm", require: false
17
+ gem "pry"
18
+ gem "pry-byebug", platforms: :mri
19
+
20
+ gemspec path: "../"
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Stubs out a new notifier and its views. Passes the notifier name, either
3
+ CamelCased or under_scored, and an optional list of push notifications
4
+ as arguments.
5
+
6
+ This generates a notifier class in app/notifiers.
7
+
8
+ Example:
9
+ rails generate pushing:notifier TweetNotifier new_mention_in_tweet
10
+
11
+ creates a TweetNotifier class and views:
12
+ Mailer: app/notifiers/tweet_notifier.rb
13
+ Views: app/views/tweet_notifier/new_mention_in_tweet.json+apn.jbuilder
14
+ app/views/tweet_notifier/new_mention_in_tweet.json+fcm.jbuilder
@@ -0,0 +1,46 @@
1
+ module Pushing
2
+ class NotifierGenerator < Rails::Generators::NamedBase
3
+ source_root File.expand_path("../templates", __FILE__)
4
+
5
+ argument :actions, type: :array, default: [], banner: "method method"
6
+
7
+ check_class_collision suffix: "Notifier"
8
+
9
+ def create_notifier_file
10
+ template "notifier.rb", File.join("app/notifiers", class_path, "#{file_name}_notifier.rb")
11
+
12
+ actions.each do |action|
13
+ template "template.json+apn.jbuilder", File.join("app/views/", "#{file_name}_notifier", "#{action}.json+apn.jbuilder")
14
+ template "template.json+fcm.jbuilder", File.join("app/views/", "#{file_name}_notifier", "#{action}.json+fcm.jbuilder")
15
+ end
16
+
17
+ in_root do
18
+ if behavior == :invoke && !File.exist?(application_notifier_file_name)
19
+ template "application_notifier.rb", application_notifier_file_name
20
+ end
21
+
22
+ if behavior == :invoke && !File.exist?(initializer_file_name)
23
+ template "initializer.rb", initializer_file_name
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def file_name # :doc:
31
+ @_file_name ||= super.gsub(/_notifier/i, "")
32
+ end
33
+
34
+ def initializer_file_name
35
+ @_initializer_file_name ||= "config/initializers/pushing.rb"
36
+ end
37
+
38
+ def application_notifier_file_name
39
+ @_application_notifier_file_name ||= if mountable_engine?
40
+ "app/notifiers/#{namespaced_path}/application_notifier.rb"
41
+ else
42
+ "app/notifiers/application_notifier.rb"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ <% module_namespacing do -%>
2
+ class ApplicationNotifier < Pushing::Base
3
+ end
4
+ <% end %>
@@ -0,0 +1,10 @@
1
+ Pushing::Platforms.configure do |config|
2
+ config.fcm.adapter = Rails.env.test? ? :test : :andpush
3
+ config.fcm.server_key = 'YOUR_FCM_TEST_SERVER_KEY'
4
+
5
+ config.apn.environment = Rails.env.production? ? :production : :development
6
+ config.apn.adapter = Rails.env.test? ? :test : :apnotic
7
+ config.apn.topic = 'com.awesomecompany.app'
8
+ config.apn.certificate_path = '/config/your_certificate.pem'
9
+ config.apn.certificate_password = 'PASSWORD_FOR_CERT'
10
+ end
@@ -0,0 +1,11 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Notifier < ApplicationNotifier
3
+ <% actions.each do |action| -%>
4
+ def <%= action %>
5
+ @greeting = "Hi"
6
+
7
+ push apn: "device-token", fcm: true
8
+ end
9
+ <% end -%>
10
+ end
11
+ <% end -%>
@@ -0,0 +1,53 @@
1
+ # In this file you can customize the APN payload. For more details about key preference, see
2
+ # Apple's offitial documentation here: http://bit.ly/apns-doc
3
+
4
+ # the JSON dictionary can include custom keys and values with your app-specific content.
5
+ # json.custom_data1 'content1...'
6
+ # json.custom_data2 'content2...'
7
+
8
+ # The aps dictionary contains the keys used by Apple to deliver the notification to the user's device.
9
+ json.aps do
10
+
11
+ # Include this key when you want the system to display a standard alert or a banner (Dictionary or String).
12
+ # json.alert 'REPLACE_WITH_ACTUAL_TITLE'
13
+ json.alert do
14
+ # A short string describing the purpose of the notification.
15
+ json.title 'REPLACE_WITH_ACTUAL_TITLE'
16
+
17
+ # The text of the alert message.
18
+ json.body 'REPLACE_WITH_ACTUAL_BODY'
19
+
20
+ # The key to a title string in the Localizable.strings file for the current localization.
21
+ # json.set! 'title-loc-key', 'key in Localizable.strings'
22
+
23
+ # Variable string values to appear in place of the format specifiers in 'title-loc-key'.
24
+ # json.set! 'title-loc-args', ['arg1', 'arg2']
25
+
26
+ # If a string is specified, the system displays an alert that includes the Close and View buttons.
27
+ # json.set! 'action-loc-key', 'key in Localizable.strings'
28
+
29
+ # A key to an alert-message string in a Localizable.strings file for the current localization.
30
+ # json.set! 'loc-key', 'key in Localizable.strings'
31
+
32
+ # Variable string values to appear in place of the format specifiers in loc-key.
33
+ # json.set! 'loc-args', ['arg1', 'arg2']
34
+
35
+ # The filename of an image file in the app bundle, with or without the filename extension.
36
+ # json.set! 'launch-image', 'your_launch_iamge.png'
37
+ end
38
+
39
+ # Include this key when you want the system to modify the badge of your app icon.
40
+ json.badge 1
41
+
42
+ # Include this key when you want the system to play a sound.
43
+ json.sound 'bingbong.aiff'
44
+
45
+ # Provide this key with a string value that represents the notification's type.
46
+ # json.category 'your-category-type', 'CATEGORY-TYPE'
47
+
48
+ # Include this key with a value of 1 to configure a silent notification.
49
+ # json.set! 'content-available', 1
50
+
51
+ # Provide this key with a string value that represents the app-specific identifier for grouping notifications.
52
+ # json.set! 'thread-id', 'THREAD-ID'
53
+ end
@@ -0,0 +1,75 @@
1
+ # In this file you can customize the FCM payload. For more details about Firebase Cloud Messaging HTTP Protocol, see
2
+ # Google's offitial documentation here: https://goo.gl/tgssxy
3
+
4
+ # The recipient of a message. The value can be a device's registration token, a device group's notification key, or a
5
+ # single topic (prefixed with /topics/). Either the 'to' or 'registration_ids' key should be present.
6
+ json.to 'REPLACE_WITH_ACTUAL_REGISTRATION_ID_OR_TOPIC'
7
+
8
+ # The recipient of a multicast message, a message sent to more than one registration token. Either the 'to' or
9
+ # 'registration_ids' key should be present.
10
+ # json.registration_ids 'REPLACE_WITH_ACTUAL_REGISTRATION_IDS_OR_TOPIC'
11
+
12
+ # A logical expression of conditions that determine the message target.
13
+ # json.condition 'Topic'
14
+
15
+ # This parameter identifies a group of messages (e.g., with collapse_key:
16
+ # "Updates Available") that can be collapsed.
17
+ # json.collapse_key "Updates Available"
18
+
19
+ # Sets the priority of the message. Valid values are "normal" and "high".
20
+ # json.priority "high"
21
+
22
+ # How long (in seconds) the message should be kept in FCM storage if the device is offline.
23
+ # json.time_to_live 4.weeks.to_i
24
+
25
+ # The package name of the application where the registration tokens must match in order to receive the message.
26
+ # json.restricted_package_name 'com.yourdomain.app'
27
+
28
+ # This parameter, when set to true, allows developers to test a request without actually sending a message.
29
+ # json.dry_run true
30
+
31
+ # The custom key-value pairs of the message's payload.
32
+ # json.data do
33
+ # json.custom_data "data..."
34
+ # end
35
+
36
+ # The predefined, user-visible key-value pairs of the notification payload.
37
+ json.notification do
38
+ # The notification's title.
39
+ json.title 'REPLACE_WITH_ACTUAL_TITLE'
40
+
41
+ # The notification's body text.
42
+ json.body 'REPLACE_WITH_ACTUAL_BODY'
43
+
44
+ # The notification's channel id (new in Android O).
45
+ # json.android_channel_id "my_channel_01"
46
+
47
+ # The notification's icon.
48
+ json.icon 1
49
+
50
+ # The sound to play when the device receives the notificatio
51
+ json.sound 'default'
52
+
53
+ # Identifier used to replace existing notifications in the notification drawer.
54
+ # json.tag 'tag-name'
55
+
56
+ # The notification's icon color, expressed in #rrggbb format.
57
+ # json.color "#ffffff"
58
+
59
+ # The action associated with a user click on the notification.
60
+ # json.click_action "OPEN_ACTIVITY_1"
61
+
62
+ # The key to the body in the app's string resources to use to localize the body to the user's current localization.
63
+ # json.body_loc_key 'key in Localizable.strings'
64
+
65
+ # String values to be used in place of the format specifiers in 'body_loc_key' to use to localize the body to the
66
+ # user's current localization.
67
+ # json.body_loc_args ['arg1', 'arg2']
68
+
69
+ # The key to the title in the app's string resources to use to localize the title to the user's current localization.
70
+ # json.title_loc_key 'key in Localizable.strings'
71
+
72
+ # String values to be used in place of the format specifiers in title_loc_key to use to localize the title text to
73
+ # the user's current localization.
74
+ # json.title_loc_args ['arg1', 'arg2']
75
+ end
data/lib/pushing.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "active_support/dependencies/autoload"
2
+ require "pushing/version"
3
+
4
+ module Pushing
5
+ extend ::ActiveSupport::Autoload
6
+
7
+ autoload :Adapters
8
+ autoload :Base
9
+ autoload :DeliveryJob
10
+ autoload :NotificationDelivery
11
+ autoload :Platforms
12
+ end
13
+
14
+ if defined?(Rails)
15
+ require 'pushing/railtie'
16
+ end
@@ -0,0 +1,43 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Pushing
4
+ module Adapters
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :HoustonAdapter, 'pushing/adapters/apn/houston_adapter'
8
+ autoload :ApnoticAdapter, 'pushing/adapters/apn/apnotic_adapter'
9
+ autoload :LowdownAdapter, 'pushing/adapters/apn/lowdown_adapter'
10
+ autoload :AndpushAdapter, 'pushing/adapters/fcm/andpush_adapter'
11
+ autoload :FcmGemAdapter, 'pushing/adapters/fcm/fcm_gem_adapter'
12
+ autoload :TestAdapter
13
+
14
+ # Hash object that holds referenses to adapter instances.
15
+ ADAPTER_INSTANCES = {}
16
+
17
+ # Mutex object used to ensure the +instance+ method creates a singleton object.
18
+ MUTEX = Mutex.new
19
+
20
+ private_constant :ADAPTER_INSTANCES, :MUTEX
21
+
22
+ class << self
23
+ ##
24
+ # Returns the constant for the specified adapter name.
25
+ #
26
+ # Pushing::Adapters.lookup(:apnotic)
27
+ # # => Pushing::Adapters::ApnoticAdapter
28
+ def lookup(name)
29
+ const_get("#{name.to_s.camelize}Adapter")
30
+ end
31
+
32
+ ##
33
+ # Provides an adapter instance specified in the +configuration+. If the adapter is not found in
34
+ # +ADAPTER_INSTANCES+, it'll look up the adapter class and create a new instance using the
35
+ # +configuration+.
36
+ def instance(configuration)
37
+ ADAPTER_INSTANCES[configuration.adapter] || MUTEX.synchronize do
38
+ ADAPTER_INSTANCES[configuration.adapter] ||= lookup(configuration.adapter).new(configuration)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,86 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'apnotic'
4
+
5
+ module Pushing
6
+ module Adapters
7
+ class ApnoticAdapter
8
+ APS_DICTIONARY_KEYS = %i[
9
+ alert
10
+ badge
11
+ sound
12
+ content_available
13
+ category
14
+ url_args
15
+ mutable_content
16
+ ].freeze
17
+
18
+ DEFAULT_ADAPTER_OPTIONS = {
19
+ size: Process.getrlimit(Process::RLIMIT_NOFILE).first / 8
20
+ }.freeze
21
+
22
+ attr_reader :environment, :topic, :connection_pool
23
+
24
+ def initialize(apn_settings)
25
+ @environment = apn_settings.environment.to_sym
26
+ @topic = apn_settings.topic
27
+
28
+ options = {
29
+ cert_path: apn_settings.certificate_path,
30
+ cert_pass: apn_settings.certificate_password
31
+ }
32
+
33
+ @connection_pool = {
34
+ development: Apnotic::ConnectionPool.development(options, DEFAULT_ADAPTER_OPTIONS),
35
+ production: Apnotic::ConnectionPool.new(options, DEFAULT_ADAPTER_OPTIONS),
36
+ }
37
+ end
38
+
39
+ def push!(notification)
40
+ message = Apnotic::Notification.new(notification.device_token)
41
+ json = notification.payload
42
+
43
+ if aps = json.delete(:aps)
44
+ APS_DICTIONARY_KEYS.each {|key| message.instance_variable_set(:"@#{key}", aps[key]) }
45
+ end
46
+
47
+ message.custom_payload = json
48
+
49
+ message.apns_id = notification.headers[:'apns-id'] || message.apns_id
50
+ message.expiration = notification.headers[:'apns-expiration'].to_i
51
+ message.priority = notification.headers[:'apns-priority']
52
+ message.topic = notification.headers[:'apns-topic'] || topic
53
+ message.apns_collapse_id = notification.headers[:'apns-collapse-id']
54
+
55
+ response = connection_pool[notification.environment || environment].with {|connection| connection.push(message) }
56
+
57
+ if !response
58
+ raise "Timeout sending a push notification"
59
+ elsif response.status != '200'
60
+ raise response.body.to_s
61
+ end
62
+
63
+ ApnResponse.new(response)
64
+ rescue => cause
65
+ response = response ? ApnResponse.new(response) : nil
66
+ error = Pushing::ApnDeliveryError.new("Error while trying to send push notification: #{cause.message}", response, notification)
67
+
68
+ raise error, error.message, cause.backtrace
69
+ end
70
+
71
+ class ApnResponse < SimpleDelegator
72
+ def code
73
+ __getobj__.status.to_i
74
+ end
75
+ alias status code
76
+
77
+ def json
78
+ @json ||= __getobj__.body.symbolize_keys if !__getobj__.body.empty?
79
+ end
80
+ alias body json
81
+ end
82
+
83
+ private_constant :ApnResponse
84
+ end
85
+ end
86
+ end