kw_apn 0.2

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/README.rdoc ADDED
@@ -0,0 +1,51 @@
1
+ ====================================
2
+ Getting Started
3
+ ====================================
4
+ For Rails start the generator: kw_apn
5
+ This will copy the configuration example into your project.
6
+
7
+ If you not working with Rails you need to specify either Rails.root and Rails.env or RACK_ROOT and RACK_ENV for the gem to work.
8
+
9
+ Manual:
10
+ create the File
11
+ <project_root>/config/kw_apn.yml
12
+
13
+ structure of the config file should look like this:
14
+
15
+
16
+ environment:
17
+ cert_file: path to ssl certificate file
18
+ push_host: 'gateway.sandbox.push.apple.com' or 'gateway.push.apple.com' for live
19
+ push_port: 2195
20
+ feedback_host: 'feedback.sandbox.push.apple.com' or 'feedback.push.apple.com' for live
21
+ feedback_port: 2196
22
+
23
+
24
+ ====================================
25
+ Example
26
+ ====================================
27
+
28
+ users = MyAPNUsers.all
29
+ n = []
30
+ payload = {:aps => {:alert => "Something very important for everyone to read", :sound => 'annoying_beep'}}
31
+
32
+ users.each do |u|
33
+ n << KwAPN::Notification.create(u.token, payload, 0)
34
+ end
35
+
36
+ status, ret = KwAPN::Sender.push(n, 'TestSession')
37
+
38
+ if status == :ok
39
+ ret.each do |token|
40
+ MyAPNUsers.delete_all('token'=>token)
41
+ end
42
+ end
43
+
44
+
45
+ ====================================
46
+ Copyright
47
+ ====================================
48
+
49
+ Distributed under the MIT License.
50
+ Based in part on Apns4r by Leonid Ponomarev (http://rdoc.info/projects/thegeekbird/Apns4r)
51
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ task :gem do
5
+ `gem build *.gemspec`
6
+ end
7
+
8
+ task :install do
9
+ Rake::Task[:gem].invoke
10
+ `sudo gem install *.gem`
11
+ Rake::Task[:cleanup].invoke
12
+ end
13
+
14
+ task :cleanup do
15
+ `rm *.gem`
16
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2
data/kw_apn.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ version = File.read('VERSION')
2
+
3
+ File.open(File.join(File.dirname(__FILE__), 'VERSION'), 'w') do |f|
4
+ f.write(version)
5
+ end
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'kw_apn'
9
+ s.version = version
10
+ s.authors = ['Jonathan Cichon']
11
+ s.email = 'cichon@kupferwerk.com'
12
+ s.homepage = 'http://kupferwerk.com'
13
+ s.summary = 'APN Lib by Kupferwerk'
14
+ s.description = 'Apple Push Notification Library by Kupferwerk'
15
+ s.has_rdoc = true
16
+ s.extra_rdoc_files = ['README.rdoc']
17
+ s.require_path = 'lib'
18
+ s.files = Dir['lib/**/*'] + Dir['*.gemspec'] + ['Rakefile', 'README.rdoc', 'VERSION']
19
+ end
data/lib/config.rb ADDED
@@ -0,0 +1,40 @@
1
+ module KwAPN
2
+ require 'erb'
3
+ class Config
4
+ class << self
5
+ def options
6
+ @@options ||= nil
7
+ unless @@options
8
+ p_root = if defined? Rails
9
+ Rails.root
10
+ elsif defined? RACK_ROOT
11
+ RACK_ROOT
12
+ else
13
+ puts "Warning (KwAPN): You need to specifiy either Rails.root or RACK_ROOT for apns to work!"
14
+ nil
15
+ end
16
+
17
+ p_env = if defined? Rails
18
+ Rails.env
19
+ elsif defined? RACK_ENV
20
+ RACK_ENV
21
+ else
22
+ puts "Warning (KwAPN): You need to specifiy either Rails.env or RACK_ENV for apns to work!"
23
+ nil
24
+ end
25
+
26
+ @@options = begin
27
+ raw_config = File.read(p_root.join("config", "kw_apn.yml"))
28
+ parsed_config = ERB.new(raw_config).result
29
+ YAML.load(parsed_config)[p_env].symbolize_keys
30
+ rescue => e
31
+ puts "Warning (KwAPN): Could not parse config file: #{e.message}"
32
+ {}
33
+ end
34
+ @@options[:root] = p_root
35
+ end
36
+ return @@options
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/connection.rb ADDED
@@ -0,0 +1,30 @@
1
+ module KwAPN
2
+
3
+ require 'socket'
4
+ require 'openssl'
5
+
6
+ class Connection
7
+
8
+ def connect(host, port, opts)
9
+ ctx = OpenSSL::SSL::SSLContext.new()
10
+ ctx.cert = OpenSSL::X509::Certificate.new(File::read(opts[:cert_file]))
11
+ ctx.key = OpenSSL::PKey::RSA.new(File::read(opts[:cert_file]))
12
+
13
+ s = TCPSocket.new(host, port)
14
+ ssl = OpenSSL::SSL::SSLSocket.new(s, ctx)
15
+ ssl.connect # start SSL session
16
+ ssl.sync_close = true # close underlying socket on SSLSocket#close
17
+ ssl
18
+ end
19
+
20
+ class << self
21
+ def log(s)
22
+ File.open(KwAPN::Config.options[:root].join("log", "kw_apn.log"), File::WRONLY|File::APPEND|File::CREAT, 0666) do |f|
23
+ f.write("#{s}\n")
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ end
data/lib/core.rb ADDED
@@ -0,0 +1,66 @@
1
+ begin
2
+ require 'json'
3
+ rescue
4
+ puts "Warning: you need the json gem for apns to work!"
5
+ end
6
+
7
+ class Hash
8
+ MAX_PAYLOAD_LEN = 256
9
+
10
+ # Converts hash into JSON String.
11
+ # When payload is too long but can be chopped, tries to cut self.[:aps][:alert].
12
+ # If payload still don't fit Apple's restrictions, returns nil
13
+ #
14
+ # @return [String, nil] the object converted into JSON or nil.
15
+ def to_apn_payload
16
+ # Payload too long
17
+ if (to_json.length > MAX_PAYLOAD_LEN)
18
+ alert = self[:aps][:alert]
19
+ self[:aps][:alert] = ''
20
+ # can be chopped?
21
+ if (to_json.length > MAX_PAYLOAD_LEN)
22
+ return nil
23
+ else # inefficient way, but payload may be full of unicode-escaped chars, so...
24
+ self[:aps][:alert] = alert
25
+ while (self.to_json.length > MAX_PAYLOAD_LEN)
26
+ self[:aps][:alert].chop!
27
+ end
28
+ end
29
+ end
30
+ to_json
31
+ end
32
+
33
+ # Invokes {Hash#to_payload} and returns it's length
34
+ # @return [Fixnum, nil] length of object converted into JSON or nil.
35
+ def apn_payload_length
36
+ p = to_apn_payload
37
+ p ? p.length : nil
38
+ end
39
+
40
+ end
41
+
42
+ module KwAPN
43
+
44
+ class Notification
45
+
46
+ attr_accessor :identifier, :token
47
+ def initialize(token, payload, timestamp=0)
48
+ @token, @payload, @timestamp = token, payload, timestamp
49
+ end
50
+
51
+ # Creates new notification with given token and payload
52
+ # @param [String, Fixnum] token APNs token of device to notify
53
+ # @param [Hash, String] payload attached payload
54
+ def Notification.create(token, payload, timestamp=0)
55
+ Notification.new(token.kind_of?(String) ? token.delete(' ') : token.to_s(16) , payload.kind_of?(Hash) ? payload.to_apn_payload : payload, timestamp)
56
+ end
57
+
58
+ # Converts to binary string wich can be writen directly into socket
59
+ # @return [String] binary string representation
60
+ def to_s
61
+ [1, @identifier, @timestamp, 32, @token, @payload.length, @payload].pack("CNNnH*na*")
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,26 @@
1
+ module KwAPN
2
+ class FeedbackReader < Connection
3
+
4
+ attr_accessor :host, :port
5
+ def initialize(host=nil, port=nil)
6
+ @host = host || KwAPN::Config.options[:feedback_host]
7
+ @port = port || KwAPN::Config.options[:feedback_port]
8
+ end
9
+
10
+ def read
11
+ records ||= []
12
+ begin
13
+ @ssl = connect(@host, @port, KwAPN::Config.options)
14
+ while record = @ssl.read(38)
15
+ feedback = record.strip.unpack('NnH*')
16
+ records << feedback[2].scan(/.{0,8}/).join(' ').strip
17
+ end
18
+ rescue => e
19
+ puts "Error reading feedback channel: #{e.message}"
20
+ ensure
21
+ @ssl.close
22
+ end
23
+ return records
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ require 'rails/generators'
2
+
3
+ class KwApnGenerator < Rails::Generators::Base
4
+
5
+ def install_kw_apn
6
+ source_paths << File.join(File.dirname(__FILE__), 'templates')
7
+
8
+ copy_file "config/kw_apn.yml"
9
+ directory "config/cert"
10
+ directory "log"
11
+ copy_file "log/kw_apn.log"
12
+
13
+ gem 'kw_apn'
14
+ end
15
+
16
+
17
+
18
+ end
@@ -0,0 +1,18 @@
1
+ defaults: &defaults
2
+ cert_file: <%= Rails.root.join("config/cert", "apn_#{Rails.env}_cert.pem") %>
3
+ push_host: 'gateway.sandbox.push.apple.com'
4
+ push_port: 2195
5
+ feedback_host: 'feedback.sandbox.push.apple.com'
6
+ feedback_port: 2196
7
+
8
+ development:
9
+ <<: *defaults
10
+ profile: true
11
+
12
+ test:
13
+ <<: *defaults
14
+
15
+ production:
16
+ <<: *defaults
17
+ push_host: 'gateway.push.apple.com'
18
+ feedback_host: 'feedback.push.apple.com'
File without changes
@@ -0,0 +1 @@
1
+ Installs config templates to get you started with your project
data/lib/kw_apn.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'config'
2
+ require 'connection'
3
+ require 'sender'
4
+ require 'feedback_reader'
5
+ require 'core'
data/lib/sender.rb ADDED
@@ -0,0 +1,145 @@
1
+ module KwAPN
2
+
3
+ # maybe somebody knows why but Apple seems to have dificulties handling identifiers between 9 and ~ 20
4
+ # if there is a bug on our side feel free to fix it, but for now it seems to work with the offset workaround
5
+ ID_OFFSET = 333
6
+ class Sender < Connection
7
+ attr_accessor :host, :port, :count, :fail_count, :work_thread, :watch_thread, :failed_index_array, :session_id
8
+
9
+ # Creates new {Sender} object with given host and port
10
+ def initialize(session_id, host=nil, port=nil)
11
+ @session_id = session_id
12
+ @host = host || KwAPN::Config.options[:push_host] || 'gateway.sandbox.push.apple.com'
13
+ @port = port || KwAPN::Config.options[:push_port] || 2195
14
+ @count = 0
15
+ @fail_count = 0
16
+ @failed_index_array = []
17
+ self
18
+ end
19
+
20
+ def push_batch(notifications=[])
21
+ begin
22
+ if @ssl != nil
23
+ return [:nok, 'Start a new Connection for every batch you want to perform']
24
+ end
25
+ @count = notifications.length
26
+ start_threads(notifications)
27
+ return [:ok, @failed_index_array.collect{|a| notifications[a].token}]
28
+ rescue => e
29
+ failed
30
+ self.class.log("(#{session_id}) Exception: #{e.message}")
31
+ return [:nok, "Exception: #{e.message}"]
32
+ end
33
+ end
34
+
35
+ def close_connection
36
+ @ssl.close if @ssl
37
+ @ssl = nil
38
+ end
39
+
40
+ private
41
+
42
+ def start_threads(notifications, index=0)
43
+ @ssl = connect(@host, @port, KwAPN::Config.options)
44
+ if @ssl
45
+ @watch_thread = Thread.new do
46
+ perform_watch()
47
+ end
48
+
49
+ @work_thread = Thread.new do
50
+ perform_batch(notifications, index)
51
+ end
52
+
53
+ @work_thread.join
54
+
55
+ if @failed_index_array.last and index <= @failed_index_array.last and @failed_index_array.last < @count - 1 and @ssl.nil?
56
+ # wait for apple to respond errors
57
+ # sleep(1)
58
+ start_threads(notifications, @failed_index_array.last + 1)
59
+ end
60
+ else
61
+ failed
62
+ end
63
+ end
64
+
65
+ def perform_batch(notifications, index=0)
66
+ notifications[index..-1].each_with_index do |n, i|
67
+ begin
68
+ n.identifier = i + index + ID_OFFSET
69
+ bytes = @ssl.write(n.to_s)
70
+ if bytes <= 0
71
+ self.class.log("(#{session_id}) Warning at index #{i+index}: could not write to Socket")
72
+ # TODO?
73
+ # we do not realy want to respond to network errors, as we do not know how many apns might have been lost.
74
+ # At the moment we hope the watchthread does everything right and our connection holds.
75
+ end
76
+ rescue => e
77
+ # probably interrupted by watchthread, do nothing wait for restart
78
+ self.class.log("(#{session_id}) Exception at index #{i+index}: #{e.message}")
79
+
80
+ #if e.message == 'Broken pipe'
81
+ #end
82
+ end
83
+ end
84
+ # wait for apple to respond errors
85
+ sleep(5)
86
+ end
87
+
88
+ def perform_watch
89
+ ret = @ssl.read
90
+ err = ret.strip.unpack('CCN')
91
+ if err[1] != 0 and err[2]
92
+ @failed_index_array << (err[2] - ID_OFFSET)
93
+ failed
94
+ @work_thread.exit
95
+ else
96
+ perform_watch
97
+ end
98
+ end
99
+
100
+ def failed
101
+ @fail_count += 1
102
+ close_connection
103
+ end
104
+
105
+ def error_msg(status)
106
+ case status
107
+ when 1
108
+ "Processing error"
109
+ when 2
110
+ "Missing device token"
111
+ when 3
112
+ "Missing topic"
113
+ when 4
114
+ "Missing payload"
115
+ when 5
116
+ "Invalid token size"
117
+ when 6
118
+ "Invalid topic size"
119
+ when 7
120
+ "Invalid payload size"
121
+ when 8
122
+ "Invalid token"
123
+ when 255
124
+ "unknown"
125
+ end
126
+ end
127
+
128
+ class << self
129
+ # convenient way of connecting with apple and pushing the notifications
130
+ # @param [Array] notifications - An Array of Objects of Type KwAPN::Notification
131
+ # @param [String] session_id - A Identifier for Login purpose
132
+ #
133
+ # @returns [Symbol, Array/String] if no Problems occured :ok and an Array of Tokens failed to push is returned. The Caller should take care of those invalid Tokens.
134
+ def push(notifications, session_id=nil)
135
+ s = self.new(session_id)
136
+ startdate = Time.now
137
+ status, ret = s.push_batch(notifications)
138
+ log("(#{session_id}) #{startdate.to_s} SENT APN #{s.count - s.fail_count}/#{s.count} in #{Time.now.to_i - startdate.to_i} seconds")
139
+ s.close_connection
140
+ return [status, ret]
141
+ end
142
+ end
143
+ end
144
+ end
145
+
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kw_apn
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ version: "0.2"
10
+ platform: ruby
11
+ authors:
12
+ - Jonathan Cichon
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-08-05 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Apple Push Notification Library by Kupferwerk
22
+ email: cichon@kupferwerk.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.rdoc
29
+ files:
30
+ - lib/config.rb
31
+ - lib/connection.rb
32
+ - lib/core.rb
33
+ - lib/feedback_reader.rb
34
+ - lib/generators/kw_apn/kw_apn_generator.rb
35
+ - lib/generators/kw_apn/templates/config/cert/dummy.pem
36
+ - lib/generators/kw_apn/templates/config/kw_apn.yml
37
+ - lib/generators/kw_apn/templates/log/kw_apn.log
38
+ - lib/generators/kw_apn/usage
39
+ - lib/kw_apn.rb
40
+ - lib/sender.rb
41
+ - kw_apn.gemspec
42
+ - Rakefile
43
+ - README.rdoc
44
+ - VERSION
45
+ has_rdoc: true
46
+ homepage: http://kupferwerk.com
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.7
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: APN Lib by Kupferwerk
79
+ test_files: []
80
+