groem 0.0.4

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,169 @@
1
+ require 'eventmachine'
2
+
3
+ module Groem
4
+ class Client < EM::Connection
5
+ include Groem::Constants
6
+ include EM::Deferrable
7
+
8
+ DEFAULT_HOST = 'localhost'
9
+ DEFAULT_PORT = 23053
10
+
11
+ class << self
12
+ def response_class
13
+ @response_class ||= anonymous_response_class
14
+ end
15
+
16
+ def response_class=(klass)
17
+ @response_class = klass
18
+ end
19
+ alias_method :load_response_as, :response_class=
20
+
21
+ def request(request, host = DEFAULT_HOST, port = DEFAULT_PORT)
22
+ connection = EM.connect host, port, self, request
23
+ connection
24
+ end
25
+ alias_method :register, :request
26
+ alias_method :notify, :request
27
+
28
+ def anonymous_response_class
29
+ @klass_resp ||= \
30
+ Class.new {
31
+ include(Groem::Marshal::Response)
32
+ require 'forwardable'
33
+ extend Forwardable
34
+ def_delegators :@raw, :[], :[]=
35
+ def raw; @raw ||= {}; end
36
+ def initialize(input = {})
37
+ @raw = input
38
+ end
39
+ }
40
+ end
41
+
42
+ def anonymous_request_class
43
+ @klass_req ||= \
44
+ Class.new {
45
+ include(Groem::Marshal::Request)
46
+ require 'forwardable'
47
+ extend Forwardable
48
+ def_delegators :@raw, :[], :[]=
49
+ def raw; @raw ||= {}; end
50
+ def initialize(input = {})
51
+ @raw = input
52
+ end
53
+ }
54
+ end
55
+
56
+ end
57
+
58
+ def response_class
59
+ self.class.response_class
60
+ end
61
+
62
+ def when_ok(&blk)
63
+ @cb_response = blk
64
+ end
65
+
66
+ def when_callback(&blk)
67
+ @cb_callback = blk
68
+ end
69
+
70
+ # deprecated
71
+ # def each_ok_response(&blk)
72
+ # @cb_each_response = blk
73
+ # end
74
+ #
75
+ # def each_callback_response(&blk)
76
+ # @cb_each_callback = blk
77
+ # end
78
+ #
79
+ # def each_error_response(&blk)
80
+ # @cb_each_errback = blk
81
+ # end
82
+
83
+ def initialize(req)
84
+ super
85
+ @req = req
86
+ @req_action = req[ENVIRONMENT_KEY][GNTP_REQUEST_METHOD_KEY]
87
+ cb_context = req[HEADERS_KEY][GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY]
88
+ cb_context_type = req[HEADERS_KEY][GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY]
89
+ cb_target = req[HEADERS_KEY][GNTP_NOTIFICATION_CALLBACK_TARGET_KEY]
90
+ @wait_for_callback = @req_action == GNTP_NOTIFY_METHOD &&
91
+ cb_context && cb_context_type && !cb_target
92
+ end
93
+
94
+ def post_init
95
+ reset_state!
96
+ #puts @req.dump.inspect
97
+ send_data @req.dump
98
+ end
99
+
100
+ def receive_data data
101
+ @buffer.extract(data).each do |line|
102
+ #print "#{line.inspect}"
103
+ @lines << line
104
+ end
105
+ if eof?
106
+ receive_message @lines.join("\r\n") + "\r\n"
107
+ reset_message_buffer!
108
+ end
109
+ end
110
+
111
+ protected
112
+
113
+ def reset_state!
114
+ @state = :init
115
+ reset_message_buffer!
116
+ end
117
+
118
+ def reset_message_buffer!
119
+ @buffer = BufferedTokenizer.new("\r\n")
120
+ @lines = []
121
+ end
122
+
123
+ def eof?
124
+ @lines[-1] == ''
125
+ end
126
+
127
+ def receive_message(message)
128
+ raw = response_class.load(message, nil)
129
+ update_state_from_response!(raw)
130
+ #puts "Client received message, state = #{@state}"
131
+ resp = response_class.new(raw)
132
+ case @state
133
+ when :ok
134
+ @cb_response.call(resp) if @cb_response
135
+ self.succeed(resp) unless waiting_for_callback?
136
+ when :callback
137
+ @cb_callback.call(resp) if @cb_callback
138
+ self.succeed(resp)
139
+ when :error, :unknown
140
+ #@cb_response.call(resp) if @cb_response
141
+ self.fail(resp)
142
+ end
143
+ #puts "Waiting for callback? #{waiting_for_callback? ? 'yes' : 'no'}"
144
+ close_connection_after_writing unless waiting_for_callback?
145
+ end
146
+
147
+ def update_state_from_response!(resp)
148
+ @state = \
149
+ case resp[0].to_i
150
+ when 0
151
+ if resp[2][GNTP_NOTIFICATION_CALLBACK_RESULT_KEY]
152
+ :callback
153
+ else
154
+ :ok
155
+ end
156
+ when 100..500
157
+ :error
158
+ else
159
+ :unknown
160
+ end
161
+ end
162
+
163
+ def waiting_for_callback?
164
+ @wait_for_callback && [:ok, :init].include?(@state)
165
+ end
166
+
167
+ end
168
+
169
+ end
@@ -0,0 +1,74 @@
1
+ module Groem
2
+
3
+ module Constants
4
+
5
+ def self.included(mod)
6
+ self.constants.each do |c|
7
+ mod.const_set(c.to_s, self.const_get(c.to_s))
8
+ end
9
+ end
10
+
11
+ def growlify_key(str)
12
+ parts = str.to_s.tr('_','-').split('-')
13
+ parts.map {|p| p[0..0].upcase + p[1..-1]}.join('-')
14
+ end
15
+
16
+ def growlify_action(str)
17
+ act = str.to_s.upcase
18
+ act = {'CLICK' => GNTP_CLICK_CALLBACK_RESULT,
19
+ 'CLICKED' => GNTP_CLICK_CALLBACK_RESULT,
20
+ 'CLOSE' => GNTP_CLOSE_CALLBACK_RESULT,
21
+ 'CLOSED' => GNTP_CLOSE_CALLBACK_RESULT,
22
+ 'TIMEOUT' => GNTP_TIMEDOUT_CALLBACK_RESULT,
23
+ 'TIMEDOUT' => GNTP_TIMEDOUT_CALLBACK_RESULT
24
+ }[act]
25
+ end
26
+
27
+ ENVIRONMENT_KEY = 'environment'
28
+ HEADERS_KEY = 'headers'
29
+ NOTIFICATIONS_KEY = 'notifications'
30
+
31
+ GNTP_PROTOCOL_KEY = 'protocol'
32
+ GNTP_VERSION_KEY = 'version'
33
+ GNTP_REQUEST_METHOD_KEY = 'request_method'
34
+ GNTP_ENCRYPTION_ID_KEY = 'encryption_id'
35
+
36
+ GNTP_REGISTER_METHOD = 'REGISTER'
37
+ GNTP_NOTIFY_METHOD = 'NOTIFY'
38
+ GNTP_SUBSCRIBE_METHOD = 'SUBSCRIBE'
39
+
40
+ GNTP_DEFAULT_ENVIRONMENT = {GNTP_PROTOCOL_KEY => 'GNTP',
41
+ GNTP_VERSION_KEY => '1.0',
42
+ GNTP_REQUEST_METHOD_KEY => 'NOTIFY',
43
+ GNTP_ENCRYPTION_ID_KEY => 'NONE'
44
+ }
45
+
46
+ GNTP_APPLICATION_NAME_KEY = 'Application-Name'
47
+ GNTP_APPLICATION_ICON_KEY = 'Application-Icon'
48
+ GNTP_NOTIFICATION_COUNT_KEY = 'Notifications-Count'
49
+ GNTP_NOTIFICATION_NAME_KEY = 'Notification-Name'
50
+ GNTP_NOTIFICATION_ICON_KEY = 'Notification-Icon'
51
+ GNTP_NOTIFICATION_ID_KEY = 'Notification-ID'
52
+ GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY = 'Notification-Callback-Context'
53
+ GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY = 'Notification-Callback-Context-Type'
54
+ GNTP_NOTIFICATION_CALLBACK_TARGET_KEY = 'Notification-Callback-Target'
55
+
56
+ GNTP_RESPONSE_METHOD_KEY = 'response_method'
57
+ GNTP_RESPONSE_ACTION_KEY = 'Response-Action'
58
+ GNTP_ERROR_CODE_KEY = 'Error-Code'
59
+ GNTP_NOTIFICATION_CALLBACK_RESULT_KEY = 'Notification-Callback-Result'
60
+ GNTP_NOTIFICATION_CALLBACK_TIMESTAMP_KEY = 'Notification-Callback-Timestamp'
61
+
62
+ GNTP_OK_RESPONSE = '-OK'
63
+ GNTP_ERROR_RESPONSE = '-ERROR'
64
+ GNTP_CALLBACK_RESPONSE = '-CALLBACK'
65
+
66
+ GNTP_ERROR_CODE_OK = '0'
67
+
68
+ GNTP_CLICK_CALLBACK_RESULT = 'CLICK'
69
+ GNTP_CLOSE_CALLBACK_RESULT = 'CLOSE'
70
+ GNTP_TIMEDOUT_CALLBACK_RESULT = 'TIMEDOUT'
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,349 @@
1
+ require 'strscan'
2
+
3
+ module Groem
4
+
5
+ module Marshal
6
+ module Request
7
+ include Groem::Constants
8
+
9
+ def self.included(mod)
10
+ mod.extend ClassMethods
11
+ end
12
+
13
+ # write GNTP request string, print lines as \r\n
14
+ # assumes that including class delegates :[] to raw hash
15
+ # (described below under load).
16
+ # TODO: calculate UUIDs for binary sections and output them
17
+ def dump
18
+ out = []
19
+ env = GNTP_DEFAULT_ENVIRONMENT.merge(self[ENVIRONMENT_KEY])
20
+ hdrs = self[HEADERS_KEY]
21
+ notifs = self[NOTIFICATIONS_KEY]
22
+
23
+ out << "#{env[(GNTP_PROTOCOL_KEY)]}" +
24
+ "/#{env[(GNTP_VERSION_KEY)]} "+
25
+ "#{env[(GNTP_REQUEST_METHOD_KEY)]} "+
26
+ "#{env[(GNTP_ENCRYPTION_ID_KEY)]}"
27
+ hdrs.each_pair do |k, v|
28
+ unless v.nil?
29
+ out << "#{(k)}: #{v}"
30
+ end
31
+ end
32
+
33
+ if env[(GNTP_REQUEST_METHOD_KEY)] == GNTP_REGISTER_METHOD
34
+ out << "#{GNTP_NOTIFICATION_COUNT_KEY}: #{notifs.keys.count}"
35
+ out << nil
36
+ notifs.each_pair do |name, pairs|
37
+ out << "#{GNTP_NOTIFICATION_NAME_KEY}: #{name}"
38
+ pairs.each do |pair|
39
+ unless pair[1].nil?
40
+ out << "#{(pair[0])}: #{pair[1]}"
41
+ end
42
+ end
43
+ out << nil
44
+ end
45
+ end
46
+
47
+ if out.last.nil?
48
+ 1.times { out << nil }
49
+ else
50
+ 2.times { out << nil }
51
+ end
52
+
53
+ out.join("\r\n")
54
+ end
55
+
56
+
57
+ module ClassMethods
58
+ include Groem::Constants
59
+
60
+ #
61
+ # Load GNTP request into hash of:
62
+ # 'environment' => hash of environment (protocol, version, request_method, encryption data)
63
+ # 'headers' => hash of headers
64
+ # 'notifications' => hash of notifications keyed by name (REGISTER requests only, otherwise empty)
65
+ #
66
+ # Note that binary identifiers are resolved in both headers and notifications.
67
+ #
68
+ # If passed an optional klass, will return klass.new(out), otherwise just the hash.
69
+ # By default it tries to use the including object's class
70
+ #
71
+ # Note entire GNTP message must be passed as input.
72
+ #
73
+ # No semantic validation of input is done,
74
+ # and all key values are stored as strings, not casted
75
+ #
76
+ # Syntactic validation may be implemented in the future.
77
+ #
78
+ def load(input, klass = self)
79
+ env, hdrs, notifs = {}, {}, {}
80
+ meth, notif_name, id, len, bin = nil
81
+ section = :init
82
+ s = StringScanner.new(input)
83
+ until s.eos?
84
+ line, section = scan_line(s, meth, section)
85
+ case section
86
+ when :first
87
+ parse_first_header(line, env)
88
+ meth = env[(GNTP_REQUEST_METHOD_KEY)]
89
+ when :headers
90
+ parse_header(line, hdrs)
91
+ when :notification_start
92
+ notif_name = parse_notification_name(line)
93
+ when :notification
94
+ parse_notification_header(line, notif_name, notifs)
95
+ when :identifier_start
96
+ id = parse_identifier(line)
97
+ when :identifier_length
98
+ len = parse_identifier_length(line)
99
+ when :binary
100
+ bin = \
101
+ (1..len).inject('') do |memo, i|
102
+ memo << s.getch; memo
103
+ end
104
+ resolve_binary_key(id, bin, hdrs)
105
+ resolve_binary_key(id, bin, notifs)
106
+ end
107
+ end
108
+
109
+ out = { ENVIRONMENT_KEY => env,
110
+ HEADERS_KEY => hdrs,
111
+ NOTIFICATIONS_KEY => notifs
112
+ }
113
+
114
+ klass ? klass.new(out) : out
115
+ end
116
+
117
+ protected
118
+
119
+ def scan_line(scanner, method, state)
120
+ line = nil
121
+ new_state = state
122
+ case state
123
+ when :init
124
+ line = scanner.scan(/.*\n/)
125
+ new_state = :first
126
+ when :first
127
+ line = scanner.scan(/.*\n/)
128
+ new_state = :headers
129
+ when :headers
130
+ line = scanner.scan(/.*\n/)
131
+ new_state = if line =~ /^\w*identifier\w*:/i
132
+ :identifier_start
133
+ elsif method == GNTP_REGISTER_METHOD && \
134
+ line =~ /^\s*#{GNTP_NOTIFICATION_NAME_KEY}\s*:/i
135
+ :notification_start
136
+ else
137
+ :headers
138
+ end
139
+ when :notification_start
140
+ line = scanner.scan(/.*\n/)
141
+ new_state = :notification
142
+ when :notification
143
+ line = scanner.scan(/.*\n/)
144
+ new_state = if line =~ /^\s*identifier\s*:/i
145
+ :identifier_start
146
+ elsif method == GNTP_REGISTER_METHOD && \
147
+ line =~ /^\s*#{GNTP_NOTIFICATION_NAME_KEY}\s*:/i
148
+ :notification_start
149
+ else
150
+ :notification
151
+ end
152
+ when :identifier_start
153
+ line = scanner.scan(/.*\n/)
154
+ new_state = :identifier_length if line =~ /^\s*length\s*:/i
155
+ when :identifier_length
156
+ new_state = :binary
157
+ when :binary
158
+ line = scanner.scan(/.*\n/)
159
+ new_state = if line =~ /^\s*identifier\s*:/i
160
+ :identifier_start
161
+ elsif method == GNTP_REGISTER_METHOD && \
162
+ line =~ /^\s*#{GNTP_NOTIFICATION_NAME_KEY}\s*:/i
163
+ :notification_start
164
+ else
165
+ :headers
166
+ end
167
+ end
168
+ #puts "state #{state} --> #{new_state}"
169
+ state = new_state
170
+ line = line.chomp if line
171
+ [line, state]
172
+ end
173
+
174
+ def parse_first_header(line, hash)
175
+ return hash unless line && line.size > 0
176
+ tokens = line.split(' ')
177
+ proto, vers = tokens[0].split('/')
178
+ msgtype = tokens[1]
179
+ encrypid, ivvalue = if tokens[2]; tokens[2].split(':'); end
180
+ keyhashid = if tokens[3]; tokens[3].split(':')[0]; end
181
+ keyhash, salt = if tokens[3] && tokens[3].split(':')[1]
182
+ tokens[3].split(':')[1].split('.')
183
+ end
184
+ hash[(GNTP_PROTOCOL_KEY)] = proto
185
+ hash[(GNTP_VERSION_KEY)] = vers
186
+ hash[(GNTP_REQUEST_METHOD_KEY)] = msgtype
187
+ hash[(GNTP_ENCRYPTION_ID_KEY)] = encrypid
188
+ # TODO the rest
189
+ hash
190
+ end
191
+
192
+ def parse_header(line, hash)
193
+ return hash unless line && line.size > 0
194
+ key, val = line.split(':', 2).map {|t| t.strip }
195
+ key = (key)
196
+ hash[key] = val
197
+ hash
198
+ end
199
+
200
+ def parse_notification_name(line)
201
+ return nil unless line && line.size > 0
202
+ key, val = line.split(':', 2).map {|t| t.strip }
203
+ val if key.downcase == GNTP_NOTIFICATION_NAME_KEY.downcase
204
+ end
205
+
206
+ def parse_notification_header(line, name, hash)
207
+ return hash unless line && line.size > 0
208
+ key, val = line.split(':', 2).map {|t| t.strip }
209
+ key = (key)
210
+ (hash[name] ||= {})[key] = val
211
+ hash
212
+ end
213
+
214
+ def parse_identifier(line)
215
+ return nil unless line && line.size > 0
216
+ key, val = line.split(':', 2).map {|t| t.strip }
217
+ val if key.downcase == 'identifier'
218
+ end
219
+
220
+ def parse_identifier_length(line)
221
+ return nil unless line && line.size > 0
222
+ key, val = line.split(':', 2).map {|t| t.strip }
223
+ val.to_i if key.downcase == 'length'
224
+ end
225
+
226
+ def resolve_binary_key(key, data, hash)
227
+ if key && \
228
+ pairs = hash.select do |k, v|
229
+ v =~ /x-growl-resource:\/\/#{Regexp.escape(key)}/i
230
+ end
231
+ pairs.each { |p| hash[p[0]] = data }
232
+ end
233
+ hash
234
+ end
235
+
236
+ end # GNTP::Marshal::Request::ClassMethods
237
+
238
+ end # GNTP::Marshal::Request
239
+
240
+
241
+ module Response
242
+ include Groem::Constants
243
+
244
+ def self.included(mod)
245
+ mod.extend ClassMethods
246
+ end
247
+
248
+ # write GNTP request string, print lines as \r\n
249
+ # assumes that including class delegates :[] to raw hash
250
+ # (described below under load).
251
+ def dump
252
+ #TODO
253
+ end
254
+
255
+ module ClassMethods
256
+ include Groem::Constants
257
+
258
+ # Load GNTP response into array of:
259
+ # status (error code or '0' for OK)
260
+ # hash of headers (except error code and notification-callback-*)
261
+ # hash of callback headers (for callback responses, otherwise {})
262
+ #
263
+ # Note this is explicitly modeled after Rack's interface
264
+ #
265
+ def load(input, klass = self)
266
+ env, hdrs, cb_hdrs = {}, {}, {}
267
+ status, meth = nil
268
+ section = :init
269
+ s = StringScanner.new(input)
270
+ until s.eos?
271
+ line, section = scan_line(s, meth, section)
272
+ case section
273
+ when :first
274
+ parse_first_header(line, env)
275
+ when :headers
276
+ parse_header(line, hdrs)
277
+ end
278
+ end
279
+
280
+ # pull out status from headers
281
+ status = hdrs.delete(GNTP_ERROR_CODE_KEY)
282
+ status ||= GNTP_ERROR_CODE_OK
283
+
284
+ # pull out notification-callback-* from headers
285
+ [GNTP_NOTIFICATION_CALLBACK_CONTEXT_KEY,
286
+ GNTP_NOTIFICATION_CALLBACK_CONTEXT_TYPE_KEY,
287
+ GNTP_NOTIFICATION_CALLBACK_RESULT_KEY,
288
+ GNTP_NOTIFICATION_CALLBACK_TIMESTAMP_KEY].each do |key|
289
+ if val = hdrs.delete(key)
290
+ cb_hdrs[key] = val
291
+ end
292
+ end
293
+
294
+ out = [ status, hdrs, cb_hdrs ]
295
+
296
+ klass ? klass.new(out) : out
297
+ end
298
+
299
+ protected
300
+
301
+ def scan_line(scanner, method, state)
302
+ line = nil
303
+ new_state = state
304
+ case state
305
+ when :init
306
+ line = scanner.scan(/.*\n/)
307
+ new_state = :first
308
+ when :first
309
+ line = scanner.scan(/.*\n/)
310
+ new_state = :headers
311
+ when :headers
312
+ line = scanner.scan(/.*\n/)
313
+ new_state = :headers
314
+ end
315
+ #puts "state #{state} --> #{new_state}"
316
+ state = new_state
317
+ line = line.chomp if line
318
+ [line, state]
319
+ end
320
+
321
+ def parse_first_header(line, hash)
322
+ return hash unless line && line.size > 0
323
+ tokens = line.split(' ')
324
+ proto, vers = tokens[0].split('/')
325
+ msgtype = tokens[1]
326
+ encrypid = tokens[2]
327
+ hash[(GNTP_PROTOCOL_KEY)] = proto
328
+ hash[(GNTP_VERSION_KEY)] = vers
329
+ hash[(GNTP_RESPONSE_METHOD_KEY)] = msgtype
330
+ hash[(GNTP_ENCRYPTION_ID_KEY)] = encrypid
331
+ hash
332
+ end
333
+
334
+ def parse_header(line, hash)
335
+ return hash unless line && line.size > 0
336
+ key, val = line.split(':', 2).map {|t| t.strip }
337
+ key = (key)
338
+ hash[key] = val
339
+ hash
340
+ end
341
+
342
+ end
343
+
344
+ end # GNTP::Marshal::Response
345
+
346
+
347
+ end # GNTP::Marshal
348
+
349
+ end