em-xmpp 0.0.1 → 0.0.2

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.
data/README.md CHANGED
@@ -1,9 +1,18 @@
1
1
  # Em::Xmpp
2
2
 
3
- TODO: Write a gem description
3
+ EM::Xmpp is an XMPP client library for EventMachine.
4
+ It uses Nokogiri as an XML parser and XML builder.
5
+
6
+ EM::Xmpp provides decorator-style modules in the mean of contexts
7
+ to easily match and reply to stanzas.
4
8
 
5
9
  ## Installation
6
10
 
11
+ ### Standard
12
+
13
+ gem install em-xmpp
14
+
15
+ ### Bundler
7
16
  Add this line to your application's Gemfile:
8
17
 
9
18
  gem 'em-xmpp'
@@ -18,7 +27,92 @@ Or install it yourself as:
18
27
 
19
28
  ## Usage
20
29
 
21
- TODO: Write usage instructions here
30
+ ### Connecting to an XMPP server
31
+
32
+ Like many EventMachine libraries, you need to first create a module and pass it
33
+ to EM::Xmpp::Connection.start
34
+
35
+ When the connection is ready, EM::Xmpp will call :ready on your connection
36
+ object. From that point, you can start handling stanzas.
37
+
38
+ ### Receiving stanzas
39
+
40
+ You can setup handlers for message, presence, and iq with on_message,
41
+ on_presence, and on_iq methods. All these methods take a callback block
42
+ argument. EM::Xmpp will be call-back the block with a Context object. This
43
+ context object helps you while reading the content of a stanza and replying to
44
+ it.
45
+
46
+ You may have multiple handlers per stanza type and the callback block argument
47
+ must return the same or another context that will be used for further matching.
48
+ This layering lets you write stack-like middlewares where every Context handler
49
+ adds some features/environment-variables to the Context.
50
+
51
+ You can call #done! on a Context to notify the stack that you are done with
52
+ this stanza. That is, you do not give a chance to subsequent handlers to match
53
+ on the stanza.
54
+
55
+ You can also call #delete_xpath_handler! on a context handler to remove it from
56
+ the stack for the next stanzas. This let you build temporary handlers quite
57
+ easily.
58
+
59
+ Any handler can also throw :halt to interrupt the layering and all the handler
60
+ removal operations. You should read the code to understand well what you skip
61
+ by doing so.
62
+
63
+ You can use on(*xpath_args) to build a matcher for any XPath in the stanza.
64
+ The arguments are passed verbatim to Nokogiri. A special argument is
65
+ on(:anything) that will match any stanza (e.g., for logging). This is useful
66
+ to build new decorators for handling a specific XEP (if you do so, please share
67
+ with a pull-request).
68
+
69
+ When an exception occurs in a stanza handler, the stack rescues the error and
70
+ runs the Context through a set of exception handlers. To handle exception you
71
+ can use the on_exception(:anything) method.
72
+
73
+ See the ./samples directory for basic examples.
74
+
75
+ ### Sending stanzas
76
+
77
+ EM::XMPP for now builds stanzas with Nokogiri::XML::Builder in the form with an
78
+ explicit block argument.
79
+ Then you send raw_strings with Connection#send_raw or pre-built stanzas with Connection#send_stanza.
80
+ Note that if you send malformed XML, your server will disconnect you. Hence,
81
+ take care when writing XML without and XML builder.
82
+
83
+ Contexts for message/iq/presence provide a "reply" method that will pre-fill
84
+ some fields for you. Otherwise, you can use
85
+
86
+ data = message_stanza('to' => 'someone@somehost') do |xml|
87
+ xml.body('hello world')
88
+ end
89
+ send_stanza data
90
+
91
+ to send a stanza to someone. Note that since EM:Xmpp sets jabber:client as
92
+ default namespace of the XML stream, you must not set the XML namespace for
93
+ body/iq/presence and all the things that sit in jabber:client namespace. For
94
+ other XEPs, do not forget to set your namespaces.
95
+
96
+ The XML::Builder uses method_missing and hence this building scheme may be slow
97
+ if you need a large throughput of outgoing stanzas.
98
+
99
+ See the ./samples directory for basic examples.
100
+
101
+ ## Features and Missing
102
+
103
+ This library does not manage the roster for you. You will have to
104
+ do this by hand.
105
+
106
+ We do not support but plan to support anonymous login yet.
107
+
108
+ We do not support but may support component login in the future.
109
+
110
+ SASL authentication uses the ruby-sasl gem. It misses some types of
111
+ authentication (for example, X-GOOGLE-TOKEN).
112
+
113
+ This library lets you manage the handling and matching of stanza quite close to
114
+ the wire. .
115
+
22
116
 
