pushr-core 1.0.0.pre.1

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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +156 -0
  4. data/bin/pushr +43 -0
  5. data/lib/generators/templates/feedback_processor.rb +33 -0
  6. data/lib/pushr/configuration.rb +49 -0
  7. data/lib/pushr/core.rb +54 -0
  8. data/lib/pushr/daemon.rb +116 -0
  9. data/lib/pushr/daemon/app.rb +71 -0
  10. data/lib/pushr/daemon/delivery_error.rb +19 -0
  11. data/lib/pushr/daemon/delivery_handler.rb +41 -0
  12. data/lib/pushr/daemon/feedback_handler.rb +38 -0
  13. data/lib/pushr/daemon/logger.rb +57 -0
  14. data/lib/pushr/feedback.rb +37 -0
  15. data/lib/pushr/message.rb +36 -0
  16. data/lib/pushr/redis_connection.rb +38 -0
  17. data/lib/pushr/version.rb +3 -0
  18. data/spec/lib/pushr/configuration_spec.rb +58 -0
  19. data/spec/lib/pushr/daemon/app_spec.rb +55 -0
  20. data/spec/lib/pushr/daemon/delivery_error_spec.rb +15 -0
  21. data/spec/lib/pushr/daemon/delivery_handler_spec.rb +48 -0
  22. data/spec/lib/pushr/daemon/feedback_handler_spec.rb +42 -0
  23. data/spec/lib/pushr/daemon/logger_spec.rb +25 -0
  24. data/spec/lib/pushr/daemon_spec.rb +15 -0
  25. data/spec/lib/pushr/feedback_spec.rb +27 -0
  26. data/spec/lib/pushr/message_spec.rb +33 -0
  27. data/spec/lib/pushr/redis_connection_spec.rb +14 -0
  28. data/spec/spec_helper.rb +30 -0
  29. data/spec/support/logger.rb +11 -0
  30. data/spec/support/pushr_configuration_dummy.rb +22 -0
  31. data/spec/support/pushr_connection_dummy.rb +22 -0
  32. data/spec/support/pushr_dummy.rb +16 -0
  33. data/spec/support/pushr_feedback_dummy.rb +11 -0
  34. data/spec/support/pushr_feedback_processor_dummy.rb +9 -0
  35. data/spec/support/pushr_invalid_configuration_dummy.rb +8 -0
  36. data/spec/support/pushr_message_dummy.rb +13 -0
  37. metadata +281 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7c269b18e78e10769483231241fb24d5244b28d8
