Browse files

Nearly done with feedback. Just adding documentation.

  • Loading branch information...
1 parent cf4ae4f commit 3b50ff10da109a6f70b0ad3cd0cfd54e6f78c994 @markbates markbates committed Jul 31, 2009
View
8 README
@@ -84,6 +84,14 @@ see fit:
That's it, now you're ready to start creating notifications.
+===Upgrade Notes:
+
+If you are upgrading to a new version of APN on Rails you should always run:
+
+ $ ruby script/generate apn_migrations
+
+That way you ensure you have the latest version of the database tables needed.
+
==Example:
$ ./script/console
View
9 README.textile
@@ -94,6 +94,15 @@ see fit:
That's it, now you're ready to start creating notifications.
+h3. Upgrade Notes:
+
+If you are upgrading to a new version of APN on Rails you should always run:
+<pre><code>
+ $ ruby script/generate apn_migrations
+</code></pre>
+
+That way you ensure you have the latest version of the database tables needed.
+
h2. Example:
<pre><code>
View
2 Rakefile
@@ -4,7 +4,7 @@ require 'gemstub'
Gemstub.test_framework = :rspec
Gemstub.gem_spec do |s|
- s.version = "0.2.2"
+ s.version = "0.3.0"
s.rubyforge_project = "magrathea"
s.add_dependency('configatron')
s.email = 'mark@markbates.com'
View
6 apn_on_rails.gemspec
@@ -2,15 +2,15 @@
Gem::Specification.new do |s|
s.name = %q{apn_on_rails}
- s.version = "0.2.2.20090730143010"
+ s.version = "0.3.0.20090731174823"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["markbates"]
- s.date = %q{2009-07-30}
+ s.date = %q{2009-07-31}
s.description = %q{apn_on_rails was developed by: markbates}
s.email = %q{mark@markbates.com}
s.extra_rdoc_files = ["README", "LICENSE"]
- s.files = ["lib/apn_on_rails/apn_on_rails.rb", "lib/apn_on_rails/app/models/apn/connection.rb", "lib/apn_on_rails/app/models/apn/device.rb", "lib/apn_on_rails/app/models/apn/notification.rb", "lib/apn_on_rails/tasks/apn.rake", "lib/apn_on_rails/tasks/db.rake", "lib/apn_on_rails.rb", "lib/apn_on_rails_tasks.rb", "README", "LICENSE", "generators/apn_migrations_generator.rb", "generators/templates/apn_migrations/001_create_apn_devices.rb", "generators/templates/apn_migrations/002_create_apn_notifications.rb"]
+ s.files = ["lib/apn_on_rails/apn_on_rails.rb", "lib/apn_on_rails/app/models/apn/base.rb", "lib/apn_on_rails/app/models/apn/device.rb", "lib/apn_on_rails/app/models/apn/notification.rb", "lib/apn_on_rails/libs/connection.rb", "lib/apn_on_rails/libs/feedback.rb", "lib/apn_on_rails/tasks/apn.rake", "lib/apn_on_rails/tasks/db.rake", "lib/apn_on_rails.rb", "lib/apn_on_rails_tasks.rb", "README", "LICENSE", "generators/apn_migrations_generator.rb", "generators/templates/apn_migrations/001_create_apn_devices.rb", "generators/templates/apn_migrations/002_create_apn_notifications.rb", "generators/templates/apn_migrations/003_add_registered_at_to_apn_devices.rb"]
s.homepage = %q{http://www.metabates.com}
s.require_paths = ["lib"]
s.rubyforge_project = %q{magrathea}
View
10 generators/apn_migrations_generator.rb
@@ -9,11 +9,13 @@ def manifest # :nodoc:
m.directory(db_migrate_path)
- ['001_create_apn_devices', '002_create_apn_notifications'].each_with_index do |f, i|
+ Dir.glob(File.join(File.dirname(__FILE__), 'templates', 'apn_migrations', '*.rb')).sort.each_with_index do |f, i|
+ f = File.basename(f)
+ f.match(/\d+\_(.+)/)
timestamp = timestamp.succ
- if Dir.glob(File.join(db_migrate_path, "*_#{f}.rb")).empty?
- m.file(File.join('apn_migrations', "#{f}.rb"),
- File.join(db_migrate_path, "#{timestamp}_#{f}.rb"),
+ if Dir.glob(File.join(db_migrate_path, "*_#{$1}")).empty?
+ m.file(File.join('apn_migrations', f),
+ File.join(db_migrate_path, "#{timestamp}_#{$1}"),
{:collision => :skip})
end
end
View
22 generators/templates/apn_migrations/003_add_registered_at_to_apn_devices.rb
@@ -0,0 +1,22 @@
+class AddRegisteredAtToApnDevices < ActiveRecord::Migration # :nodoc:
+
+ module APN
+ class Device < ActiveRecord::Base
+ set_table_name 'apn_devices'
+ end
+ end
+
+ def self.up
+ add_column :apn_devices, :last_registered_at, :datetime
+
+ APN::Device.all.each do |device|
+ device.last_registered_at = device.created_at
+ device.save!
+ end
+
+ end
+
+ def self.down
+ remove_column :apn_devices, :last_registered_at
+ end
+end
View
9 lib/apn_on_rails/app/models/apn/base.rb
@@ -0,0 +1,9 @@
+module APN
+ class Base < ActiveRecord::Base # :nodoc:
+
+ def self.table_name # :nodoc:
+ self.to_s.gsub("::", "_").tableize
+ end
+
+ end
+end
View
17 lib/apn_on_rails/app/models/apn/device.rb
@@ -1,16 +1,24 @@
# Represents an iPhone (or other APN enabled device).
# An APN::Device can have many APN::Notification.
#
+# In order for the APN::Feedback system to work properly you *MUST*
+# touch the <tt>last_registered_at</tt> column everytime someone opens
+# your application. If you do not, then it is possible, and probably likely,
+# that their device will be removed and will no longer receive notifications.
+#
# Example:
# Device.create(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
-class APN::Device < ActiveRecord::Base
- set_table_name 'apn_devices'
+class APN::Device < APN::Base
has_many :notifications, :class_name => 'APN::Notification'
validates_uniqueness_of :token
validates_format_of :token, :with => /^[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}$/
+ before_save :set_last_registered_at
+
+ attr_accessor :feedback_at
+
# Stores the token (Apple's device ID) of the iPhone (device).
#
# If the token comes in like this:
@@ -29,4 +37,9 @@ def to_hexa
[self.token.delete(' ')].pack('H*')
end
+ private
+ def set_last_registered_at
+ self.last_registered_at = Time.now if self.last_registered_at.nil?
+ end
+
end
View
4 lib/apn_on_rails/app/models/apn/notification.rb
@@ -14,12 +14,10 @@
#
# As each APN::Notification is sent the <tt>sent_at</tt> column will be timestamped,
# so as to not be sent again.
-class APN::Notification < ActiveRecord::Base
+class APN::Notification < APN::Base
include ::ActionView::Helpers::TextHelper
extend ::ActionView::Helpers::TextHelper
- set_table_name 'apn_notifications'
-
belongs_to :device, :class_name => 'APN::Device'
# Stores the text alert message you want to send to the device.
View
0 ...apn_on_rails/app/models/apn/connection.rb → lib/apn_on_rails/libs/connection.rb
File renamed without changes.
View
42 lib/apn_on_rails/libs/feedback.rb
@@ -0,0 +1,42 @@
+module APN
+ # Module for talking to the Apple Feedback Service.
+ # The service is meant to let you know when a device is no longer
+ # registered to receive notifications for your application.
+ module Feedback
+
+ class << self
+
+ # Returns an Array of APN::Device objects that
+ # has received feedback from Apple. Each APN::Device will
+ # have it's <tt>feedback_at</tt> accessor marked with the time
+ # that Apple believes the device de-registered itself.
+ def devices(&block)
+ devices = []
+ APN::Connection.open_for_feedback do |conn, sock|
+ while line = sock.gets # Read lines from the socket
+ line.strip!
+ feedback = line.unpack('N1n1H140')
+ token = feedback[2].scan(/.{0,8}/).join(' ').strip
+ device = APN::Device.find(:first, :conditions => {:token => token})
+ if device
+ device.feedback_at = Time.at(feedback[0])
+ devices << device
+ end
+ end
+ end
+ devices.each(&block) if block_given?
+ return devices
+ end # devices
+
+ def process_devices
+ APN::Feedback.devices.each do |device|
+ if device.last_registered_at < device.feedback_at
+ device.destroy
+ end
+ end
+ end # process_devices
+
+ end # class << self
+
+ end # Feedback
+end # APN
View
9 lib/apn_on_rails/tasks/apn.rake
@@ -9,4 +9,13 @@ namespace :apn do
end # notifications
+ namespace :feedback do
+
+ desc "Process all devices that have feedback from APN."
+ task :process => [:environment] do
+ APN::Feedback.process_devices
+ end
+
+ end
+
end # apn
View
17 spec/apn_on_rails/app/models/apn/device_spec.rb
@@ -40,4 +40,21 @@
end
+ describe 'before_save' do
+
+ it 'should set the last_registered_at date to Time.now if nil' do
+ time = Time.now
+ Time.stub(:now).and_return(time)
+ device = DeviceFactory.create
+ device.last_registered_at.should_not be_nil
+ device.last_registered_at.to_s.should == time.to_s
+
+ ago = 1.week.ago
+ device = DeviceFactory.create(:last_registered_at => ago)
+ device.last_registered_at.should_not be_nil
+ device.last_registered_at.to_s.should == ago.to_s
+ end
+
+ end
+
end
View
2 spec/apn_on_rails/app/models/apn/notification_spec.rb
@@ -70,7 +70,7 @@
ssl_mock = mock('ssl_mock')
ssl_mock.should_receive(:write).with('message-0')
ssl_mock.should_receive(:write).with('message-1')
- APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock)
+ APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil)
APN::Notification.send_notifications(notifications)
View
6 ...n_rails/app/models/apn/connection_spec.rb → spec/apn_on_rails/libs/connection_spec.rb
@@ -1,4 +1,4 @@
-require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb')
+require File.dirname(__FILE__) + '/../../spec_helper'
describe APN::Connection do
@@ -28,7 +28,7 @@
ssl_mock.should_receive(:close)
OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_mock, ctx_mock).and_return(ssl_mock)
- APN::Connection.open_for_delivery do |conn|
+ APN::Connection.open_for_delivery do |conn, sock|
conn.write('message-0')
conn.write('message-1')
end
@@ -37,4 +37,4 @@
end
-end
+end
View
57 spec/apn_on_rails/libs/feedback_spec.rb
@@ -0,0 +1,57 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe APN::Feedback do
+
+ describe 'devices' do
+
+ before(:each) do
+ @time = Time.now
+ @device = DeviceFactory.create
+
+ @data_mock = mock('data_mock')
+ @data_mock.should_receive(:strip!)
+ @data_mock.should_receive(:unpack).with('N1n1H140').and_return([@time.to_i, 12388, @device.token.delete(' ')])
+
+ @ssl_mock = mock('ssl_mock')
+ @sock_mock = mock('sock_mock')
+ @sock_mock.should_receive(:gets).twice.and_return(@data_mock, nil)
+
+ end
+
+ it 'should an Array of devices that need to be processed' do
+ APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock)
+
+ devices = APN::Feedback.devices
+ devices.size.should == 1
+ r_device = devices.first
+ r_device.token.should == @device.token
+ r_device.feedback_at.to_s.should == @time.to_s
+ end
+
+ it 'should yield up each device' do
+ APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock)
+ lambda {
+ APN::Feedback.devices do |r_device|
+ r_device.token.should == @device.token
+ r_device.feedback_at.to_s.should == @time.to_s
+ raise BlockRan.new
+ end
+ }.should raise_error(BlockRan)
+ end
+
+ end
+
+ describe 'process_devices' do
+
+ it 'should destroy devices that have a last_registered_at date that is before the feedback_at date' do
+ devices = [DeviceFactory.create(:last_registered_at => 1.week.ago, :feedback_at => Time.now),
+ DeviceFactory.create(:last_registered_at => 1.week.from_now, :feedback_at => Time.now)]
+ APN::Feedback.should_receive(:devices).and_return(devices)
+ lambda {
+ APN::Feedback.process_devices
+ }.should change(APN::Device, :count).by(-1)
+ end
+
+ end
+
+end
View
3 spec/spec_helper.rb
@@ -50,4 +50,7 @@ def write_fixture(name, value)
def apn_cert
File.read(File.join(File.dirname(__FILE__), 'rails_root', 'config', 'apple_push_notification_development.pem'))
+end
+
+class BlockRan < StandardError
end

0 comments on commit 3b50ff1

Please sign in to comment.