23
117
  ## Contributing
24
118
 
@@ -48,7 +48,7 @@ module EM::Xmpp
48
48
  def negotiation_finished
49
49
  @pass = nil
50
50
  @handler = Routine.new self
51
- send_raw presence_stanza()
51
+ send_stanza presence_stanza()
52
52
  ready
53
53
  end
54
54
 
@@ -56,44 +56,61 @@ module EM::Xmpp
56
56
  raise RuntimeError, "could not negotiate a stream:\n#{node}"
57
57
  end
58
58
 
59
- #should add 'xml:lang'
59
+ OutgoingStanza = Struct.new(:xml, :params)
60
60
 
61
- def default_presence_config
61
+ def default_presence_params
62
62
  {}
63
63
  end
64
64
 
65
- def default_message_config
65
+ def default_message_params
66
66
  {'to' => @jid.domain, 'id' => "em-xmpp.#{rand(65535)}"}
67
67
  end
68
68
 
69
- def default_iq_config
69
+ def default_iq_params
70
70
  {'type' => 'get', 'id' => "em-xmpp.#{rand(65535)}"}
71
71
  end
72
72
 
73
- def presence_stanza(cfg={}, &blk)
74
- cfg = default_presence_config.merge(cfg)
75
- build_xml do |x|
76
- x.presence(cfg, &blk)
73
+ def presence_stanza(params={}, &blk)
74
+ params = default_presence_params.merge(params)
75
+ xml = build_xml do |x|
76
+ x.presence(params, &blk)
77
77
  end
78
+ OutgoingStanza.new xml, params
78
79
  end
79
80
 
80
- def message_stanza(cfg={}, &blk)
81
- cfg = default_message_config.merge(cfg)
82
- build_xml do |x|
83
- x.message(cfg, &blk)
81
+ def message_stanza(params={}, &blk)
82
+ params = default_message_params.merge(params)
83
+ xml = build_xml do |x|
84
+ x.message(params, &blk)
84
85
  end
86
+ OutgoingStanza.new xml, params
85
87
  end
86
88
 
87
- def iq_stanza(cfg={}, &blk)
88
- cfg = default_iq_config.merge(cfg)
89
- build_xml do |x|
90
- x.iq(cfg, &blk)
89
+ def iq_stanza(params={}, &blk)
90
+ params = default_iq_params.merge(params)
91
+ xml = build_xml do |x|
92
+ x.iq(params, &blk)
93
+ end
94
+ OutgoingStanza.new xml, params
95
+ end
96
+
97
+ def send_stanza(stanza)
98
+ send_raw stanza.xml
99
+ if block_given?
100
+ on(:anything) do |ctx|
101
+ if ctx.id == stanza.params['id']
102
+ yield ctx
103
+ ctx.delete_xpath_handler!
104
+ else
105
+ ctx
106
+ end
107
+ end
91
108
  end
92
109
  end
93
110
 
94
111
  %w{on on_exception on_presence on_iq on_message}.each do |meth|
95
112
  define_method(meth) do |*args,&blk|
96
- @handler.send meth, *args, &blk
113
+ @handler.send meth, *args, &blk
97
114
  end
98
115
  end
99
116
 
@@ -2,6 +2,7 @@
2
2
  require 'em-xmpp/jid'
3
3
  require 'em-xmpp/namespaces'
4
4
  require 'time'
5
+ require 'ostruct'
5
6
 
6
7
  module EM::Xmpp
7
8
  class Context
@@ -74,7 +75,7 @@ module EM::Xmpp
74
75
  obj
75
76
  end
76
77
 
77
- module Stanza
78
+ module IncomingStanza
78
79
  include Namespaces
79
80
  %w{type id lang}.each do |w|
80
81
  define_method w do
@@ -87,10 +88,42 @@ module EM::Xmpp
87
88
  def from
88
89
  read_attr(stanza, 'from'){|j| JID.parse(j)}
89
90
  end
91
+ def error?
92
+ type == 'error'
93
+ end
94
+ def delay?
95
+ false
96
+ end
97
+ end
98
+
99
+ module Error
100
+ def error_node
101
+ xpath('//xmlns:error',{'xmlns' => Client}).first
102
+ end
103
+
104
+ def error_code
105
+ n = error_node
106
+ read_attr(n, 'code') if n
107
+ end
108
+
109
+ def error_type
110
+ n = error_node
111
+ read_attr(n, 'type') if n
112
+ end
113
+
114
+ def error_condition_node
115
+ n = error_node
116
+ n.children.first if n
117
+ end
118
+
119
+ def error_condition
120
+ n = error_condition_node
121
+ n.name if n
122
+ end
90
123
  end
