mwotton-apnd 0.1.8

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.
@@ -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