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,65 @@
1
+ module APND
2
+ #
3
+ # APND::Feedback receives feedback from Apple when notifications are
4
+ # being rejected for a specific token. This is usually due to the user
5
+ # uninstalling your application.
6
+ #
7
+ class Feedback
8
+
9
+ class << self
10
+ #
11
+ # The host to receive feedback from, usually apple
12
+ #
13
+ attr_accessor :upstream_host
14
+
15
+ #
16
+ # The port to connect to upstream_host on
17
+ #
18
+ attr_accessor :upstream_port
19
+ end
20
+
21
+ #
22
+ # Set upstream host/port to default values
23
+ #
24
+ self.upstream_host = APND.settings.feedback.host
25
+ self.upstream_port = APND.settings.feedback.port.to_i
26
+
27
+ #
28
+ # Connect to Apple's Feedback Service and return an array of device
29
+ # tokens that should no longer receive push notifications
30
+ #
31
+ def self.find_stale_devices(&block)
32
+ feedback = self.new
33
+ devices = feedback.receive
34
+ devices.each { |device| yield *device } if block_given?
35
+ devices
36
+ end
37
+
38
+ #
39
+ # Create a connection to Apple's Feedback Service
40
+ #
41
+ def initialize
42
+ @apple = APND::Daemon::AppleConnection.new({
43
+ :cert => APND.settings.apple.cert,
44
+ :cert_pass => APND.settings.apple.cert_pass,
45
+ :host => self.class.upstream_host,
46
+ :port => self.class.upstream_port.to_i
47
+ })
48
+ end
49
+
50
+ #
51
+ # Receive feedback from Apple and return an array of device tokens
52
+ #
53
+ def receive
54
+ tokens = []
55
+ @apple.open do |sock|
56
+ while line = sock.gets
57
+ payload = line.strip.unpack('N1n1H140')
58
+ tokens << [payload[2].strip, Time.at(payload[0])]
59
+ end
60
+ end
61
+ tokens
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,189 @@
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
+ # Parse raw data into a new Notification
92
+ #
93
+ def self.parse(data)
94
+ buffer = data.dup
95
+ notification = Notification.new
96
+
97
+ header = buffer.slice!(0, 3).unpack('ccc')
98
+
99
+ if header[0] != 0
100
+ raise APND::Errors::InvalidNotificationHeader.new(header)
101
+ end
102
+
103
+ notification.token = buffer.slice!(0, 32).unpack('H*').first
104
+
105
+ json_length = buffer.slice!(0, 2).unpack('CC')
106
+
107
+ json = buffer.slice!(0, json_length.last)
108
+
109
+ payload = JSON.parse(json)
110
+
111
+ %w[alert sound badge].each do |key|
112
+ if payload['aps'] && payload['aps'][key]
113
+ notification.send("#{key}=", payload['aps'][key])
114
+ end
115
+ end
116
+
117
+ payload.delete('aps')
118
+
119
+ unless payload.empty?
120
+ notification.custom = payload
121
+ end
122
+
123
+ notification
124
+ end
125
+
126
+ #
127
+ # Create a new Notification object from a hash
128
+ #
129
+ def initialize(params = {})
130
+ @token = params[:token]
131
+ @alert = params[:alert]
132
+ @badge = params[:badge]
133
+ @sound = params[:sound]
134
+ @custom = params[:custom]
135
+ end
136
+
137
+ #
138
+ # Token in hex format
139
+ #
140
+ def hex_token
141
+ [self.token.delete(' ')].pack('H*')
142
+ end
143
+
144
+ #
145
+ # aps hash sent to Apple
146
+ #
147
+ def aps
148
+ aps = {}
149
+ aps['alert'] = self.alert if self.alert
150
+ aps['badge'] = self.badge.to_i if self.badge
151
+ aps['sound'] = self.sound if self.sound
152
+
153
+ output = { 'aps' => aps }
154
+
155
+ if self.custom
156
+ self.custom.each do |key, value|
157
+ output[key.to_s] = value
158
+ end
159
+ end
160
+ output
161
+ end
162
+
163
+ #
164
+ # Pushes notification to upstream host:port (default is localhost:22195)
165
+ #
166
+ def push!
167
+ self.class.open_upstream_socket { |sock| sock.write(to_bytes) }
168
+ end
169
+
170
+ #
171
+ # Returns the Notification's aps hash as json
172
+ #
173
+ def aps_json
174
+ return @aps_json if @aps_json
175
+ json = aps.to_json
176
+ raise APND::Errors::InvalidPayload.new(json) if json.size > 256
177
+ @aps_json = json
178
+ end
179
+
180
+ #
181
+ # Format the notification as a string for submission
182
+ # to Apple
183
+ #
184
+ def to_bytes
185
+ @bytes ||= "\0\0 %s\0%s%s" % [hex_token, aps_json.length.chr, aps_json]
186
+ end
187
+
188
+ end
189
+ end
@@ -0,0 +1,205 @@
1
+ module APND
2
+ #
3
+ # Settings for APND
4
+ #
5
+ class Settings
6
+
7
+ #
8
+ # Settings for APND::Daemon::AppleConnection
9
+ #
10
+ class AppleConnection
11
+
12
+ #
13
+ # Host used to connect to Apple
14
+ #
15
+ # Development: gateway.sandbox.push.apple.com
16
+ # Production: gateway.push.apple.com
17
+ #
18
+ attr_accessor :host
19
+
20
+ #
21
+ # Port used to connect to Apple
22
+ #
23
+ attr_accessor :port
24
+
25
+ #
26
+ # Path to APN cert for your application
27
+ #
28
+ attr_accessor :cert
29
+
30
+ #
31
+ # Password for APN cert, optional
32
+ #
33
+ attr_accessor :cert_pass
34
+
35
+ def initialize
36
+ @host = 'gateway.sandbox.push.apple.com'
37
+ @port = 2195
38
+ end
39
+ end
40
+
41
+ #
42
+ # Settings for APND::Daemon
43
+ #
44
+ class Daemon
45
+
46
+ #
47
+ # IP to bind APND::Daemon to
48
+ #
49
+ # Default: '0.0.0.0'
50
+ #
51
+ attr_accessor :bind
52
+
53
+ #
54
+ # Port APND::Daemon will run on
55
+ #
56
+ # Default: 22195
57
+ #
58
+ attr_accessor :port
59
+
60
+ #
61
+ # Path to APND::Daemon log
62
+ #
63
+ # Default: /var/log/apnd.log
64
+ #
65
+ attr_accessor :log_file
66
+
67
+ #
68
+ # Interval (in seconds) the queue will be processed
69
+ #
70
+ # Default: 30
71
+ #
72
+ attr_accessor :timer
73
+
74
+ def initialize
75
+ @timer = 30
76
+ @bind = '0.0.0.0'
77
+ @port = 22195
78
+ @log_file = '/var/log/apnd.log'
79
+ end
80
+ end
81
+
82
+ #
83
+ # Settings for APND::Notification
84
+ #
85
+ class Notification
86
+
87
+ #
88
+ # Host to send notification to, usually the one running APND::Daemon
89
+ #
90
+ # Default: localhost
91
+ #
92
+ attr_accessor :host
93
+
94
+ #
95
+ # Port to send notifications to
96
+ #
97
+ # Default: 22195
98
+ #
99
+ attr_accessor :port
100
+
101
+ def initialize
102
+ @host = 'localhost'
103
+ @port = 22195
104
+ end
105
+ end
106
+
107
+ #
108
+ # Settings for APND::Feedback
109
+ #
110
+ class Feedback
111
+
112
+ #
113
+ # Host used to connect to Apple
114
+ #
115
+ # Development: feedback.sandbox.push.apple.com
116
+ # Production: feedback.push.apple.com
117
+ #
118
+ attr_accessor :host
119
+
120
+ #
121
+ # Port used to connect to Apple
122
+ #
123
+ # Default: 2196
124
+ #
125
+ attr_accessor :port
126
+
127
+ def initialize
128
+ @host = 'feedback.sandbox.push.apple.com'
129
+ @port = 2196
130
+ end
131
+ end
132
+
133
+ #
134
+ # Returns the AppleConnection settings
135
+ #
136
+ def apple
137
+ @apple ||= APND::Settings::AppleConnection.new
138
+ end
139
+
140
+ #
141
+ # Mass assign AppleConnection settings
142
+ #
143
+ def apple=(options = {})
144
+ if options.respond_to?(:keys)
145
+ apple.cert = options[:cert] if options[:cert]
146
+ apple.cert_pass = options[:cert_pass] if options[:cert_pass]
147
+ apple.host = options[:host] if options[:host]
148
+ apple.port = options[:port] if options[:port]
149
+ end
150
+ end
151
+
152
+ #
153
+ # Returns the Daemon settings
154
+ #
155
+ def daemon
156
+ @daemon ||= APND::Settings::Daemon.new
157
+ end
158
+
159
+ #
160
+ # Mass assign Daemon settings
161
+ #
162
+ def daemon=(options = {})
163
+ if options.respond_to?(:keys)
164
+ daemon.bind = options[:bind] if options[:bind]
165
+ daemon.port = options[:port] if options[:port]
166
+ daemon.log_file = options[:log_file] if options[:log_file]
167
+ daemon.timer = options[:timer] if options[:timer]
168
+ end
169
+ end
170
+
171
+ #
172
+ # Returns the Notification settings
173
+ #
174
+ def notification
175
+ @notification ||= APND::Settings::Notification.new
176
+ end
177
+
178
+ #
179
+ # Mass assign Notification settings
180
+ #
181
+ def notification=(options = {})
182
+ if options.respond_to?(:keys)
183
+ notification.port = options[:port] if options[:port]
184
+ notification.host = options[:host] if options[:host]
185
+ end
186
+ end
187
+
188
+ #
189
+ # Returns the Feedback settings
190
+ #
191
+ def feedback
192
+ @feedback ||= APND::Settings::Feedback.new
193
+ end
194
+
195
+ #
196
+ # Mass assign Feedback settings
197
+ #
198
+ def feedback=(options = {})
199
+ if options.respond_to?(:keys)
200
+ feedback.port = options[:port] if options[:port]
201
+ feedback.host = options[:host] if options[:host]
202
+ end
203
+ end
204
+ end
205
+ end