91
124
 
92
125
  module Presence
93
- include Stanza
126
+ include IncomingStanza
94
127
  def reply_default_params
95
128
  jid = @connection.jid.full
96
129
  {'from' => jid, 'to' => from, 'id' => id}
@@ -123,10 +156,13 @@ module EM::Xmpp
123
156
  def subscription_request?
124
157
  type == 'subscribe'
125
158
  end
159
+ def entity_left?
160
+ type == 'unavailable'
161
+ end
126
162
  end
127
163
 
128
164
  module Message
129
- include Stanza
165
+ include IncomingStanza
130
166
  def subject_node
131
167
  xpath('//xmlns:subject',{'xmlns' => Client}).first
132
168
  end
@@ -155,10 +191,14 @@ module EM::Xmpp
155
191
  args = reply_default_params.merge args
156
192
  @connection.message_stanza(args,&blk)
157
193
  end
194
+
195
+ def groupchat?
196
+ type == 'groupchat'
197
+ end
158
198
  end
159
199
 
160
200
  module Iq
161
- include Stanza
201
+ include IncomingStanza
162
202
  def reply_default_params
163
203
  jid = @connection.jid.full
164
204
  {'from' => jid, 'to' => from, 'type' => 'result', 'id' => id}
@@ -171,6 +211,9 @@ module EM::Xmpp
171
211
  end
172
212
 
173
213
  module Delay
214
+ def delay?
215
+ true
216
+ end
174
217
  #does not handle legacy delay
175
218
  def delay_node
176
219
  xpath('//xmlns:delay',{'xmlns' => EM::Xmpp::Namespaces::Delay}).first
@@ -245,5 +288,228 @@ module EM::Xmpp
245
288
  end
246
289
  end
247
290
  end