4
+ data.tar.gz: fc7fcd4d877e6061ac263fd97530777158b053aa
5
+ SHA512:
6
+ metadata.gz: bee51328463ac62dd6731e8e049e230c7a0ed582ee38bf9065ed8019989c8b43267067512806c596862e44ab547f61e76a78d6de470457866982ea61780e714a
7
+ data.tar.gz: 49e84cf9943ee2293a06fd4ecc84d12a9a00580b5a4a4a21329c097084f40c854ea1249b8b02efa8a4473c88a6e6a09a3934eeee5e5da00ee9e2c3a0051ac72f
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Pushr
2
+
3
+ [![Build Status](https://travis-ci.org/9to5/pushr-core.svg?branch=master)](https://travis-ci.org/9to5/pushr-core)
4
+ [![Code Climate](https://codeclimate.com/github/9to5/pushr-core.png)](https://codeclimate.com/github/9to5/pushr-core)
5
+ [![Coverage Status](https://coveralls.io/repos/9to5/pushr-core/badge.png)](https://coveralls.io/r/9to5/pushr-core)
6
+
7
+ ## Features
8
+
9
+ * Multi-App
10
+ * Multi-Provider ([APNS](https://github.com/tompesman/push-apns), [GCM](https://github.com/tompesman/push-gcm), [C2DM](https://github.com/tompesman/push-c2dm))
11
+ * Integrated feedback processing
12
+ * Rake task to cleanup the database
13
+ * Database for storage (no external dependencies)
14
+
15
+ ## Installation
16
+
17
+ Add to your `GemFile`
18
+
19
+ gem 'push-core'
20
+
21
+ and add the push provider to you Gemfile:
22
+
23
+ For __APNS__ (iOS: Apple Push Notification Services):
24
+
25
+ gem 'push-apns'
26
+
27
+ For __C2DM__ (Android: Cloud to Device Messaging, deprecated by Google, not this gem):
28
+
29
+ gem 'push-c2dm'
30
+
31
+ For __GCM__ (Android: Google Cloud Messaging):
32
+
33
+ gem 'push-gcm'
34
+
35
+ And run `bundle install` to install the gems.
36
+
37
+ To generate the migration and the configuration files run:
38
+
39
+ rails g push
40
+ bundle exec rake db:migrate
41
+
42
+ ## Configuration
43
+
44
+ The configuration is in the database and you add the configuration per push provider with the console (`rails c`):
45
+
46
+ APNS ([see](https://github.com/tompesman/push-core#generating-certificates)):
47
+ ```ruby
48
+ Pushr::ConfigurationApns.create(app: 'app_name', connections: 2, enabled: true,
49
+ certificate: File.read('certificate.pem'),
50
+ feedback_poll: 60,
51
+ sandbox: false)
52
+ ```
53
+
54
+ The `skip_check_for_error` parameter is optional and can be set to `true` or `false`. If set to `true` the APNS service will not check for errors when sending messages. This option should be used in a production environment and improves performance. In production the errors are reported and handled by the feedback service.
55
+
56
+ C2DM ([see](https://developers.google.com/android/c2dm/)):
57
+ ```ruby
58
+ Pushr::ConfigurationC2dm.create(app: 'app_name', connections: 2, enabled: true,
59
+ email: '<email address here>',
60
+ password: '<password here>')
61
+ ```
62
+
63
+ GCM ([see](http://developer.android.com/guide/google/gcm/gs.html)):
64
+ ```ruby
65
+ Pushr::ConfigurationGcm.create(app: 'app_name', connections: 2, enabled: true,
66
+ key: '<api key here>')
67
+ ```
68
+
69
+ You can have each provider per app_name and you can have more than one app_name. Use the instructions below to generate the certificate for the APNS provider. If you only want to prepare the database with the configurations, you can set the `enabled` switch to `false`. Only enabled configurations will be used by the daemon.
70
+
71
+ ### Generating Certificates for APNS
72
+
73
+ 1. Open up Keychain Access and select the `Certificates` category in the sidebar.
74
+ 2. Expand the disclosure arrow next to the iOS Push Services certificate you want to export.
75
+ 3. Select both the certificate and private key.
76
+ 4. Right click and select `Export 2 items...`.
77
+ 5. Save the file as `cert.p12`, make sure the File Format is `Personal Information Exchange (p12)`.
78
+ 6. If you decide to set a password for your exported certificate, please read the Configuration section below.
79
+ 7. Convert the certificate to a .pem, where `<environment>` should be `development` or `production`, depending on the certificate you exported.
80
+
81
+ `openssl pkcs12 -nodes -clcerts -in cert.p12 -out <environment>.pem`
82
+
83
+ 8. Move the .pem file somewhere where you can use the `File.read` to load the file in the database.
84
+
85
+ ## Daemon
86
+
87
+ To start the daemon:
88
+
89
+ bundle exec push <environment> <options>
90
+
91
+ Where `<environment>` is your Rails environment and `<options>` can be:
92
+
93
+ -f, --foreground Run in the foreground. Log is not written.
94
+ -p, --pid-file PATH Path to write PID file. Relative to Rails root unless absolute.
95
+ -P, --push-poll N Frequency in seconds to check for new notifications. Default: 2.
96
+ -n, --error-notification Enables error notifications via Airbrake or Bugsnag.
97
+ -F, --feedback-poll N Frequency in seconds to check for feedback for the feedback processor. Default: 60. Use 0 to disable.
98
+ -b, --feedback-processor PATH Path to the feedback processor. Default: lib/push/feedback_processor.
99
+ -v, --version Print this version of push.
100
+ -h, --help You're looking at it.
101
+
102
+
103
+ ## Sending notifications
104
+ APNS:
105
+ ```ruby
106
+ Pushr::MessageApns.create(
107
+ app: 'app_name',
108
+ device: '<APNS device_token here>',
109
+ alert: 'Hello World',
110
+ sound: '1.aiff',
111
+ badge: 1,
112
+ expiry: 1.day.to_i,
113
+ attributes_for_device: {key: 'MSG'})
114
+ ```
115
+ C2DM:
116
+ ```ruby
117
+ Pushr::MessageC2dm.create(
118
+ app: 'app_name',
119
+ device: '<C2DM registration_id here>',
120
+ payload: { message: 'Hello World' },
121
+ collapse_key: 'MSG')
122
+ ```
123
+
124
+ GCM:
125
+ ```ruby
126
+ Pushr::MessageGcm.create(
127
+ app: 'app_name',
128
+ device: '<GCM registration_id here>',
129
+ payload: { message: 'Hello World' },
130
+ collapse_key: 'MSG')
131
+ ```
132
+
133
+ ## Feedback processing
134
+
135
+ The push providers return feedback in various ways and these are captured and stored in the `push_feedback` table. The installer installs the `lib/push/feedback_processor.rb` file which is by default called every 60 seconds. In this file you can process the feedback which is different for every application.
136
+
137
+ ## Maintenance
138
+
139
+ The push-core comes with a rake task to delete all the messages and feedback of the last 7 days or by the DAYS parameter.
140
+
141
+ bundle exec rake push:clean DAYS=2
142
+
143
+ ## Heroku
144
+
145
+ Push runs on Heroku with the following line in the `Procfile`.
146
+
147
+ push: bundle exec push $RACK_ENV -f
148
+
149
+ ## Prerequisites
150
+
151
+ * Rails 3.2.x
152
+ * Ruby 1.9.x
153
+
154
+ ## Thanks
155
+
156
+ This project started as a fork of Ian Leitch [RAPNS](https://github.com/ileitch/rapns) project. The differences between this project and RAPNS is the support for C2DM and the modularity of the push providers.
data/bin/pushr ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ Bundler.require
5
+
6
+ require 'optparse'
7
+ require 'pushr/core'
8
+ require 'pushr/daemon'
9
+
10
+ config = Struct.new(:foreground, :pid_file, :error_notification, :feedback_processor, :stats_processor).new
11
+ config.foreground = false
12
+ config.error_notification = false
13
+ config.feedback_processor = nil
14
+ config.stats_processor = nil
15
+
16
+ banner = 'Usage: pushr [options]'
17
+ ARGV.options do |opts|
18
+ opts.banner = banner
19
+ opts.on('-f', '--foreground', 'Run in the foreground. Log is not written.') { config.foreground = true }
20
+ opts.on('-p PATH', '--pid-file PATH', String, 'Path to write PID file. Relative to Rails root unless absolute.') { |path| config.pid_file = path }
21
+ opts.on('-b PATH', '--feedback-processor PATH', String, "Path to the feedback processor. Default: none. Example: 'lib/pushr/feedback_processor'") { |n| config.feedback_processor = n }
22
+ opts.on('-s PATH', '--stats-processor PATH', String, "Path to the stats processor. Default: none. Example: 'lib/pushr/stats_processor'") { |n| config.stats_processor = n }
23
+ opts.on('-v', '--version', 'Print this version of pushr.') { puts "pushr #{Pushr::VERSION}"; exit }
24
+ opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
25
+ opts.parse!
26
+ end
27
+
28
+ if config.pid_file && !Pathname.new(config.pid_file).absolute?
29
+ config.pid_file = File.join(Dir.pwd.root, config.pid_file)
30
+ end
31
+
32
+ if ENV['AIRBRAKE_API_KEY']
33
+ require 'airbrake'
34
+ config.error_notification = true
35
+ Airbrake.configure do |config|
36
+ config.api_key = ENV['AIRBRAKE_API_KEY']
37
+ config.host = ENV['AIRBRAKE_HOST']
38
+ config.port = ENV['AIRBRAKE_PORT'] ? ENV['AIRBRAKE_PORT'].to_i : 80
39
+ config.secure = config.port == 443
40
+ end
41
+ end
42
+
43
+ Pushr::Daemon.start(config)
@@ -0,0 +1,33 @@
1
+ module Pushr
2
+ class FeedbackProcessor
3
+ def initialize
4
+ # make sure you've set the RAILS_ENV variable
5
+ load 'config/environment.rb'
6
+ end
7
+
8
+ def process(feedback)
9
+ if feedback.instance_of? Pushr::FeedbackGcm
10
+ if feedback.follow_up == 'delete'
11
+ # TODO: delete gcm device
12
+ Pushr::Daemon.logger.info('[FeedbackProcessor] Pushr::FeedbackGcm delete')
13
+ elsif feedback.follow_up == 'update'
14
+ # TODO: update gcm device
15
+ # device = feedback.update_to
16
+ Pushr::Daemon.logger.info('[FeedbackProcessor] Pushr::FeedbackGcm update')
17
+ end
18
+ elsif feedback.instance_of? Pushr::FeedbackC2dm
19
+ if feedback.follow_up == 'delete'
20
+ # TODO: delete c2dm device
21
+ Pushr::Daemon.logger.info('[FeedbackProcessor] Pushr::FeedbackC2dm delete')
22
+ end
23
+ elsif feedback.instance_of? Pushr::FeedbackApns
24
+ if feedback.follow_up == 'delete'
25
+ # TODO: delete apns device
26
+ Pushr::Daemon.logger.info('[FeedbackProcessor] Pushr::FeedbackApns delete')
27
+ end
28
+ else
29
+ Pushr::Daemon.logger.info('[FeedbackProcessor] Unknown feedback type')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ module Pushr
2
+ class Configuration
3
+ include ActiveModel::Validations
4
+
5
+ validates :app, presence: true
6
+ validates :connections, presence: true
7
+ validates :connections, numericality: { greater_than: 0, only_integer: true }
8
+
9
+ def initialize(attributes = {})
10
+ attributes.each do |name, value|
11
+ send("#{name}=", value)
12
+ end
13
+ end
14
+
15
+ def key
16
+ "#{app}:#{name}"
17
+ end
18
+
19
+ def save
20
+ if valid?
21
+ Pushr::Core.redis { |conn| conn.hset('pushr:configurations', key, to_json) }
22
+ return true
23
+ else
24
+ return false
25
+ end
26
+ end
27
+
28
+ def delete
29
+ Pushr::Core.redis { |conn| conn.hdel('pushr:configurations', key) }
30
+ end
31
+
32
+ def self.all
33
+ configurations = Pushr::Core.redis { |conn| conn.hgetall('pushr:configurations') }
34
+ configurations.each { |key, config| configurations[key] = instantiate(config, key) }
35
+ configurations.values
36
+ end
37
+
38
+ def self.find(key)
39
+ config = Pushr::Core.redis { |conn| conn.hget('pushr:configurations', key) }
40
+ instantiate(config, key)
41
+ end
42
+
43
+ def self.instantiate(config, id)
44
+ hsh = ::MultiJson.load(config).merge!(id: id)
45
+ klass = hsh['type'].split('::').reduce(Object) { |a, e| a.const_get e }
46
+ klass.new(hsh)
47
+ end
48
+ end
49
+ end
data/lib/pushr/core.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'active_model'
2
+ require 'multi_json'
3
+ require 'pushr/version'
4
+ require 'pushr/configuration'
5
+ require 'pushr/message'
6
+ require 'pushr/feedback'
7
+ require 'pushr/redis_connection'
8
+
9
+ module Pushr
10
+ module Core
11
+ NAME = 'Pushr'
12
+ DEFAULTS = {}
13
+
14
+ attr_writer :options
15
+
16
+ def self.options
17
+ @options ||= DEFAULTS.dup
18
+ end
19
+
20
+ ##
21
+ # Configuration for Pushr, use like:
22
+ #
23
+ # Pushr.configure do |config|
24
+ # config.redis = { :namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/mydb' }
25
+ # end
26
+ def self.configure
27
+ yield self
28
+ end
29
+
30
+ def self.redis(&block)
31
+ fail ArgumentError, 'requires a block' unless block
32
+ @redis ||= Pushr::RedisConnection.create
33
+ @redis.with(&block)
34
+ end
35
+
36
+ def self.redis=(hash)
37
+ if hash.is_a?(Hash)
38
+ @redis = RedisConnection.create(hash)
39
+ options[:namespace] ||= hash[:namespace]
40
+ elsif hash.is_a?(ConnectionPool)
41
+ @redis = hash
42
+ else
43
+ fail ArgumentError, 'redis= requires a Hash or ConnectionPool'
44
+ end
45
+ end
46
+
47
+ # instruments with a block
48
+ def self.instrument(name, payload = {}, &block)
49
+ ActiveSupport::Notifications.instrument(name, payload) do
50
+ yield
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,116 @@
1
+ require 'thread'
2
+ require 'logger'
3
+ require 'multi_json'
4
+ require 'pushr/redis_connection'
5
+ require 'pushr/daemon/delivery_error'
6
+ require 'pushr/daemon/delivery_handler'
7
+ require 'pushr/daemon/feedback_handler'
8
+ require 'pushr/daemon/logger'
9
+ require 'pushr/daemon/app'
10
+
11
+ module Pushr
12
+ module Daemon
13
+ class << self
14
+ attr_accessor :logger, :config, :feedback_handler
15
+ end
16
+
17
+ def self.start(config)
18
+ self.config = config
19
+ self.logger = Logger.new(foreground: config.foreground, error_notification: config.error_notification)
20
+ setup_signal_hooks
21
+
22
+ daemonize unless config.foreground
23
+ write_pid_file
24
+
25
+ load_stats_processor
26
+
27
+ App.load
28
+ scale_redis_connections
29
+ App.start
30
+ self.feedback_handler = FeedbackHandler.new(config.feedback_processor)
31
+ feedback_handler.start
32
+
33
+ logger.info('[Daemon] Ready')
34
+ while !@shutting_down
35
+ sleep 1
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ def self.scale_redis_connections
42
+ # feedback handler + app + app.totalconnections
43
+ connections = 1 + 1 + App.total_connections
44
+ Pushr::Core.configure do |config|
45
+ config.redis = { size: connections }
46
+ end
47
+ end
48
+
49
+ def self.load_stats_processor
50
+ if config.stats_processor
51
+ require "#{Dir.pwd}/#{config.stats_processor}"
52
+ end
53
+ rescue => e
54
+ logger.error("Failed to stats_processor: #{e.inspect}")
55
+ end
56
+
57
+ def self.setup_signal_hooks
58
+ @shutting_down = false
59
+
60
+ %w(SIGINT SIGTERM).each do |signal|
61
+ Signal.trap(signal) do
62
+ handle_shutdown_signal
63
+ end
64
+ end
65
+ end
66
+
67
+ def self.handle_shutdown_signal
68
+ exit 1 if @shutting_down
69
+ @shutting_down = true
70
+ shutdown
71
+ end
72
+
73
+ def self.shutdown
74
+ print "\nShutting down..."
75
+ feedback_handler.stop
76
+ App.stop
77
+
78
+ while Thread.list.count > 1
79
+ sleep 0.1
80
+ print '.'
81
+ end
82
+ print "\n"
83
+ delete_pid_file
84
+ end
85
+
86
+ def self.daemonize
87
+ exit if pid = fork
88
+ Process.setsid
89
+ exit if pid = fork
90
+
91
+ Dir.chdir '/'
92
+ File.umask 0000
93
+
94
+ STDIN.reopen '/dev/null'
95
+ STDOUT.reopen '/dev/null', 'a'
96
+ STDERR.reopen STDOUT
97
+ end
98
+
99
+ def self.write_pid_file
100
+ unless config[:pid_file].blank?
101
+ begin
102
+ File.open(config[:pid_file], 'w') do |f|
103
+ f.puts $PROCESS_ID
104
+ end
105
+ rescue SystemCallError => e
106
+ logger.error("Failed to write PID to '#{config[:pid_file]}': #{e.inspect}")
107
+ end
108
+ end
109
+ end
110
+
111
+ def self.delete_pid_file
112
+ pid_file = config[:pid_file]
113
+ File.delete(pid_file) if !pid_file.blank? && File.exist?(pid_file)
114
+ end
115
+ end
116
+ end