resque-aps 0.9.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.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.pem
data/HISTORY.md ADDED
@@ -0,0 +1,4 @@
1
+ ## 0.9.0 (2010-07-05)
2
+
3
+ * Initial code
4
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Ashley Martens
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.
21
+
data/README ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ load 'tasks/resque_aps.rake'
2
+
3
+ $LOAD_PATH.unshift 'lib'
4
+
5
+ task :default => :test
6
+
7
+ desc "Run tests"
8
+ task :test do
9
+ Dir['test/*_test.rb'].each do |f|
10
+ require f
11
+ end
12
+ end
13
+
14
+
15
+ desc "Build a gem"
16
+ task :gem => [ :test, :gemspec, :build ]
17
+
18
+ begin
19
+ begin
20
+ require 'jeweler'
21
+ rescue LoadError
22
+ puts "Jeweler not available. Install it with: "
23
+ puts "gem install jeweler"
24
+ end
25
+
26
+ require 'resque_aps/version'
27
+
28
+ Jeweler::Tasks.new do |gemspec|
29
+ gemspec.name = "resque-aps"
30
+ gemspec.summary = "Queuing system for Apple's Push Service on top of Resque"
31
+ gemspec.description = %{Queuing system for Apple's Push Service on top of Resque.
32
+ Adds methods enqueue_aps to queue a notification message.
33
+ Also includes helper classes and methods to format JSON.}
34
+ gemspec.email = "ashleym1972@gmail.com"
35
+ gemspec.homepage = "http://github.com/ashleym1972/resque-aps"
36
+ gemspec.authors = ["Ashley Martens"]
37
+ gemspec.version = ResqueAps::Version
38
+
39
+ gemspec.add_dependency "redis", ">= 2.0.1"
40
+ gemspec.add_dependency "resque", ">= 1.8.0"
41
+ gemspec.add_development_dependency "jeweler"
42
+ gemspec.add_development_dependency "mocha"
43
+ gemspec.add_development_dependency "rack-test"
44
+ end
45
+ end
46
+
47
+
48
+ desc "Push a new version to Gemcutter"
49
+ task :publish => [ :test, :gemspec, :build ] do
50
+ system "git tag v#{ResqueAps::Version}"
51
+ system "git push origin v#{ResqueAps::Version}"
52
+ system "git push origin master"
53
+ system "gem push pkg/resque-aps-#{ResqueAps::Version}.gem"
54
+ system "git clean -fd"
55
+ end
@@ -0,0 +1,142 @@
1
+ require 'openssl'
2
+
3
+ module ResqueAps
4
+ class Application
5
+ include ResqueAps::Helper
6
+ extend ResqueAps::Helper
7
+
8
+ attr_accessor :name, :cert_file, :cert_passwd
9
+
10
+ @queue = "apple_push_service"
11
+
12
+ def inspect
13
+ "#<#{self.class.name} #{name.inspect}, #{cert_passwd.inspect}, #{cert_file.inspect}>"
14
+ end
15
+
16
+ def self.perform(*args)
17
+ count = 0
18
+ start = Time.now
19
+ app_name = args[0]
20
+ Resque.aps_application(app_name).socket do |socket, app|
21
+ while true
22
+ n = Resque.dequeue_aps(app_name)
23
+ if n.nil?
24
+ if app.aps_nil_notification_retry? count, start
25
+ next
26
+ else
27
+ break
28
+ end
29
+ end
30
+
31
+ app.before_aps_write n
32
+ begin
33
+ socket.write(n.formatted)
34
+ app.after_aps_write n
35
+ rescue
36
+ logger.error application_exception($!) if logger
37
+ app.failed_aps_write n
38
+ end
39
+ count += 1
40
+ end
41
+ end
42
+ logger.info("Sent #{count} #{app_name} notifications in batch over #{Time.now - start} sec.") if logger
43
+ end
44
+
45
+ #
46
+ # Create the TCP and SSL sockets for sending the notification
47
+ #
48
+ def self.create_sockets(cert_file, passphrase, host, port)
49
+ cert = File.read(cert_file)
50
+
51
+ ctx = OpenSSL::SSL::SSLContext.new
52
+ ctx.key = OpenSSL::PKey::RSA.new(cert, passphrase)
53
+ ctx.cert = OpenSSL::X509::Certificate.new(cert)
54
+
55
+ s = TCPSocket.new(host, port)
56
+ ssl = OpenSSL::SSL::SSLSocket.new(s, ctx)
57
+ ssl.sync = true
58
+
59
+ return s, ssl
60
+ end
61
+
62
+ #
63
+ # Close the sockets
64
+ #
65
+ def self.close_sockets(socket, ssl_socket)
66
+ begin
67
+ if ssl_socket
68
+ ssl_socket.close
69
+ end
70
+ rescue
71
+ Resque.logger.error("#{$!}: #{$!.backtrace.join("\n")}") if Resque.logger
72
+ end
73
+
74
+ begin
75
+ if socket
76
+ socket.close
77
+ end
78
+ rescue
79
+ Resque.logger.error("#{$!}: #{$!.backtrace.join("\n")}") if Resque.logger
80
+ end
81
+ end
82
+
83
+ def initialize(attributes)
84
+ attributes.each do |k, v|
85
+ respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(ResqueAps::UnknownAttributeError, "unknown attribute: #{k}")
86
+ end
87
+ end
88
+
89
+ def socket(&block)
90
+ logger.debug("resque-aps: ssl_socket(#{name})") if logger
91
+ exc = nil
92
+
93
+ socket, ssl_socket = Application.create_sockets(cert_file, cert_passwd, Resque.aps_gateway_host, Resque.aps_gateway_port)
94
+
95
+ begin
96
+ ssl_socket.connect
97
+ yield ssl_socket, self if block_given?
98
+ rescue
99
+ exc = application_exception($!)
100
+ if $! =~ /^SSL_connect .* certificate (expired|revoked)/
101
+ notify_aps_admin exc
102
+ end
103
+ raise exc
104
+ ensure
105
+ Application.close_sockets(socket, ssl_socket)
106
+ end
107
+
108
+ exc
109
+ end
110
+
111
+ def application_exception(exception)
112
+ exc = Exception.new("#{exception} (#{name})")
113
+ exc.set_backtrace(exception.backtrace)
114
+ return exc
115
+ end
116
+
117
+ def to_hash
118
+ {'name' => name, 'cert_file' => cert_file, 'cert_passwd' => cert_passwd}
119
+ end
120
+
121
+ def to_json
122
+ to_hash.to_json
123
+ end
124
+
125
+ def before_aps_write(notification)
126
+ end
127
+
128
+ def after_aps_write(notification)
129
+ end
130
+
131
+ def failed_aps_write(notification)
132
+ end
133
+
134
+ def notify_aps_admin(exception)
135
+ end
136
+
137
+ def aps_nil_notification_retry?(sent_count, start_time)
138
+ false
139
+ end
140
+
141
+ end
142
+ end
@@ -0,0 +1,11 @@
1
+
2
+ module ResqueAps
3
+ module Helper
4
+ extend Resque::Helpers
5
+
6
+ def logger
7
+ Resque.logger
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,193 @@
1
+ module ResqueAps
2
+ class Notification
3
+ include ResqueAps::Helper
4
+ extend ResqueAps::Helper
5
+
6
+ attr_accessor :application_name, :device_token, :payload
7
+
8
+ def initialize(attributes)
9
+ attributes.each do |k, v|
10
+ respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(ResqueAps::UnknownAttributeError, "unknown attribute: #{k}")
11
+ end
12
+ end
13
+
14
+ def inspect
15
+ "#<#{self.class.name} #{application_name.inspect}, #{device_token.inspect}, #{payload.inspect}>"
16
+ end
17
+
18
+ def to_s
19
+ "#{device_token.inspect}, #{payload.inspect}"
20
+ end
21
+
22
+ def to_hash
23
+ {:application_name => application_name, :device_token => device_token, :payload => payload}
24
+ end
25
+
26
+ # SSL Configuration
27
+ # open Keychain Access, and export the "Apple Development Push" certificate associated with your app in p12 format
28
+ # Convert the certificate to PEM using openssl:
29
+ # openssl pkcs12 -in mycert.p12 -out client-cert.pem -nodes -clcerts
30
+
31
+ # To use with cross application push
32
+ # Notification.new(:user => user, :current_product => product, :cross_app => { :capabilities => 'cross_app', :payload => '{aps.....}' })
33
+
34
+ #
35
+ # https://developer.apple.com/iphone/prerelease/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1
36
+ #
37
+ # Table 2-1 Keys and values of the aps dictionary
38
+ # alert | string or dictionary
39
+ # => If this property is included, iPhone OS displays a standard alert. You may specify a string as the value of alert or a dictionary as its value.
40
+ # => If you specify a string, it becomes the message text of an alert with two buttons: Close and View. If the user taps View, the application is launched.
41
+ # => Alternatively, you can specify a dictionary as the value of alert. See Table 2-2 for descriptions of the keys of this dictionary.
42
+ #
43
+ # badge | number
44
+ # => The number to display as the badge of the application icon. If this property is absent, any badge number currently shown is removed.
45
+ #
46
+ # sound | string
47
+ # => The name of a sound file in the application bundle. The sound in this file is played as an alert.
48
+ # => If the sound file doesn’t exist or default is specified as the value, the default alert sound is played.
49
+ # => The audio must be in one of the audio data formats that are compatible with system sounds; see “Preparing Custom Alert Sounds” for details.
50
+ #
51
+ # Table 2-2 Child properties of the alert property
52
+ # body | string
53
+ # => The text of the alert message.
54
+ #
55
+ # action-loc-key | string or null
56
+ # => If a string is specified, displays an alert with two buttons, whose behavior is described in Table 2-1.
57
+ # => However, iPhone OS uses the string as a key to get a localized string in the current localization to use for the right button’s title instead of “View”.
58
+ # => If the value is null, the system displays an alert with a single OK button that simply dismisses the alert when tapped.
59
+ #
60
+ # loc-key | string
61
+ # => A key to an alert-message string in a Localizable.strings file for the current localization (which is set by the user’s language preference).
62
+ # => The key string can be formatted with %@ and %n$@ specifiers to take the variables specified in loc-args. See “Localized Formatted Strings” for more information.
63
+ #
64
+ # loc-args | array of strings
65
+ # => Variable string values to appear in place of the format specifiers in loc-key. See “Localized Formatted Strings” for more information.
66
+ #
67
+ #
68
+ # Example Result:
69
+ # {
70
+ # "aps" : {
71
+ # "alert" : "You got your emails.",
72
+ # "badge" : 9,
73
+ # "sound" : "bingbong.aiff"
74
+ # },
75
+ # "acme1" : "bar",
76
+ # "acme2" : 42
77
+ #}
78
+ # Or
79
+ # {
80
+ # "aps" : {
81
+ # "alert" : {
82
+ # "action-loc-key" : "PLAY",
83
+ # "loc-key" : "SERVER.ERROR"
84
+ # "loc-args" : ["bob", "sierra"]
85
+ # },
86
+ # "badge" : 9,
87
+ # "sound" : "bingbong.aiff"
88
+ # },
89
+ # "acme1" : "bar",
90
+ # "acme2" : 42
91
+ #}
92
+ def self.to_payload(alert = nil, badge = nil, sound = nil, app_data = nil)
93
+ result = ActiveSupport::OrderedHash.new
94
+ result['aps'] = ActiveSupport::OrderedHash.new
95
+ result['aps']['alert'] = self.format_alert(alert) unless alert.blank?
96
+ result['aps']['badge'] = badge.to_i unless badge.blank?
97
+ result['aps']['sound'] = sound unless sound.blank?
98
+ result.merge!(app_data) unless app_data.blank?
99
+ self.to_json(result)
100
+ end
101
+
102
+ #
103
+ # Create an ordered hash of the data in the given alert hash
104
+ #
105
+ def self.format_alert(alert)
106
+ if alert.is_a? Hash
107
+ result = ActiveSupport::OrderedHash.new
108
+ result['action-loc-key'] = alert['action-loc-key'] unless alert['action-loc-key'].blank?
109
+ result['loc-key'] = alert['loc-key'] unless alert['loc-key'].blank?
110
+ unless alert['loc-args'].blank?
111
+ if alert['loc-args'].is_a? Hash
112
+ result['loc-args'] = Array.new(alert['loc-args'].size)
113
+ alert['loc-args'].map do |key,value|
114
+ result['loc-args'][key.to_i] = value
115
+ end
116
+ else
117
+ result['loc-args'] = alert['loc-args']
118
+ end
119
+ end
120
+ return result
121
+ else
122
+ return alert
123
+ end
124
+ end
125
+
126
+ #
127
+ # Generate a JSON string from the given Hash/Array which does not screw up the ordering of the Hash
128
+ #
129
+ def self.to_json(hash)
130
+ if hash.is_a? Hash
131
+ hash_keys = hash.keys
132
+
133
+ result = '{'
134
+ result << hash_keys.map do |key|
135
+ if hash[key].is_a?(Hash) || hash[key].is_a?(Array)
136
+ "#{key.to_s.to_json}:#{to_json(hash[key])}"
137
+ else
138
+ "#{key.to_s.to_json}:#{hash[key].to_json}"
139
+ end
140
+ end * ','
141
+ result << '}'
142
+ elsif hash.is_a? Array
143
+ result = '['
144
+ result << hash.map do |value|
145
+ if value.is_a?(Hash) || value.is_a?(Array)
146
+ "#{to_json(value)}"
147
+ else
148
+ value.to_json
149
+ end
150
+ end * ','
151
+ result << ']'
152
+ end
153
+ end
154
+
155
+ #
156
+ # The message formatted for sending in binary
157
+ #
158
+ def formatted
159
+ ResqueAps::Notification.format_message_for_sending(self.device_token, self.payload)
160
+ end
161
+
162
+ #
163
+ # A HEX dump of the formatted message so that you can debug the binary data
164
+ #
165
+ def to_hex
166
+ formatted.unpack('H*')
167
+ end
168
+
169
+ #
170
+ # HEX version of the device token
171
+ #
172
+ def self.device_token_hex(device_token)
173
+ #self.device_token
174
+ [device_token.gsub(' ', '')].pack('H*')
175
+ end
176
+
177
+ #
178
+ # Combine the device token and JSON into a binary package to send.
179
+ #
180
+ def self.format_message_for_sending(device_token, json)
181
+ token_hex = self.device_token_hex(device_token)
182
+ tl = [token_hex.length].pack('n')
183
+ # puts("token length [#{tl.unpack('H*')}]")
184
+ # puts("device token [#{token_hex.unpack('H*')}]")
185
+ # logger.debug "Formatting #{json} for #{self.device_token}"
186
+ jl = [json.length].pack('n')
187
+ # puts("json length [#{jl.unpack('H*')}]")
188
+ # puts("json [#{json}]")
189
+ "\0#{tl}#{token_hex}#{jl}#{json}"
190
+ # "\0\0 #{token_hex}\0#{json.length.chr}#{json}"
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,25 @@
1
+ <h1>Apple Push Notification Applications</h1>
2
+
3
+ <p class='intro'>
4
+ The list below contains the APN application queues.
5
+ </p>
6
+
7
+ <p class='sub'>
8
+ Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%=size = resque.aps_applications_count %></b> applications.
9
+ </p>
10
+
11
+ <table>
12
+ <tr>
13
+ <th></th>
14
+ <th>Application</th>
15
+ <th>Notification count</th>
16
+ </tr>
17
+ <% resque.aps_application_names(start, start+20).each do |application_name| %>
18
+ <tr>
19
+ <td><a href="<%= url "aps_applicatons/#{application_name}" %>"><%= application_name %></a></td>
20
+ <td><%= resque.aps_notification_count_for_application(application_name) %></td>
21
+ </tr>
22
+ <% end %>
23
+ </table>
24
+
25
+ <%= partial :next_more, :start => start, :size => size %>
@@ -0,0 +1,24 @@
1
+ <% application_name = params[:application_name] %>
2
+
3
+ <h1>Apple Push Service Notifications scheduled for <%= application_name %></h1>
4
+
5
+ <p class='sub'>Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%=size = resque.aps_notification_count_for_application(application_name)%></b> notifications</p>
6
+
7
+ <table class='jobs'>
8
+ <tr>
9
+ <th>Payload</th>
10
+ </tr>
11
+ <% notifications = resque.aps_notifications_for_application(application_name, start, 20) %>
12
+ <% notifications.each do |notification| %>
13
+ <tr>
14
+ <td class='args'><%=h notification.to_s %></td>
15
+ </tr>
16
+ <% end %>
17
+ <% if notifications.empty? %>
18
+ <tr>
19
+ <td class='no-data' colspan='2'>There are no pending notifications scheduled for this application.</td>
20
+ </tr>
21
+ <% end %>
22
+ </table>
23
+
24
+ <%= partial :next_more, :start => start, :size => size %>
@@ -0,0 +1,29 @@
1
+
2
+ # Extend Resque::Server to add tabs
3
+ module ResqueAps
4
+
5
+ module Server
6
+
7
+ def self.included(base)
8
+
9
+ base.class_eval do
10
+
11
+ get "/aps" do
12
+ # Is there a better way to specify alternate template locations with sinatra?
13
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/aps_applications.erb'))
14
+ end
15
+
16
+ get "/aps/:application_name" do
17
+ # Is there a better way to specify alternate template locations with sinatra?
18
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/notifications.erb'))
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ Resque::Server.tabs << 'APS'
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,14 @@
1
+ # require 'resque/tasks'
2
+ # will give you the resque tasks
3
+
4
+ namespace :resque do
5
+ task :setup
6
+
7
+ desc "Queue an APN in Resque"
8
+ task :aps => :setup do
9
+ require 'resque'
10
+ require 'resque_aps'
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,4 @@
1
+ module ResqueAps
2
+ class UnknownAttributeError < Exception
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module ResqueAps
2
+ Version = '0.9.0'
3
+ end
data/lib/resque_aps.rb ADDED
@@ -0,0 +1,145 @@
1
+ require 'rubygems'
2
+ require 'resque'
3
+ require 'logger'
4
+ require 'resque/server'
5
+ require 'resque_aps/helper'
6
+ require 'resque_aps/version'
7
+ require 'resque_aps/server'
8
+ require 'resque_aps/application'
9
+ require 'resque_aps/notification'
10
+
11
+ module ResqueAps
12
+
13
+ def logger=(logger)
14
+ @logger = logger
15
+ end
16
+
17
+ def logger
18
+ unless @logger
19
+ @logger = Logger.new(STDOUT)
20
+ @logger.level = Logger::WARN
21
+ end
22
+ @logger
23
+ end
24
+
25
+ def aps_gateway_host=(host)
26
+ @aps_gateway_host = host
27
+ end
28
+
29
+ def aps_gateway_host
30
+ @aps_gateway_host ||= "gateway.sandbox.push.apple.com"
31
+ end
32
+
33
+ def aps_gateway_port=(port)
34
+ @aps_gateway_port = port
35
+ end
36
+
37
+ def aps_gateway_port
38
+ @aps_gateway_port ||= 2195
39
+ end
40
+
41
+ def aps_feedback_host=(host)
42
+ @aps_feedback_host = host
43
+ end
44
+
45
+ def aps_feedback_host
46
+ @aps_feedback_host ||= "feedback.sandbox.push.apple.com"
47
+ end
48
+
49
+ def aps_feedback_port=(port)
50
+ @aps_feedback_port = port
51
+ end
52
+
53
+ def aps_feedback_port
54
+ @aps_feedback_port ||= 2196
55
+ end
56
+
57
+ def aps_queue_size_lower=(size)
58
+ @aps_queue_size_lower = size
59
+ end
60
+
61
+ def aps_queue_size_lower
62
+ @aps_queue_size_lower ||= 0
63
+ end
64
+
65
+ def aps_queue_size_upper=(size)
66
+ @aps_queue_size_upper = size
67
+ end
68
+
69
+ def aps_queue_size_upper
70
+ @aps_queue_size_upper ||= 500
71
+ end
72
+
73
+ def enqueue_aps(application_name, notification)
74
+ count = aps_notification_count_for_application(application_name)
75
+ push(aps_application_queue_key(application_name), notification.to_hash)
76
+ enqueue(ResqueAps::Application, application_name) if count <= aps_queue_size_lower || count >= aps_queue_size_upper
77
+ true
78
+ end
79
+
80
+ def dequeue_aps(application_name)
81
+ h = pop(aps_application_queue_key(application_name))
82
+ return ResqueAps::Notification.new(h) if h
83
+ nil
84
+ end
85
+
86
+ # Returns the number of queued notifications for a given application
87
+ def aps_notification_count_for_application(application_name)
88
+ size(aps_application_queue_key(application_name))
89
+ end
90
+
91
+ # Returns an array of queued notifications for the given application
92
+ def aps_notifications_for_application(application_name, start = 0, count = 1)
93
+ r = peek(aps_application_queue_key(application_name), start, count)
94
+ if r
95
+ r.map { |h| ResqueAps::Notification.new(h) }
96
+ else
97
+ []
98
+ end
99
+ end
100
+
101
+ def aps_application_class=(klass)
102
+ @aps_application_class = klass
103
+ end
104
+
105
+ def aps_application_class
106
+ @aps_application_class ||= ResqueAps::Application
107
+ end
108
+
109
+ def create_aps_application(name, cert_file, cert_passwd = nil)
110
+ redis.set(aps_application_key(name), encode({'name' => name, 'cert_file' => cert_file, 'cert_passwd' => cert_passwd}))
111
+ redis.sadd(:aps_applications, name)
112
+ end
113
+
114
+ def aps_application(name)
115
+ h = decode(redis.get(aps_application_key(name)))
116
+ return aps_application_class.new(h) if h
117
+ nil
118
+ end
119
+
120
+ # Returns an array of applications based on start and count
121
+ def aps_application_names(start = 0, count = 1)
122
+ a = redis.smembers(:aps_applications)
123
+ ret = a[start..(start + count)]
124
+ return [] unless ret
125
+ ret
126
+ end
127
+
128
+ # Returns the number of application queues
129
+ def aps_applications_count
130
+ redis.smembers(:aps_applications).size
131
+ end
132
+
133
+ def aps_application_key(application_name)
134
+ "aps:application:#{application_name}"
135
+ end
136
+
137
+ def aps_application_queue_key(application_name)
138
+ "#{aps_application_key(application_name)}:queue"
139
+ end
140
+ end
141
+
142
+ Resque.extend ResqueAps
143
+ Resque::Server.class_eval do
144
+ include ResqueAps::Server
145
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'resque_aps/tasks'
@@ -0,0 +1,59 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ context "ResqueAps::Application" do
4
+ test "has a nice #inspect" do
5
+ n = ResqueAps::Application.new('name' => 'SomeApp', 'cert_file' => '/var/apps/certificates/some_app.pem', 'cert_passwd' => 'hello')
6
+ assert_equal '#<ResqueAps::Application "SomeApp", "hello", "/var/apps/certificates/some_app.pem">', n.inspect
7
+ end
8
+
9
+ test "can create and close sockets" do
10
+ socket, ssl = ResqueAps::Application.create_sockets(File.dirname(__FILE__) + "/../test-dev.pem", nil, Resque.aps_gateway_host, Resque.aps_gateway_port)
11
+ ResqueAps::Application.close_sockets(socket, ssl)
12
+ end
13
+
14
+ test "can run socket block" do
15
+ a = ResqueAps::Application.new(:name => "TestApp", :cert_file => File.dirname(__FILE__) + "/../test-dev.pem")
16
+ a.socket do |s, a|
17
+ end
18
+ end
19
+
20
+ test "can perform" do
21
+ n = ResqueAps::Notification.new('application_name' => 'TestApp', 'device_token' => 'aihdf08u2402hbdfquhiwr', 'payload' => '{"aps": { "alert": "hello"}}')
22
+ assert Resque.enqueue_aps('TestApp', n)
23
+ Resque.create_aps_application('TestApp', File.dirname(__FILE__) + "/../test-dev.pem", nil)
24
+ ResqueAps::Application.perform('TestApp')
25
+ end
26
+
27
+ context "ApplicationWithHooks" do
28
+ class ApplicationWithHooks < ResqueAps::Application
29
+ def before_aps_write(notification)
30
+ logger.debug "before_aps_write #{notification.inspect}"
31
+ end
32
+
33
+ def after_aps_write(notification)
34
+ logger.debug "after_aps_write #{notification.inspect}"
35
+ end
36
+
37
+ def failed_aps_write(notification)
38
+ logger.debug "failed_aps_write #{notification.inspect}"
39
+ end
40
+
41
+ def notify_aps_admin(exception)
42
+ logger.debug "notify_aps_admin #{exception}"
43
+ end
44
+
45
+ def aps_nil_notification_retry?(sent_count, start_time)
46
+ logger.debug "aps_nil_notification_retry #{sent_count}"
47
+ false
48
+ end
49
+ end
50
+
51
+ test "can perform with logging hooks" do
52
+ n = ResqueAps::Notification.new('application_name' => 'TestApp', 'device_token' => 'aihdf08u2402hbdfquhiwr', 'payload' => '{"aps": { "alert": "hello"}}')
53
+ assert Resque.enqueue_aps('TestApp', n)
54
+ Resque.aps_application_class = ApplicationWithHooks
55
+ Resque.create_aps_application('TestApp', File.dirname(__FILE__) + "/../test-dev.pem", nil)
56
+ ResqueAps::Application.perform('TestApp')
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ context "ResqueAps::Notification" do
4
+ test "has a nice #inspect" do
5
+ n = ResqueAps::Notification.new('application_name' => 'SomeApp', 'device_token' => 'aihdf08u2402hbdfquhiwr', 'payload' => '{"aps": { "alert": "hello"}}')
6
+ assert_equal '#<ResqueAps::Notification "SomeApp", "aihdf08u2402hbdfquhiwr", "{\"aps\": { \"alert\": \"hello\"}}">', n.inspect
7
+ end
8
+ end
@@ -0,0 +1,115 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize yes
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ pidfile ./test/redis-test.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 9736
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ save 900 1
34
+ save 300 10
35
+ save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ dbfilename dump.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ dir ./test/
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 16
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ glueoutputbuf yes
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ # Pull in the server test_helper from resque
4
+ require 'resque/server/test_helper.rb'
5
+
6
+ context "on GET to /aps" do
7
+ setup { get "/aps" }
8
+
9
+ should_respond_with_success
10
+ end
11
+
12
+ context "on GET to /aps with applications" do
13
+ setup do
14
+ Resque.redis.flushall
15
+ Resque.create_aps_application(:some_ivar_application, nil, nil)
16
+ Resque.create_aps_application(:someother_ivar_application, nil, nil)
17
+ get "/aps"
18
+ end
19
+
20
+ should_respond_with_success
21
+
22
+ test 'see the applications' do
23
+ assert last_response.body.include?('some_ivar_application')
24
+ assert last_response.body.include?('someother_ivar_application')
25
+ end
26
+ end
27
+
28
+ context "on GET to /aps/some_ivar_application" do
29
+ setup do
30
+ Resque.redis.flushall
31
+ Resque.create_aps_application(:some_ivar_application, nil, nil)
32
+ n = ResqueAps::Notification.new('application_name' => 'some_ivar_application', 'device_token' => 'aihdf08u2402hbdfquhiwr', 'payload' => '{"aps": { "alert": "hello"}}')
33
+ assert Resque.enqueue_aps(:some_ivar_application, n)
34
+ assert Resque.enqueue_aps(:some_ivar_application, n)
35
+ assert Resque.enqueue_aps(:some_ivar_application, n)
36
+ assert Resque.enqueue_aps(:some_ivar_application, n)
37
+ get "/aps/some_ivar_application"
38
+ end
39
+
40
+ should_respond_with_success
41
+
42
+ test 'see the applications' do
43
+ assert last_response.body.include?('some_ivar_application')
44
+ end
45
+ end
@@ -0,0 +1,85 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ context "ResqueAps" do
4
+ setup do
5
+ Resque.redis.flushall
6
+ end
7
+
8
+ # test "needs to infer a queue with enqueue" do
9
+ # assert_raises Resque::NoQueueError do
10
+ # Resque.enqueue(SomeJob, 20, '/tmp')
11
+ # end
12
+ # end
13
+
14
+ test "can create an application" do
15
+ assert Resque.create_aps_application('SomeApp', '/var/apps/certificates/someapp.pem')
16
+ assert_equal 1, Resque.aps_applications_count
17
+ assert Resque.create_aps_application('SomeApp2', '/var/apps/certificates/someapp2.pem', 'secret')
18
+ assert_equal 2, Resque.aps_applications_count
19
+ end
20
+
21
+ test "can get an application" do
22
+ Resque.create_aps_application('SomeApp', '/var/apps/certificates/someapp.pem', 'secret')
23
+ a = Resque.aps_application('SomeApp')
24
+ assert_equal 'SomeApp', a.name
25
+ assert_equal '/var/apps/certificates/someapp.pem', a.cert_file
26
+ assert_equal 'secret', a.cert_passwd
27
+ end
28
+
29
+ # test "can update an application" do
30
+ # end
31
+
32
+ test "knows how big the application queue is" do
33
+ assert_equal 0, Resque.aps_applications_count
34
+
35
+ Resque.create_aps_application('SomeApp', '/var/apps/certificates/someapp.pem', 'secret')
36
+ assert_equal 1, Resque.aps_applications_count
37
+ end
38
+
39
+ test "can get a list of application names" do
40
+ assert Resque.create_aps_application('SomeApp', '/var/apps/certificates/someapp.pem')
41
+ assert Resque.create_aps_application('SomeApp2', '/var/apps/certificates/someapp2.pem', 'secret')
42
+ assert_equal ['SomeApp2', 'SomeApp'], Resque.aps_application_names(0, 2)
43
+ end
44
+
45
+ test "can enqueue aps notifications" do
46
+ n = ResqueAps::Notification.new('application_name' => 'SomeApp', 'device_token' => 'aihdf08u2402hbdfquhiwr', 'payload' => '{"aps": { "alert": "hello"}}')
47
+ assert Resque.enqueue_aps('SomeApp', n)
48
+ assert_equal 1, Resque.aps_notification_count_for_application('SomeApp')
49
+ assert Resque.enqueue_aps('SomeApp', n)
50
+ assert_equal 2, Resque.aps_notification_count_for_application('SomeApp')
51
+ end
52
+
53
+ test "can dequeue aps notifications" do
54
+ n = ResqueAps::Notification.new('application_name' => 'SomeApp', 'device_token' => 'aihdf08u2402hbdfquhiwr', 'payload' => '{"aps": { "alert": "hello"}}')
55
+ assert Resque.enqueue_aps('SomeApp', n)
56
+ assert_equal 1, Resque.aps_notification_count_for_application('SomeApp')
57
+
58
+ nn = Resque.dequeue_aps('SomeApp')
59
+
60
+ assert nn
61
+ assert_kind_of ResqueAps::Notification, nn
62
+ assert_equal n.application_name, nn.application_name
63
+ assert_equal n.device_token, nn.device_token
64
+ assert_equal n.payload, nn.payload
65
+ end
66
+
67
+ test "knows how big the application notification queue is" do
68
+ n = ResqueAps::Notification.new('application_name' => 'SomeApp', 'device_token' => 'aihdf08u2402hbdfquhiwr', 'payload' => '{"aps": { "alert": "hello"}}')
69
+ assert Resque.enqueue_aps('SomeApp', n)
70
+ assert_equal 1, Resque.aps_notification_count_for_application('SomeApp')
71
+
72
+ assert Resque.enqueue_aps('SomeApp', n)
73
+ assert_equal 2, Resque.aps_notification_count_for_application('SomeApp')
74
+ end
75
+
76
+ test "can get a list of application notifications" do
77
+ n = ResqueAps::Notification.new('application_name' => 'SomeApp', 'device_token' => 'aihdf08u2402hbdfquhiwr', 'payload' => '{"aps": { "alert": "hello"}}')
78
+ assert Resque.enqueue_aps('SomeApp', n)
79
+ assert Resque.enqueue_aps('SomeApp', n)
80
+ assert Resque.enqueue_aps('SomeApp', n)
81
+ assert_equal 3, Resque.aps_notification_count_for_application('SomeApp')
82
+ a = Resque.aps_notifications_for_application('SomeApp', 0, 20)
83
+ assert_equal 3, a.size
84
+ end
85
+ end
@@ -0,0 +1,78 @@
1
+
2
+ # Pretty much copied this file from the resque test_helper since we want
3
+ # to do all the same stuff
4
+
5
+ dir = File.dirname(File.expand_path(__FILE__))
6
+
7
+ require 'rubygems'
8
+ require 'test/unit'
9
+ require 'mocha'
10
+ require 'resque'
11
+ require File.join(dir, '../lib/resque_aps')
12
+ $LOAD_PATH.unshift File.dirname(File.expand_path(__FILE__)) + '/../lib'
13
+
14
+
15
+ #
16
+ # make sure we can run redis
17
+ #
18
+
19
+ if !system("which redis-server")
20
+ puts '', "** can't find `redis-server` in your path"
21
+ puts "** try running `sudo rake install`"
22
+ abort ''
23
+ end
24
+
25
+
26
+ #
27
+ # start our own redis when the tests start,
28
+ # kill it when they end
29
+ #
30
+
31
+ at_exit do
32
+ next if $!
33
+
34
+ if defined?(MiniTest)
35
+ exit_code = MiniTest::Unit.new.run(ARGV)
36
+ else
37
+ exit_code = Test::Unit::AutoRunner.run
38
+ end
39
+
40
+ pid = `ps -e -o pid,command | grep [r]edis-test`.split(" ")[0]
41
+ puts "Killing test redis server..."
42
+ `rm -f #{dir}/dump.rdb`
43
+ Process.kill("KILL", pid.to_i)
44
+ exit exit_code
45
+ end
46
+
47
+ puts "Starting redis for testing at localhost:9736..."
48
+ `redis-server #{dir}/redis-test.conf`
49
+ Resque.redis = 'localhost:9736'
50
+
51
+ ##
52
+ # test/spec/mini 3
53
+ # http://gist.github.com/25455
54
+ # chris@ozmm.org
55
+ #
56
+ def context(*args, &block)
57
+ return super unless (name = args.first) && block
58
+ require 'test/unit'
59
+ klass = Class.new(defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase) do
60
+ def self.test(name, &block)
61
+ define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
62
+ end
63
+ def self.xtest(*args) end
64
+ def self.setup(&block) define_method(:setup, &block) end
65
+ def self.teardown(&block) define_method(:teardown, &block) end
66
+ end
67
+ (class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') }
68
+ klass.class_eval &block
69
+ end
70
+
71
+ class SomeJob
72
+ def self.perform(repo_id, path)
73
+ end
74
+ end
75
+
76
+ class SomeIvarJob < SomeJob
77
+ @queue = :ivar
78
+ end
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-aps
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 9
8
+ - 0
9
+ version: 0.9.0
10
+ platform: ruby
11
+ authors:
12
+ - Ashley Martens
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-07-07 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: redis
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 0
30
+ - 1
31
+ version: 2.0.1
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: resque
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 8
44
+ - 0
45
+ version: 1.8.0
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: jeweler
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ version: "0"
58
+ type: :development
59
+ version_requirements: *id003
60
+ - !ruby/object:Gem::Dependency
61
+ name: mocha
62
+ prerelease: false
63
+ requirement: &id004 !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ type: :development
71
+ version_requirements: *id004
72
+ - !ruby/object:Gem::Dependency
73
+ name: rack-test
74
+ prerelease: false
75
+ requirement: &id005 !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ type: :development
83
+ version_requirements: *id005
84
+ description: |-
85
+ Queuing system for Apple's Push Service on top of Resque.
86
+ Adds methods enqueue_aps to queue a notification message.
87
+ Also includes helper classes and methods to format JSON.
88
+ email: ashleym1972@gmail.com
89
+ executables: []
90
+
91
+ extensions: []
92
+
93
+ extra_rdoc_files:
94
+ - LICENSE
95
+ - README
96
+ files:
97
+ - .gitignore
98
+ - HISTORY.md
99
+ - LICENSE
100
+ - README
101
+ - Rakefile
102
+ - lib/resque_aps.rb
103
+ - lib/resque_aps/application.rb
104
+ - lib/resque_aps/helper.rb
105
+ - lib/resque_aps/notification.rb
106
+ - lib/resque_aps/server.rb
107
+ - lib/resque_aps/server/views/aps_applications.erb
108
+ - lib/resque_aps/server/views/notifications.erb
109
+ - lib/resque_aps/tasks.rb
110
+ - lib/resque_aps/unknown_attribute_error.rb
111
+ - lib/resque_aps/version.rb
112
+ - tasks/resque_aps.rake
113
+ - test/application_test.rb
114
+ - test/notification_test.rb
115
+ - test/redis-test.conf
116
+ - test/resque-web_test.rb
117
+ - test/resque_aps_test.rb
118
+ - test/test_helper.rb
119
+ has_rdoc: true
120
+ homepage: http://github.com/ashleym1972/resque-aps
121
+ licenses: []
122
+
123
+ post_install_message:
124
+ rdoc_options:
125
+ - --charset=UTF-8
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ segments:
133
+ - 0
134
+ version: "0"
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ segments:
140
+ - 0
141
+ version: "0"
142
+ requirements: []
143
+
144
+ rubyforge_project:
145
+ rubygems_version: 1.3.6
146
+ signing_key:
147
+ specification_version: 3
148
+ summary: Queuing system for Apple's Push Service on top of Resque
149
+ test_files:
150
+ - test/application_test.rb
151
+ - test/notification_test.rb
152
+ - test/resque-web_test.rb
153
+ - test/resque_aps_test.rb
154
+ - test/test_helper.rb