291
+
292
+ module Roster
293
+ def query_node
294
+ xpath('//xmlns:query',{'xmlns' => EM::Xmpp::Namespaces::Roster}).first
295
+ end
296
+
297
+ def items
298
+ n = query_node
299
+ if n
300
+ n.children.map do |xml|
301
+ new_subscription(xml)
302
+ end
303
+ end
304
+ end
305
+
306
+ private
307
+
308
+ Subscription = Struct.new(:type, :jid, :name, :groups)
309
+
310
+ def new_subscription(n)
311
+ type = read_attr(n,'subscription')
312
+ jid = read_attr(n,'jid') {|x| JID.parse x}
313
+ name = read_attr(n,'name')
314
+ groups = n.xpath('xmlns:group', 'xmlns' => EM::Xmpp::Namespaces::Roster).map{|n| n.content}
315
+ Subscription.new(type, jid, name, groups)
316
+ end
317
+ end
318
+
319
+ module Tune
320
+ def tune_node
321
+ xpath('//xmlns:tune',{'xmlns' => Namespaces::Tune}).first
322
+ end
323
+
324
+ DecimalFields = %w{length rating}.freeze
325
+ StringFields = %w{artist source title track uri}.freeze
326
+
327
+ DecimalFields.each do |decimal|
328
+ define_method(decimal) do
329
+ n = tune_node
330
+ if n
331
+ d = n.children.find{|c| c.name == decimal}
332
+ d.content.to_i if d
333
+ end
334
+ end
335
+ end
336
+
337
+
338
+ StringFields.each do |str|
339
+ define_method(str) do
340
+ n = tune_node
341
+ if n
342
+ d = n.children.find{|c| c.name == str}
343
+ d.content
344
+ end
345
+ end
346
+ end
347
+
348
+ def tune
349
+ ostruct = OpenStruct.new
350
+ (StringFields + DecimalFields).each do |field|
351
+ val = send field
352
+ ostruct.send("#{field}=", val)
353
+ end
354
+ ostruct
355
+ end
356
+ end
357
+
358
+ module Nickname
359
+ def nickname_node
360
+ xpath('//xmlns:nick',{'xmlns' => Nick}).first
361
+ end
362
+ def nickname
363
+ n = nickname_node
364
+ n.content if n
365
+ end
366
+ end
367
+
368
+ module Geolocation
369
+ def geoloc_node
370
+ xpath('//xmlns:nick',{'xmlns' => Geoloc}).first
371
+ end
372
+
373
+ def geoloc
374
+ ostruct = OpenStruct.newt
375
+ (StringFields + DecimalFields + %w{timestamp}).each do |field|
376
+ val = send field
377
+ ostruct.send("#{field}=", val)
378
+ end
379
+ ostruct
380
+ end
381
+
382
+ StringFields = %w{area building country countrycode datum description
383
+ floor locality postalcode region room street text uri}.freeze
384
+ DecimalFields = %w{accuracy error alt lat lon bearing speed}.freeze
385
+
386
+ DecimalFields.each do |decimal|
387
+ define_method(decimal) do
388
+ n = geoloc_node
389
+ if n
390
+ d = n.children.find{|c| c.name == decimal}
391
+ d.content.to_i if d
392
+ end
393
+ end
394
+ end
395
+
396
+ StringFields.each do |str|
397
+ define_method(str) do
398
+ n = geoloc_node
399
+ if n
400
+ d = n.children.find{|c| c.name == str}
401
+ d.content
402
+ end
403
+ end
404
+ end
405
+
406
+ def timestamp
407
+ n = geoloc_node
408
+ if n
409
+ d = n.children.find{|c| c.name == 'timestamp'}
410
+ Time.parse d.content if d
411
+ end
412
+ end
413
+ end
414
+
415
+ module Useractivity
416
+ def activity_node
417
+ xpath('//xmlns:mood',{'xmlns' => Namespaces::Activity}).first
418
+ end
419
+
420
+ def activity_category_node
421
+ n = activity_node
422
+ n.children.first if n
423
+ end
424
+
425
+ def activity_text_node
426
+ n = activity_node
427
+ n.children.find{|n| n.name == 'text'} if n
428
+ end
429
+
430
+ def activity
431
+ ret = []
432
+ n = activity_category_node
433
+ if n
434
+ ret << n.name
435
+ detail = n.children.first
436
+ ret << detail.name if detail
437
+ end
438
+ ret
439
+ end
440
+
441
+ def activity_text
442
+ n = activity_text_node
443
+ n.content if n
444
+ end
445
+ end
446
+
447
+ module Mood
448
+ DefinedMoods = %w{afraid amazed angry amorous annoyed anxious aroused
449
+ ashamed bored brave calm cautious cold confident confused contemplative
450
+ contented cranky crazy creative curious dejected depressed disappointed
451
+ disgusted dismayed distracted embarrassed envious excited flirtatious
452
+ frustrated grumpy guilty happy hopeful hot humbled humiliated hungry hurt
453
+ impressed in_awe in_love indignant interested intoxicated invincible jealous
454
+ lonely lucky mean moody nervous neutral offended outraged playful proud relaxed
455
+ relieved remorseful restless sad sarcastic serious shocked shy sick sleepy
456
+ spontaneous stressed strong surprised thankful thirsty tired undefined weak
457
+ worried}.freeze
458
+
459
+ def mood_node
460
+ xpath('//xmlns:mood',{'xmlns' => Namespaces::Mood}).first
461
+ end
462
+ def mood_name_node
463
+ n = mood_node
464
+ n.children.find{|c| DefinedMoods.include?(c.name)} if n
465
+ end
466
+ def mood_text_node
467
+ n = mood_node
468
+ n.children.find{|c| c.name == 'text'}
469
+ end
470
+ def mood
471
+ n = mood_name_node
472
+ n.name if n
473
+ end
474
+ def mood
475
+ n = mood_text_node
476
+ n.content if n
477
+ end
478
+ end
479
+
480
+ module Mucuser
481
+ def x_node
482
+ xpath('//xmlns:x',{'xmlns' => EM::Xmpp::Namespaces::MucUser}).first
483
+ end
484
+
485
+ def item_node
486
+ xpath('//xmlns:item',{'xmlns' => EM::Xmpp::Namespaces::MucUser}).first
487
+ end
488
+
489
+ def status_node
490
+ xpath('//xmlns:status',{'xmlns' => EM::Xmpp::Namespaces::MucUser}).first
491
+ end
492
+
493
+ def status
494
+ n = status_node
495
+ read_attr(n, 'code') if n
496
+ end
497
+
498
+ def jid
499
+ n = item_node
500
+ jid_str = read_attr(n, 'jid') if n
501
+ JID.parse jid_str if jid_str
502
+ end
503
+
504
+ def affiliation
505
+ n = item_node
506
+ read_attr(n, 'affiliation') if n
507
+ end
508
+
509
+ def role
510
+ n = item_node
511
+ read_attr(n, 'role') if n
512
+ end
513
+ end
248
514
  end
