Skip to content

Commit

Permalink
Nearly done with feedback. Just adding documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
markbates committed Jul 31, 2009
1 parent cf4ae4f commit 3b50ff1
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 17 deletions.
8 changes: 8 additions & 0 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions README.textile
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 3 additions & 3 deletions apn_on_rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
10 changes: 6 additions & 4 deletions generators/apn_migrations_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions lib/apn_on_rails/app/models/apn/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module APN
class Base < ActiveRecord::Base # :nodoc:

def self.table_name # :nodoc:
self.to_s.gsub("::", "_").tableize
end

end
end
17 changes: 15 additions & 2 deletions lib/apn_on_rails/app/models/apn/device.rb
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
4 changes: 1 addition & 3 deletions lib/apn_on_rails/app/models/apn/notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
File renamed without changes.
42 changes: 42 additions & 0 deletions lib/apn_on_rails/libs/feedback.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions lib/apn_on_rails/tasks/apn.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions spec/apn_on_rails/app/models/apn/device_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion spec/apn_on_rails/app/models/apn/notification_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb')
require File.dirname(__FILE__) + '/../../spec_helper'

describe APN::Connection do

Expand Down Expand Up @@ -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
Expand All @@ -37,4 +37,4 @@

end

end
end
57 changes: 57 additions & 0 deletions spec/apn_on_rails/libs/feedback_spec.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.