pushr-core 1.0.0.pre.1

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