249
515
  end
@@ -1,6 +1,8 @@
1
1
 
2
2
  require 'em-xmpp/namespaces'
3
3
  require 'em-xmpp/context'
4
+ require 'em-xmpp/stanza_matcher'
5
+ require 'em-xmpp/stanza_handler'
4
6
  require 'base64'
5
7
  require 'sasl/base'
6
8
  require 'sasl'
@@ -9,16 +11,10 @@ module EM::Xmpp
9
11
  class Handler
10
12
  include Namespaces
11
13
 
12
- Xpath = Struct.new(:path, :args, :blk) do
13
- def match?(xml)
14
- path == :anything or xml.xpath(path, args).any?
15
- end
16
- end
17
-
18
14
  def initialize(conn)
19
15
  @connection = conn
20
- @xpaths = []
21
- @exception_xpaths = []
16
+ @handlers = []
17
+ @exception_handlers = []
22
18
 
23
19
  stack_decorators
24
20
  end
@@ -36,9 +32,21 @@ module EM::Xmpp
36
32
  end
37
33
 
38
34
  def stack_decorators
39
- on_presence { |ctx| ctx.with(:presence) }
40
- on_message { |ctx| ctx.with(:message) }
41
- on_iq { |ctx| ctx.with(:iq) }
35
+ on_presence do |ctx|
36
+ ctx = ctx.with(:presence)
37
+ ctx = ctx.with(:error) if ctx.error?
38
+ ctx
39
+ end
40
+ on_message do |ctx|
41
+ ctx = ctx.with(:message)
42
+ ctx = ctx.with(:error) if ctx.error?
43
+ ctx
44
+ end
45
+ on_iq do |ctx|
46
+ ctx = ctx.with(:iq)
47
+ ctx = ctx.with(:error) if ctx.error?
48
+ ctx
49
+ end
42
50
  on('//xmlns:delay', 'xmlns' => Delay) do |ctx|
43
51
  ctx.with(:delay)
44
52
  end
@@ -48,20 +56,51 @@ module EM::Xmpp
48
56
  on('//xmlns:query', 'xmlns' => DiscoverItems) do |ctx|
49
57
  ctx.with(:discoitems)
50
58
  end
59
+ on('//xmlns:query', 'xmlns' => Roster) do |ctx|
60
+ ctx.with(:roster)
61
+ end
51
62
  on('//xmlns:command', 'xmlns' => Commands) do |ctx|
52
63
  ctx.with(:command)
53
64
  end
54
65
  on('//xmlns:x', 'xmlns' => DataForms) do |ctx|
55
66
  ctx.with(:dataforms)
56
67
  end
68
+ on('//xmlns:nick', 'xmlns' => Nick) do |ctx|
69
+ ctx.with(:nickname)
70
+ end
71
+ on('//xmlns:x', 'xmlns' => MucUser) do |ctx|
72
+ ctx.with(:mucuser)
73
+ end
74
+ end
75
+
76
+ def add_handler(handler)
77
+ @handlers << handler
78
+ end
79
+
80
+ def add_exception_handler(handler)
81
+ @exception_handlers << handler
82
+ end
83
+
84
+ def remove_handler(handler)
85
+ @handlers.delete handler
86
+ end
87
+
88
+ def remove_exception_handler(handler)
89
+ @exception_handlers.delete handler
57
90
  end
58
91
 
59
92
  def on(path, args={}, &blk)
60
- @xpaths << Xpath.new(path, args, blk)
93
+ matcher = StanzaMatcher.new(path, args)
94
+ handler = StanzaHandler.new(matcher, blk)
95
+ add_handler handler
96
+ handler
61
97
  end
62
98
 
63
99
  def on_exception(path, args={}, &blk)
64
- @exception_xpaths << Xpath.new(path, args, blk)
100
+ matcher = StanzaMatcher.new(path, args)
101
+ handler = StanzaHandler.new(matcher, &blk)
102
+ add_exception_handler handler
103
+ handler
65
104
  end
66
105
 
67
106
  # wraps the stanza in a context and calls handle_context
@@ -69,9 +108,9 @@ module EM::Xmpp
69
108
  handle_context Context.new(@connection, stanza)
70
109
  end
71
110
 
72
- # runs all xpath_handlers against the stanza context
111
+ # runs all handlers against the stanza context
73
112
  # catches all exception (in which case, the context gets passed to all
74
- # exception_xpaths)
113
+ # exception_handlers)
75
114
  #
76
115
  # an xpath handler can:
77
116
  # - throw :halt to shortcircuit everything
