mwotton-apnd 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,193 @@
1
+ # APND
2
+
3
+ APND (Apple Push Notification Daemon) is a ruby library to send Apple Push
4
+ Notifications to iPhones.
5
+
6
+ Apple recommends application developers create one connection to their
7
+ upstream push notification server, rather than creating one per notification.
8
+
9
+ APND acts as an intermediary between your application and Apple (see **APND
10
+ Daemon** below). Your application's notifications are queued to APND, which
11
+ are then sent to Apple over a single connection.
12
+
13
+ Within ruby applications, `APND::Notification` can be used to send
14
+ notifications to a running APND instance (see **APND Notification** below) or
15
+ directly to Apple. The command line can be used to send single notifications
16
+ for testing purposes (see **APND Client** below).
17
+
18
+
19
+ ## General Usage
20
+
21
+ ### APND Daemon
22
+
23
+ APND receives push notifications from your application and relays them to
24
+ Apple over a single connection as explained above. The `apnd` command line
25
+ utility is used to start APND.
26
+
27
+ Usage:
28
+ apnd daemon [OPTIONS] --apple-cert </path/to/cert>
29
+
30
+ Required Arguments:
31
+ --apple-cert [PATH] PATH to APN certificate from Apple
32
+
33
+ Optional Arguments:
34
+ --apple-host [HOST] Connect to Apple at HOST (default is gateway.sandbox.push.apple.com)
35
+ --apple-port [PORT] Connect to Apple on PORT (default is 2195)
36
+ --apple-cert-pass [PASSWORD] PASSWORD for APN certificate from Apple
37
+ --daemon-port [PORT] Run APND on PORT (default is 22195)
38
+ --daemon-bind [ADDRESS] Bind APND to ADDRESS (default is 0.0.0.0)
39
+ --daemon-log-file [PATH] PATH to APND log file (default is /var/log/apnd.log)
40
+ --daemon-timer [SECONDS] Set APND queue refresh time to SECONDS (default is 30)
41
+ --foreground Run APND in foreground without daemonizing
42
+
43
+ Help:
44
+ --help Show this message
45
+
46
+
47
+ ### APND Client
48
+
49
+ APND includes a command line client which can be used to send notifications to
50
+ a running APND instance. It is only recommended to send notifications via
51
+ `apnd push` for testing purposes.
52
+
53
+ Usage:
54
+ apnd push [OPTIONS] --token <token>
55
+
56
+ Required Arguments:
57
+ --token [TOKEN] Set Notification's iPhone token to TOKEN
58
+
59
+ Optional Arguments:
60
+ --alert [MESSAGE] Set Notification's alert to MESSAGE
61
+ --sound [SOUND] Set Notification's sound to SOUND
62
+ --badge [NUMBER] Set Notification's badge number to NUMBER
63
+ --custom [JSON] Set Notification's custom data to JSON
64
+ --host [HOST] Send Notification to HOST, usually the one running APND (default is 'localhost')
65
+ --port [PORT] Send Notification on PORT (default is 22195)
66
+
67
+ Help:
68
+ --help Show this message
69
+
70
+
71
+ ### APND Notification
72
+
73
+ The `APND::Notification` class can be used within your application to send
74
+ push notifications to APND.
75
+
76
+ require 'apnd'
77
+
78
+ # Set the host/port APND is running on
79
+ # (not needed if you're using localhost:22195)
80
+ # Put this in config/initializers/apnd.rb for Rails
81
+ APND::Notification.upstream_host = 'localhost'
82
+ APND::Notification.upstream_port = 22195
83
+
84
+
85
+ # Initialize some notifications
86
+ notification1 = APND::Notification.new(
87
+ :alert => 'Alert!',
88
+ :token => 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2',
89
+ :badge => 1
90
+ )
91
+
92
+ notification2 = APND::Notification.new(
93
+ :alert => 'Red Alert!',
94
+ :token => 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2',
95
+ :badge => 99
96
+ )
97
+
98
+
99
+ # Send multiple notifications at once to avoid overhead in
100
+ # opening/closing the upstream socket connection each time
101
+ #
102
+ # *IMPORTANT!* Use sock.puts as it appends a new line. If you don't,
103
+ # you'll only receive the first notification.
104
+
105
+ APND::Notification.open_upstream_socket do |sock|
106
+ sock.puts(notification1.to_bytes)
107
+ sock.puts(notification2.to_bytes)
108
+ end
109
+
110
+
111
+ # Send a notification to the upstream socket immediately
112
+ notification3 = APND::Notification.create(
113
+ :alert => 'Alert!',
114
+ :token => 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2',
115
+ :badge => 0
116
+ )
117
+
118
+
119
+ ### APND Feedback
120
+
121
+ Apple Push Notification Service keeps a log when you attempt to deliver
122
+ a notification to a device that has removed your application. A Feedback
123
+ Service is provided which applications should periodically check to remove
124
+ from their databases.
125
+
126
+ The `APND::Feedback` class can be used within your application to retrieve
127
+ a list of device tokens that you are sending notifications to but have
128
+ removed your application.
129
+
130
+ APND::Feedback.upstream_host = 'feedback.push.apple.com'
131
+ APND::Feedback.upstream_port = 2196
132
+
133
+ # Block form
134
+ APND::Feedback.find_stale_devices do |token, removed_at|
135
+ device = YourApp::Device.find_by_token(token)
136
+ unless device.registered_at > removed_at
137
+ device.push_enabled = 0
138
+ device.save
139
+ end
140
+ end
141
+
142
+ # Array form
143
+ stale = APND::Feedback.find_stale_devices
144
+ stale.each do |(token, removed_at)|
145
+ device = YourApp::Device.find_by_token(token)
146
+ unless device.registered_at > removed_at
147
+ device.push_enabled = 0
148
+ device.save
149
+ end
150
+ end
151
+
152
+
153
+ ## Prerequisites
154
+
155
+ You must have a valid Apple Push Notification Certificate for your iPhone
156
+ application. Obtain your APN certificate from the iPhone Provisioning Portal
157
+ at [developer.apple.com](http://developer.apple.com/).
158
+
159
+
160
+ ## Requirements
161
+
162
+ * [EventMachine](http://github.com/eventmachine/eventmachine)
163
+ * [Daemons](http://github.com/ghazel/daemons)
164
+ * [JSON](http://github.com/flori/json)
165
+
166
+ Ruby must be compiled with OpenSSL support.
167
+
168
+ Tests use [Shoulda](http://github.com/thoughtbot/shoulda), and optionally
169
+ [TURN](https://github.com/TwP/turn).
170
+
171
+
172
+ ## Installation
173
+
174
+ RubyGems:
175
+
176
+ gem install apnd
177
+
178
+ Git:
179
+
180
+ git clone git://github.com/itspriddle/apnd.git
181
+
182
+
183
+ ## Credit
184
+
185
+ APND is based on [apnserver](http://github.com/bpoweski/apnserver) and
186
+ [apn_on_rails](http://github.com/PRX/apn_on_rails). Either worked just how I
187
+ wanted, so I rolled my own using theirs as starting points. If APND doesn't
188
+ suit you, check them out instead.
189
+
190
+
191
+ ## Copyright
192
+
193
+ Copyright (c) 2010-2011 Joshua Priddle. See LICENSE for details.
@@ -0,0 +1,31 @@
1
+ $:.unshift 'lib'
2
+
3
+ task :default => :test
4
+
5
+ require 'rake/testtask'
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'lib' << 'test' << '.'
8
+ test.pattern = 'test/**/*_test.rb'
9
+ test.verbose = true
10
+ end
11
+
12
+ desc "Open an irb session preloaded with this library"
13
+ task :console do
14
+ sh "irb -rubygems -r ./lib/apnd.rb -I ./lib"
15
+ end
16
+
17
+ require 'sdoc_helpers'
18
+ desc "Push a new version to Gemcutter"
19
+ task :publish do
20
+ require 'apnd/version'
21
+
22
+ ver = APND::Version
23
+
24
+ sh "gem build apnd.gemspec"
25
+ sh "gem push mwotton-apnd-#{ver}.gem"
26
+ sh "git tag -a -m 'APND v#{ver}' v#{ver}"
27
+ sh "git push origin v#{ver}"
28
+ sh "git push origin master"
29
+ sh "git clean -fd"
30
+ sh "rake pages"
31
+ end
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ARGV << '--help' if ARGV.empty?
4
+
5
+ if $0 == __FILE__
6
+ require 'rubygems'
7
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
8
+ end
9
+
10
+ require 'apnd'
11
+
12
+ command = ARGV.shift
13
+
14
+ case command
15
+ when 'daemon'
16
+ APND::CLI.daemon(ARGV)
17
+ when 'push'
18
+ APND::CLI.push(ARGV)
19
+ when '--version', '-v'
20
+ puts "APND v#{APND::Version}"
21
+ else
22
+ puts "Error: Invalid command" unless %w(-h --help).include?(command)
23
+ puts <<-HELP
24
+ Usage: apnd COMMAND [ARGS]
25
+
26
+ Command list:
27
+ daemon Start the APND Daemon
28
+ push Send a single push notification (for development use only)
29
+
30
+ Help:
31
+ --version Show version
32
+ --help Show this message
33
+
34
+ HELP
35
+ end
@@ -0,0 +1,33 @@
1
+ require 'json'
2
+
3
+ module APND
4
+ autoload :Version, 'apnd/version'
5
+ autoload :CLI, 'apnd/cli'
6
+ autoload :Errors, 'apnd/errors'
7
+ autoload :Settings, 'apnd/settings'
8
+ autoload :Daemon, 'apnd/daemon'
9
+ autoload :Notification, 'apnd/notification'
10
+ autoload :Feedback, 'apnd/feedback'
11
+
12
+ #
13
+ # Returns APND::Settings
14
+ #
15
+ def self.settings
16
+ @@settings ||= Settings.new
17
+ end
18
+
19
+ #
20
+ # Yields APND::Settings
21
+ #
22
+ def self.configure
23
+ yield settings
24
+ end
25
+
26
+ #
27
+ # Write message to stdout with date
28
+ #
29
+ def self.logger(message) #:nodoc:
30
+ puts "[%s] %s" % [Time.now.strftime("%Y-%m-%d %H:%M:%S"), message]
31
+ end
32
+
33
+ end
@@ -0,0 +1,193 @@
1
+ require 'socket'
2
+
3
+ module APND
4
+ #
5
+ # APND::Notification is the base class for creating new push notifications.
6
+ #
7
+ class Notification
8
+
9
+ class << self
10
+ #
11
+ # The host notifications will be written to, usually one
12
+ # running APND
13
+ #
14
+ attr_accessor :upstream_host
15
+
16
+ #
17
+ # The port to connect to upstream_host on
18
+ #
19
+ attr_accessor :upstream_port
20
+ end
21
+
22
+ #
23
+ # Set upstream host/port to default values
24
+ #
25
+ self.upstream_host = APND.settings.notification.host
26
+ self.upstream_port = APND.settings.notification.port.to_i
27
+
28
+ #
29
+ # The device token from Apple
30
+ #
31
+ attr_accessor :token
32
+
33
+ #
34
+ # The alert to send
35
+ #
36
+ attr_accessor :alert
37
+
38
+ #
39
+ # The badge number to set
40
+ #
41
+ attr_accessor :badge
42
+
43
+ #
44
+ # The sound to play
45
+ #
46
+ attr_accessor :sound
47
+
48
+ #
49
+ # Custom data to send
50
+ #
51
+ attr_accessor :custom
52
+
53
+ #
54
+ # Creates a new socket to upstream_host:upstream_port
55
+ #
56
+ def self.upstream_socket
57
+ @socket = TCPSocket.new(upstream_host, upstream_port)
58
+ end
59
+
60
+ #
61
+ # Opens a new socket upstream, yields it, and closes it
62
+ #
63
+ def self.open_upstream_socket(&block)
64
+ socket = upstream_socket
65
+ yield socket
66
+ socket.close
67
+ end
68
+
69
+ #
70
+ # Create a new APN
71
+ #
72
+ def self.create(params = {}, push = true)
73
+ notification = Notification.new(params)
74
+ notification.push! if push
75
+ notification
76
+ end
77
+
78
+ #
79
+ # Try to create a new Notification from raw data
80
+ # Used by Daemon::Protocol to validate incoming data
81
+ #
82
+ def self.valid?(data)
83
+ parse(data)
84
+ rescue => e
85
+ puts e.inspect
86
+ puts e.backtrace
87
+ false
88
+ end
89
+
90
+
91
+ def self.parse_from_handle(handle)
92
+
93
+ end
94
+ #
95
+ # Parse raw data into a new Notification
96
+ #
97
+ def self.parse(data)
98
+ buffer = data.dup
99
+ notification = Notification.new
100
+
101
+ header = buffer.slice!(0, 3).unpack('ccc')
102
+
103
+ if header[0] != 0
104
+ raise APND::Errors::InvalidNotificationHeader.new(header)
105
+ end
106
+
107
+ notification.token = buffer.slice!(0, 32).unpack('H*').first
108
+
109
+ json_length = buffer.slice!(0, 2).unpack('CC')
110
+
111
+ json = buffer.slice!(0, json_length.last)
112
+
113
+ payload = JSON.parse(json)
114
+
115
+ %w[alert sound badge].each do |key|
116
+ if payload['aps'] && payload['aps'][key]
117
+ notification.send("#{key}=", payload['aps'][key])
118
+ end
119
+ end
120
+
121
+ payload.delete('aps')
122
+
123
+ unless payload.empty?
124
+ notification.custom = payload
125
+ end
126
+
127
+ notification
128
+ end
129
+
130
+ #
131
+ # Create a new Notification object from a hash
132
+ #
133
+ def initialize(params = {})
134
+ @token = params[:token]
135
+ @alert = params[:alert]
136
+ @badge = params[:badge]
137
+ @sound = params[:sound]
138
+ @custom = params[:custom]
139
+ end
140
+
141
+ #
142
+ # Token in hex format
143
+ #
144
+ def hex_token
145
+ [self.token.delete(' ')].pack('H*')
146
+ end
147
+
148
+ #
149
+ # aps hash sent to Apple
150
+ #
151
+ def aps
152
+ aps = {}
153
+ aps['alert'] = self.alert if self.alert
154
+ aps['badge'] = self.badge.to_i if self.badge
155
+ aps['sound'] = self.sound if self.sound
156
+
157
+ output = { 'aps' => aps }
158
+
159
+ if self.custom
160
+ self.custom.each do |key, value|
161
+ output[key.to_s] = value
162
+ end
163
+ end
164
+ output
165
+ end
166
+
167
+ #
168
+ # Pushes notification to upstream host:port (default is localhost:22195)
169
+ #
170
+ def push!
171
+ self.class.open_upstream_socket { |sock| sock.write(to_bytes) }
172
+ end
173
+
174
+ #
175
+ # Returns the Notification's aps hash as json
176
+ #
177
+ def aps_json
178
+ return @aps_json if @aps_json
179
+ json = aps.to_json
180
+ raise APND::Errors::InvalidPayload.new(json) if json.size > 256
181
+ @aps_json = json
182
+ end
183
+
184
+ #
185
+ # Format the notification as a string for submission
186
+ # to Apple
187
+ #
188
+ def to_bytes
189
+ @bytes ||= "\0\0 %s\0%s%s" % [hex_token, aps_json.length.chr, aps_json]
190
+ end
191
+
192
+ end
193
+ end