em-xmpp 0.0.1 → 0.0.2

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