@@ -80,25 +119,17 @@ module EM::Xmpp
80
119
  # handlers such as request/responses
81
120
  def handle_context(ctx)
82
121
  catch :halt do
83
- @xpaths = run_xpath_handlers ctx, @xpaths
122
+ @handlers = run_xpath_handlers ctx, @handlers.dup
84
123
  end
85
124
  rescue => err
86
125
  ctx['error'] = err
87
- @exception_xpaths = run_xpath_handlers ctx, @exception_xpaths
126
+ @exception_handlers = run_xpath_handlers ctx, @exception_handlers
88
127
  end
89
128
 
90
129
  # runs all handlers and returns a list of handlers for the next stanza
91
130
  def run_xpath_handlers(ctx, handlers)
92
131
  handlers.map do |x|
93
- if (not ctx.done?) and (x.match?(ctx.stanza))
94
- ctx['xpath.handler'] = x
95
- ctx = x.blk.call(ctx)
96
- raise RuntimeError, "xpath handlers should return a Context" unless ctx.is_a?(Context)
97
-
98
- x if ctx.reuse_handler?
99
- else
100
- x
101
- end
132
+ x.call ctx
102
133
  end.compact
103
134
  end
104
135
  end
@@ -150,6 +181,9 @@ module EM::Xmpp
150
181
  end
151
182
 
152
183
  def setup_handlers
184
+ on_exception(:anything) do |ctx|
185
+ raise ctx['error']
186
+ end
153
187
  on('//xmlns:starttls', {'xmlns' => TLS}) do |ctx|
154
188
  @connection.ask_for_tls
155
189
  ctx.delete_xpath_handler!.done!
@@ -220,7 +254,7 @@ module EM::Xmpp
220
254
  end
221
255
 
222
256
  def bind_to_resource(wanted_res=nil)
