resque-aps 0.9.0

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