groem 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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