223
- c.send_raw(c.iq_stanza('type' => 'set') do |x|
257
+ c.send_stanza(c.iq_stanza('type' => 'set') do |x|
224
258
  x.bind('xmlns' => Bind) do |y|
225
259
  y.resource(wanted_res) if wanted_res
226
260
  end
@@ -228,7 +262,7 @@ module EM::Xmpp
228
262
  end
229
263
 
230
264
  def start_session
231
- c.send_raw(c.iq_stanza('type' => 'set', 'to' => jid.domain) do |x|
265
+ c.send_stanza(c.iq_stanza('type' => 'set', 'to' => jid.domain) do |x|
232
266
  x.session('xmlns' => Session)
233
267
  end)
234
268
  end
@@ -21,11 +21,29 @@ module EM::Xmpp
21
21
  DiscoverItems = "http://jabber.org/protocol/disco#items"
22
22
  #XMPP info discovery
23
23
  DiscoverInfos = "http://jabber.org/protocol/disco#infos"
24
+ #Jabber Roster
25
+ Roster = 'jabber:iq:roster'
24
26
  #XMPP commands
25
27
  Commands = "http://jabber.org/protocol/commands"
28
+ #entity nicknames
29
+ Nick = "http://jabber.org/protocol/nick"
30
+ #entity activity
31
+ Activity = "http://jabber.org/protocol/activity"
32
+ #entity mood
33
+ Mood = "http://jabber.org/protocol/mood"
34
+ #entity geoloc
35
+ Geoloc = "http://jabber.org/protocol/geoloc"
36
+ #entity's song
37
+ Tune = "http://jabber.org/protocol/tune"
26
38
  #XMPP delayed delivery
27
39
  Delay = "urn:xmpp:delay"
28
40
  #Jabber Data forms
29
41
  DataForms = 'jabber:x:data'
42
+ #Multi user chat - simple user
43
+ MucUser = 'http://jabber.org/protocol/muc#user'
44
+ #Multi user chat - owner
45
+ MucOwner = 'http://jabber.org/protocol/muc#owner'
46
+ #Multi user chat - roomconfig
47
+ MucRoomconfig = 'http://jabber.org/protocol/muc#roomconfig'
30
48
  end
31
49
  end
@@ -0,0 +1,28 @@
1
+
2
+ module EM::Xmpp
3
+ class StanzaHandler
4
+ attr_reader :matcher, :callback
5
+ def initialize(matcher, cb=nil, &blk)
6
+ raise ArgumentError unless matcher.respond_to? :match?
7
+ @matcher = matcher
8
+ @callback = cb || blk
9
+ end
10
+
11
+ def match?(obj)
12
+ matcher.match? obj
13
+ end
14
+
15
+ def call(ctx)
16
+ if (not ctx.done?) and (match?(ctx.stanza))
17
+ ctx['xpath.handler'] = self
18
+ ctx = callback.call(ctx)
19
+ raise RuntimeError, "xpath handlers should return a Context" unless ctx.is_a?(Context)
20
+
21
+ self if ctx.reuse_handler?
22
+ else
23
+ self
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,22 @@
1
+
2
+ module EM::Xmpp
3
+ class StanzaMatcher
4
+ attr_reader :matcher
5
+
6
+ def initialize(obj=nil,args={},&blk)
7
+ obj ||= blk
8
+ @matcher = case obj
9
+ when :anything
10
+ proc { true }
11
+ when String
12
+ proc { |xml| xml.xpath(obj, args).any? }
13
+ else
14
+ obj
15
+ end
16
+ end
17
+
18
+ def match?(xml)
19
+ matcher.call xml
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  module Em
2
2
  module Xmpp
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
data/samples/echo.rb CHANGED
@@ -19,15 +19,47 @@ module MyClient
19
19
  def ready
20
20
  puts "***** #{@jid} ready for #{self}"
21
21
 
22
+ # Sending a stanza and adding a temporary matcher to it.
23
+ # Returns an handler object that is already ready to process further stanzas.
24
+ # Note that so far there is no way to time-out such an handler automatically.
25
+ handler = send_stanza(iq_stanza('to'=> 'dicioccio@mbpldc.local') do |x|
26
+ x.body "this will trigger an XMPP error"
27
+ end) do |ctx|
28
+ if ctx.error?
29
+ p ctx.error_code
30
+ p ctx.error_type
31
+ p ctx.error_condition
32
+ end
33
+ ctx
34
+ end
35
+
36
+ # Exception handling (i.e., when Ruby raises something)
37
+ on_exception(:anything) do |ctx|
38
+ raise ctx['error']
39
+ end
40
+
41
+ # Manually add a matcher + handler for it
42
+ m = EM::Xmpp::StanzaMatcher.new do |ctx|
43
+ p "proc-ing"
44
+ true
45
+ end
46
+ h = EM::Xmpp::StanzaHandler.new(m) do |ctx|
47
+ p "proc-ed"
48
+ ctx
49
+ end
50
+ @handler.add_handler h
51
+
52
+ # Presence handler
22
53
  on_presence do |s|
23
54
  p "*presence> #{s.from} #{s.show} (#{s.status})"
24
- send_raw(s.reply('type'=>'subscribed')) if s.subscription_request?
55
+ send_stanza(s.reply('type'=>'subscribed')) if s.subscription_request?
25
56
  s
26
57
  end
27
58
 
59
+ # Message handler
28
60
  on_message do |s|
29
61
  p "*message> #{s.from}\n#{s.body}\n"
30
- send_raw(s.reply do |x|
62
+ send_stanza(s.reply do |x|
31
63
  x.body "you sent:#{s.body}"
32
64
  end)
33
65
  s
data/samples/muc.rb ADDED
@@ -0,0 +1,95 @@
1
+
2
+ $LOAD_PATH.unshift './lib'
3
+ require 'em-xmpp'
4
+ require 'em-xmpp/namespaces'
5
+ require 'em-xmpp/nodes'
6
+
7
+ if ARGV.size < 3
8
+ puts "usage: #{__FILE__} <jid> <pass> <muc> [<invitees>...]"
9
+ exit 0
10
+ end
11
+
12
+ jid = ARGV.first
13
+ pass = ARGV[1]
14
+ muc = ARGV[2]
15
+ invitees = ARGV[3 .. -1]
16
+
17
+ include EM::Xmpp::Namespaces
18
+ include EM::Xmpp::Nodes
19
+
20
+ module MyClient
21
+ def join_muc(muc, login='myclient')
22
+ p "joining muc: #{muc}"
23
+ muc = [muc, login].join('/')
24
+ send_stanza presence_stanza('to'=> muc, 'id' => 'join.muc')
25
+ end
26
+
27
+ def leave_muc(muc, login='myclient')
28
+ p "leaving muc: #{muc}"
29
+ muc = [muc, login].join('/')
30
+ send_stanza(presence_stanza('to'=> muc, 'id' => 'leave.muc', 'type' => 'unavailable')) do |ctx|
31
+ p "left muc"
32
+ ctx
33
+ end
34
+ end
35
+
36
+ def invite_to_muc(invitee, muc, text)
37
+ p "inviting #{invitee} to #{muc}"
38
+ send_stanza(message_stanza('to' => muc, 'type' => 'normal') do |xml|
39
+ xml.x(:xmlns => 'http://jabber.org/protocol/muc#user') do |x|
40
+ x.invite(:to => invitee) do |invite|
41
+ invite.reason do |reason|
42
+ reason.text text
43
+ end
44
+ end
45
+ end
46
+ end)
47
+ end
48
+
49
+ attr_accessor :muc
50
+ attr_accessor :invitees
51
+
52
+ def ready
53
+ puts "***** #{@jid} ready for #{self}"
54
+
55
+ join_muc(muc)
56
+
57
+ on('//xmlns:x', 'xmlns' => EM::Xmpp::Namespaces::MucUser) do |ctx|
58
+ muc = ctx.jid.bare if ctx.jid
59
+ p ctx.jid
60
+ p ctx.affiliation
61
+ p ctx.role
62
+ if ctx.status
63
+ invitees.each do |invitee|
64
+ p invitee
65
+ invite_to_muc muc, invitee, "hello, join my MUC"
66
+ end
67
+ end
68
+ ctx
69
+ end
70
+
71
+ on_message do |ctx|
72
+ p "message"
73
+ p ctx.nickname if ctx.respond_to?(:nickname)
74
+ leave_muc(muc) if ctx.body =~ /leave/ and not ctx.delay?
75
+ ctx
76
+ end
77
+
78
+ on_exception(:anything) do |ctx|
79
+ raise ctx['error']
80
+ end
81
+
82
+ on_presence do |s|
83
+ p "*presence> #{s.from} #{s.show} (#{s.status})"
84
+ p " has left" if s.entity_left?
85
+ s
86
+ end
87
+
88
+ end
89
+ end
90
+
91
+ EM.run do
92
+ conn = EM::Xmpp::Connection.start(jid, pass, MyClient)
93
+ conn.muc = muc
94
+ conn.invitees = invitees
95
+ end
data/samples/roster.rb ADDED
@@ -0,0 +1,41 @@
1
+
2
+
3
+ $LOAD_PATH.unshift './lib'
4
+ require 'em-xmpp'
5
+ require 'em-xmpp/namespaces'
6
+ require 'em-xmpp/nodes'
7
+
8
+ if ARGV.empty?
9
+ puts "usage: #{__FILE__} <jid> [<pass>]"
10
+ exit 0
11
+ end
12
+
13
+ jid = ARGV.first
14
+ pass = ARGV[1]
15
+
16
+ include EM::Xmpp::Namespaces
17
+ include EM::Xmpp::Nodes
18
+
19
+ module MyClient
20
+ def ask_roster
21
+ send_stanza(iq_stanza do |x|
22
+ x.query('xmlns' => Roster)
23
+ end) do |ctx|
24
+ p ctx.items
25
+ ctx
26
+ end
27
+ end
28
+
29
+ def ready
30
+ puts "***** #{@jid} ready for #{self}"
31
+ on_exception(:anything) do |ctx|
32
+ raise ctx['error']
33
+ end
34
+
35
+ ask_roster
36
+ end
37
+ end
38
+
39
+ EM.run do
40
+ EM::Xmpp::Connection.start(jid, pass, MyClient)
41
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-xmpp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-27 00:00:00.000000000Z
12
+ date: 2012-05-05 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
16
- requirement: &2153101140 !ruby/object:Gem::Requirement
16
+ requirement: &2160613180 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2153101140
24
+ version_requirements: *2160613180
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: nokogiri
27
- requirement: &2153100720 !ruby/object:Gem::Requirement
27
+ requirement: &2160612760 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *2153100720
35
+ version_requirements: *2160612760
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: ruby-sasl
38
- requirement: &2153100300 !ruby/object:Gem::Requirement
38
+ requirement: &2160612340 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *2153100300
46
+ version_requirements: *2160612340
47
47
  description: XMPP client for event machine
48
48
  email:
49
49
  - crapooze@gmail.com
@@ -67,8 +67,12 @@ files:
67
67
  - lib/em-xmpp/namespaces.rb
68
68
  - lib/em-xmpp/nodes.rb
69
69
  - lib/em-xmpp/resolver.rb
70
+ - lib/em-xmpp/stanza_handler.rb
71
+ - lib/em-xmpp/stanza_matcher.rb
70
72
  - lib/em-xmpp/version.rb
71
73
  - samples/echo.rb
74
+ - samples/muc.rb
75
+ - samples/roster.rb
72
76
  homepage: https://github.com/crapooze/em-xmpp
73
77
  licenses: []
74
78
  post_install_message: