presskit-apn_on_rails 0.1

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.
Files changed (60) hide show
  1. data/.rspec +2 -0
  2. data/.specification +80 -0
  3. data/Gemfile +19 -0
  4. data/Gemfile.lock +47 -0
  5. data/LICENSE +21 -0
  6. data/README +179 -0
  7. data/README.textile +209 -0
  8. data/Rakefile +50 -0
  9. data/VERSION +1 -0
  10. data/autotest/discover.rb +1 -0
  11. data/generators/apn_migrations_generator.rb +31 -0
  12. data/generators/templates/apn_migrations/001_create_apn_devices.rb +13 -0
  13. data/generators/templates/apn_migrations/002_create_apn_notifications.rb +23 -0
  14. data/generators/templates/apn_migrations/003_alter_apn_devices.rb +25 -0
  15. data/generators/templates/apn_migrations/004_create_apn_apps.rb +18 -0
  16. data/generators/templates/apn_migrations/005_create_groups.rb +23 -0
  17. data/generators/templates/apn_migrations/006_alter_apn_groups.rb +11 -0
  18. data/generators/templates/apn_migrations/007_create_device_groups.rb +27 -0
  19. data/generators/templates/apn_migrations/008_create_apn_group_notifications.rb +23 -0
  20. data/generators/templates/apn_migrations/009_create_pull_notifications.rb +16 -0
  21. data/generators/templates/apn_migrations/010_alter_apn_notifications.rb +21 -0
  22. data/generators/templates/apn_migrations/011_make_device_token_index_nonunique.rb +11 -0
  23. data/generators/templates/apn_migrations/012_add_launch_notification_to_apn_pull_notifications.rb +9 -0
  24. data/lib/apn_on_rails.rb +4 -0
  25. data/lib/apn_on_rails/apn_on_rails.rb +81 -0
  26. data/lib/apn_on_rails/app/models/apn/app.rb +152 -0
  27. data/lib/apn_on_rails/app/models/apn/base.rb +9 -0
  28. data/lib/apn_on_rails/app/models/apn/device.rb +50 -0
  29. data/lib/apn_on_rails/app/models/apn/device_grouping.rb +16 -0
  30. data/lib/apn_on_rails/app/models/apn/group.rb +12 -0
  31. data/lib/apn_on_rails/app/models/apn/group_notification.rb +79 -0
  32. data/lib/apn_on_rails/app/models/apn/notification.rb +93 -0
  33. data/lib/apn_on_rails/app/models/apn/pull_notification.rb +28 -0
  34. data/lib/apn_on_rails/libs/connection.rb +70 -0
  35. data/lib/apn_on_rails/libs/feedback.rb +39 -0
  36. data/lib/apn_on_rails/tasks/apn.rake +30 -0
  37. data/lib/apn_on_rails/tasks/db.rake +19 -0
  38. data/lib/apn_on_rails_tasks.rb +3 -0
  39. data/presskit-apn_on_rails.gemspec +144 -0
  40. data/spec/active_record/setup_ar.rb +19 -0
  41. data/spec/apn_on_rails/app/models/apn/app_spec.rb +230 -0
  42. data/spec/apn_on_rails/app/models/apn/device_spec.rb +60 -0
  43. data/spec/apn_on_rails/app/models/apn/group_notification_spec.rb +66 -0
  44. data/spec/apn_on_rails/app/models/apn/notification_spec.rb +71 -0
  45. data/spec/apn_on_rails/app/models/apn/pull_notification_spec.rb +100 -0
  46. data/spec/apn_on_rails/libs/connection_spec.rb +40 -0
  47. data/spec/apn_on_rails/libs/feedback_spec.rb +43 -0
  48. data/spec/extensions/string.rb +10 -0
  49. data/spec/factories/app_factory.rb +27 -0
  50. data/spec/factories/device_factory.rb +29 -0
  51. data/spec/factories/device_grouping_factory.rb +22 -0
  52. data/spec/factories/group_factory.rb +27 -0
  53. data/spec/factories/group_notification_factory.rb +22 -0
  54. data/spec/factories/notification_factory.rb +22 -0
  55. data/spec/factories/pull_notification_factory.rb +22 -0
  56. data/spec/fixtures/hexa.bin +1 -0
  57. data/spec/fixtures/message_for_sending.bin +0 -0
  58. data/spec/rails_root/config/apple_push_notification_development.pem +19 -0
  59. data/spec/spec_helper.rb +55 -0
  60. metadata +282 -0
@@ -0,0 +1,9 @@
1
+ module APN
2
+ class Base < ActiveRecord::Base # :nodoc:
3
+
4
+ def self.table_name # :nodoc:
5
+ self.to_s.gsub("::", "_").tableize
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ # Represents an iPhone (or other APN enabled device).
2
+ # An APN::Device can have many APN::Notification.
3
+ #
4
+ # In order for the APN::Feedback system to work properly you *MUST*
5
+ # touch the <tt>last_registered_at</tt> column everytime someone opens
6
+ # your application. If you do not, then it is possible, and probably likely,
7
+ # that their device will be removed and will no longer receive notifications.
8
+ #
9
+ # Example:
10
+ # Device.create(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
11
+ class APN::Device < APN::Base
12
+
13
+ belongs_to :app, :class_name => 'APN::App'
14
+ has_many :notifications, :class_name => 'APN::Notification'
15
+ has_many :unsent_notifications, :class_name => 'APN::Notification', :conditions => 'sent_at is null'
16
+ belongs_to :user
17
+
18
+ validates_uniqueness_of :token, :scope => :artist_id
19
+ 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}$/
20
+
21
+ before_create :set_last_registered_at
22
+
23
+ # The <tt>feedback_at</tt> accessor is set when the
24
+ # device is marked as potentially disconnected from your
25
+ # application by Apple.
26
+ attr_accessor :feedback_at
27
+
28
+ # Stores the token (Apple's device ID) of the iPhone (device).
29
+ #
30
+ # If the token comes in like this:
31
+ # '<5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz>'
32
+ # Then the '<' and '>' will be stripped off.
33
+ def token=(token)
34
+ res = token.scan(/\<(.+)\>/).first
35
+ unless res.nil? || res.empty?
36
+ token = res.first
37
+ end
38
+ write_attribute('token', token)
39
+ end
40
+
41
+ # Returns the hexadecimal representation of the device's token.
42
+ def to_hexa
43
+ [self.token.delete(' ')].pack('H*')
44
+ end
45
+
46
+ def set_last_registered_at
47
+ self.last_registered_at = Time.now #if self.last_registered_at.nil?
48
+ end
49
+
50
+ end
@@ -0,0 +1,16 @@
1
+ class APN::DeviceGrouping < APN::Base
2
+
3
+ belongs_to :group, :class_name => 'APN::Group'
4
+ belongs_to :device, :class_name => 'APN::Device'
5
+
6
+ validates_presence_of :device_id, :group_id
7
+ validate :same_app_id
8
+ validates_uniqueness_of :device_id, :scope => :group_id
9
+
10
+ def same_app_id
11
+ unless self.group and self.device and self.group.app_id == self.device.app_id
12
+ errors.add_to_base("device and group must belong to the same app")
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,12 @@
1
+ class APN::Group < APN::Base
2
+
3
+ belongs_to :app, :class_name => 'APN::App'
4
+ has_many :device_groupings, :class_name => "APN::DeviceGrouping", :dependent => :destroy
5
+ has_many :devices, :class_name => 'APN::Device', :through => :device_groupings
6
+ has_many :group_notifications, :class_name => 'APN::GroupNotification'
7
+ has_many :unsent_group_notifications, :class_name => 'APN::GroupNotification', :conditions => 'sent_at is null'
8
+
9
+ validates_presence_of :app_id
10
+ validates_uniqueness_of :name, :scope => :app_id
11
+
12
+ end
@@ -0,0 +1,79 @@
1
+ class APN::GroupNotification < APN::Base
2
+ include ::ActionView::Helpers::TextHelper
3
+ extend ::ActionView::Helpers::TextHelper
4
+ serialize :custom_properties
5
+
6
+ belongs_to :group, :class_name => 'APN::Group'
7
+ has_one :app, :class_name => 'APN::App', :through => :group
8
+ has_many :device_groupings, :through => :group
9
+
10
+ validates_presence_of :group_id
11
+
12
+ def devices
13
+ self.group.devices
14
+ end
15
+
16
+ # Stores the text alert message you want to send to the device.
17
+ #
18
+ # If the message is over 150 characters long it will get truncated
19
+ # to 150 characters with a <tt>...</tt>
20
+ def alert=(message)
21
+ if !message.blank? && message.size > 150
22
+ message = truncate(message, :length => 150)
23
+ end
24
+ write_attribute('alert', message)
25
+ end
26
+
27
+ # Creates a Hash that will be the payload of an APN.
28
+ #
29
+ # Example:
30
+ # apn = APN::GroupNotification.new
31
+ # apn.badge = 5
32
+ # apn.sound = 'my_sound.aiff'
33
+ # apn.alert = 'Hello!'
34
+ # apn.apple_hash # => {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}}
35
+ #
36
+ # Example 2:
37
+ # apn = APN::GroupNotification.new
38
+ # apn.badge = 0
39
+ # apn.sound = true
40
+ # apn.custom_properties = {"typ" => 1}
41
+ # apn.apple_hash # => {"aps" => {"badge" => 0, "sound" => 1.aiff},"typ" => "1"}
42
+ def apple_hash
43
+ result = {}
44
+ result['aps'] = {}
45
+ result['aps']['alert'] = self.alert if self.alert
46
+ result['aps']['badge'] = self.badge.to_i if self.badge
47
+ if self.sound
48
+ result['aps']['sound'] = self.sound if self.sound.is_a? String
49
+ result['aps']['sound'] = "1.aiff" if self.sound.is_a?(TrueClass)
50
+ end
51
+ if self.custom_properties
52
+ self.custom_properties.each do |key,value|
53
+ result["#{key}"] = "#{value}"
54
+ end
55
+ end
56
+ result
57
+ end
58
+
59
+ # Creates the JSON string required for an APN message.
60
+ #
61
+ # Example:
62
+ # apn = APN::Notification.new
63
+ # apn.badge = 5
64
+ # apn.sound = 'my_sound.aiff'
65
+ # apn.alert = 'Hello!'
66
+ # apn.to_apple_json # => '{"aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}'
67
+ def to_apple_json
68
+ self.apple_hash.to_json
69
+ end
70
+
71
+ # Creates the binary message needed to send to Apple.
72
+ def message_for_sending(device)
73
+ json = self.to_apple_json
74
+ message = "\0\0 #{device.to_hexa}\0#{json.length.chr}#{json}"
75
+ raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256
76
+ message
77
+ end
78
+
79
+ end # APN::Notification
@@ -0,0 +1,93 @@
1
+ # Represents the message you wish to send.
2
+ # An APN::Notification belongs to an APN::Device.
3
+ #
4
+ # Example:
5
+ # apn = APN::Notification.new
6
+ # apn.badge = 5
7
+ # apn.sound = 'my_sound.aiff'
8
+ # apn.alert = 'Hello!'
9
+ # apn.device = APN::Device.find(1)
10
+ # apn.save
11
+ #
12
+ # To deliver call the following method:
13
+ # APN::Notification.send_notifications
14
+ #
15
+ # As each APN::Notification is sent the <tt>sent_at</tt> column will be timestamped,
16
+ # so as to not be sent again.
17
+ class APN::Notification < APN::Base
18
+ include ::ActionView::Helpers::TextHelper
19
+ extend ::ActionView::Helpers::TextHelper
20
+ serialize :custom_properties
21
+
22
+ belongs_to :device, :class_name => 'APN::Device'
23
+ has_one :app, :class_name => 'APN::App', :through => :device
24
+
25
+ # Stores the text alert message you want to send to the device.
26
+ #
27
+ # If the message is over 150 characters long it will get truncated
28
+ # to 150 characters with a <tt>...</tt>
29
+ def alert=(message)
30
+ if !message.blank? && message.size > 150
31
+ message = truncate(message, :length => 150)
32
+ end
33
+ write_attribute('alert', message)
34
+ end
35
+
36
+ # Creates a Hash that will be the payload of an APN.
37
+ #
38
+ # Example:
39
+ # apn = APN::Notification.new
40
+ # apn.badge = 5
41
+ # apn.sound = 'my_sound.aiff'
42
+ # apn.alert = 'Hello!'
43
+ # apn.apple_hash # => {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}}
44
+ #
45
+ # Example 2:
46
+ # apn = APN::Notification.new
47
+ # apn.badge = 0
48
+ # apn.sound = true
49
+ # apn.custom_properties = {"typ" => 1}
50
+ # apn.apple_hash # => {"aps" => {"badge" => 0, "sound" => "1.aiff"}, "typ" => "1"}
51
+ def apple_hash
52
+ result = {}
53
+ result['aps'] = {}
54
+ result['aps']['alert'] = self.alert if self.alert
55
+ result['aps']['badge'] = self.badge.to_i if self.badge
56
+ if self.sound
57
+ result['aps']['sound'] = self.sound if self.sound.is_a? String
58
+ result['aps']['sound'] = "1.aiff" if self.sound.is_a?(TrueClass)
59
+ end
60
+ if self.custom_properties
61
+ self.custom_properties.each do |key,value|
62
+ result["#{key}"] = "#{value}"
63
+ end
64
+ end
65
+ result
66
+ end
67
+
68
+ # Creates the JSON string required for an APN message.
69
+ #
70
+ # Example:
71
+ # apn = APN::Notification.new
72
+ # apn.badge = 5
73
+ # apn.sound = 'my_sound.aiff'
74
+ # apn.alert = 'Hello!'
75
+ # apn.to_apple_json # => '{"aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}'
76
+ def to_apple_json
77
+ self.apple_hash.to_json
78
+ end
79
+
80
+ # Creates the binary message needed to send to Apple.
81
+ def message_for_sending
82
+ json = self.to_apple_json
83
+ message = "\0\0 #{self.device.to_hexa}\0#{json.length.chr}#{json}"
84
+ raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256
85
+ message
86
+ end
87
+
88
+ def self.send_notifications
89
+ ActiveSupport::Deprecation.warn("The method APN::Notification.send_notifications is deprecated. Use APN::App.send_notifications instead.")
90
+ APN::App.send_notifications
91
+ end
92
+
93
+ end # APN::Notification
@@ -0,0 +1,28 @@
1
+ class APN::PullNotification < APN::Base
2
+ belongs_to :app, :class_name => 'APN::App'
3
+
4
+ validates_presence_of :app_id
5
+
6
+ def self.latest_since(app_id, since_date=nil)
7
+ if since_date
8
+ res = first(:order => "created_at DESC",
9
+ :conditions => ["app_id = ? AND created_at > ? AND launch_notification = ?", app_id, since_date, false])
10
+ else
11
+ res = first(:order => "created_at DESC",
12
+ :conditions => ["app_id = ? AND launch_notification = ?", app_id, true])
13
+ res = first(:order => "created_at DESC",
14
+ :conditions => ["app_id = ? AND launch_notification = ?", app_id, false]) unless res
15
+ end
16
+ res
17
+ end
18
+
19
+ def self.all_since(app_id, since_date=nil)
20
+ if since_date
21
+ res = all(:order => "created_at DESC",
22
+ :conditions => ["app_id = ? AND created_at > ? AND launch_notification = ?", app_id, since_date, false])
23
+ else
24
+ res = all(:order => "created_at DESC",
25
+ :conditions => ["app_id = ? AND launch_notification = ?", app_id, false])
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,70 @@
1
+ module APN
2
+ module Connection
3
+
4
+ class << self
5
+
6
+ # Yields up an SSL socket to write notifications to.
7
+ # The connections are close automatically.
8
+ #
9
+ # Example:
10
+ # APN::Configuration.open_for_delivery do |conn|
11
+ # conn.write('my cool notification')
12
+ # end
13
+ #
14
+ # Configuration parameters are:
15
+ #
16
+ # configatron.apn.passphrase = ''
17
+ # configatron.apn.port = 2195
18
+ # configatron.apn.host = 'gateway.sandbox.push.apple.com' # Development
19
+ # configatron.apn.host = 'gateway.push.apple.com' # Production
20
+ # configatron.apn.cert = File.join(rails_root, 'config', 'apple_push_notification_development.pem')) # Development
21
+ # configatron.apn.cert = File.join(rails_root, 'config', 'apple_push_notification_production.pem')) # Production
22
+ def open_for_delivery(options = {}, &block)
23
+ open(options, &block)
24
+ end
25
+
26
+ # Yields up an SSL socket to receive feedback from.
27
+ # The connections are close automatically.
28
+ # Configuration parameters are:
29
+ #
30
+ # configatron.apn.feedback.passphrase = ''
31
+ # configatron.apn.feedback.port = 2196
32
+ # configatron.apn.feedback.host = 'feedback.sandbox.push.apple.com' # Development
33
+ # configatron.apn.feedback.host = 'feedback.push.apple.com' # Production
34
+ # configatron.apn.feedback.cert = File.join(rails_root, 'config', 'apple_push_notification_development.pem')) # Development
35
+ # configatron.apn.feedback.cert = File.join(rails_root, 'config', 'apple_push_notification_production.pem')) # Production
36
+ def open_for_feedback(options = {}, &block)
37
+ options = {:cert => configatron.apn.feedback.cert,
38
+ :passphrase => configatron.apn.feedback.passphrase,
39
+ :host => configatron.apn.feedback.host,
40
+ :port => configatron.apn.feedback.port}.merge(options)
41
+ open(options, &block)
42
+ end
43
+
44
+ private
45
+ def open(options = {}, &block) # :nodoc:
46
+ options = {:cert => configatron.apn.cert,
47
+ :passphrase => configatron.apn.passphrase,
48
+ :host => configatron.apn.host,
49
+ :port => configatron.apn.port}.merge(options)
50
+ #cert = File.read(options[:cert])
51
+ cert = options[:cert]
52
+ ctx = OpenSSL::SSL::SSLContext.new
53
+ ctx.key = OpenSSL::PKey::RSA.new(cert, options[:passphrase])
54
+ ctx.cert = OpenSSL::X509::Certificate.new(cert)
55
+
56
+ sock = TCPSocket.new(options[:host], options[:port])
57
+ ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
58
+ ssl.sync = true
59
+ ssl.connect
60
+
61
+ yield ssl, sock if block_given?
62
+ ensure
63
+ ssl.close
64
+ sock.close
65
+ end
66
+
67
+ end
68
+
69
+ end # Connection
70
+ end # APN
@@ -0,0 +1,39 @@
1
+ module APN
2
+ # Module for talking to the Apple Feedback Service.
3
+ # The service is meant to let you know when a device is no longer
4
+ # registered to receive notifications for your application.
5
+ module Feedback
6
+
7
+ class << self
8
+
9
+ # Returns an Array of APN::Device objects that
10
+ # has received feedback from Apple. Each APN::Device will
11
+ # have it's <tt>feedback_at</tt> accessor marked with the time
12
+ # that Apple believes the device de-registered itself.
13
+ def devices(cert, &block)
14
+ devices = []
15
+ return if cert.nil?
16
+ APN::Connection.open_for_feedback({:cert => cert}) do |conn, sock|
17
+ while line = conn.read(38) # Read 38 bytes from the SSL socket
18
+ feedback = line.unpack('N1n1H140')
19
+ token = feedback[2].scan(/.{0,8}/).join(' ').strip
20
+ device = APN::Device.find(:first, :conditions => {:token => token})
21
+ if device
22
+ device.feedback_at = Time.at(feedback[0])
23
+ devices << device
24
+ end
25
+ end
26
+ end
27
+ devices.each(&block) if block_given?
28
+ return devices
29
+ end # devices
30
+
31
+ def process_devices
32
+ ActiveSupport::Deprecation.warn("The method APN::Feedback.process_devices is deprecated. Use APN::App.process_devices instead.")
33
+ APN::App.process_devices
34
+ end
35
+
36
+ end # class << self
37
+
38
+ end # Feedback
39
+ end # APN
@@ -0,0 +1,30 @@
1
+ namespace :apn do
2
+
3
+ namespace :notifications do
4
+
5
+ desc "Deliver all unsent APN notifications."
6
+ task :deliver => [:environment] do
7
+ APN::App.send_notifications
8
+ end
9
+
10
+ end # notifications
11
+
12
+ namespace :group_notifications do
13
+
14
+ desc "Deliver all unsent APN Group notifications."
15
+ task :deliver => [:environment] do
16
+ APN::App.send_group_notifications
17
+ end
18
+
19
+ end # group_notifications
20
+
21
+ namespace :feedback do
22
+
23
+ desc "Process all devices that have feedback from APN."
24
+ task :process => [:environment] do
25
+ APN::App.process_devices
26
+ end
27
+
28
+ end
29
+
30
+ end # apn
@@ -0,0 +1,19 @@
1
+ namespace :apn do
2
+
3
+ namespace :db do
4
+
5
+ task :migrate do
6
+ puts %{
7
+ This task no longer exists. Please generate the migrations like this:
8
+
9
+ $ ruby script/generate apn_migrations
10
+
11
+ Then just run the migrations like you would normally:
12
+
13
+ $ rake db:migrate
14
+ }.strip
15
+ end
16
+
17
+ end # db
18
+
19
+ end # apn