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 +96 -2
- data/lib/em-xmpp/connection.rb +35 -18
- data/lib/em-xmpp/context.rb +270 -4
- data/lib/em-xmpp/handler.rb +62 -28
- data/lib/em-xmpp/namespaces.rb +18 -0
- data/lib/em-xmpp/stanza_handler.rb +28 -0
- data/lib/em-xmpp/stanza_matcher.rb +22 -0
- data/lib/em-xmpp/version.rb +1 -1
- data/samples/echo.rb +34 -2
- data/samples/muc.rb +95 -0
- data/samples/roster.rb +41 -0
- metadata +12 -8
data/README.md
CHANGED
@@ -1,9 +1,18 @@
|
|
1
1
|
# Em::Xmpp
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
|
data/lib/em-xmpp/connection.rb
CHANGED
@@ -48,7 +48,7 @@ module EM::Xmpp
|
|
48
48
|
def negotiation_finished
|
49
49
|
@pass = nil
|
50
50
|
@handler = Routine.new self
|
51
|
-
|
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
|
-
|
59
|
+
OutgoingStanza = Struct.new(:xml, :params)
|
60
60
|
|
61
|
-
def
|
61
|
+
def default_presence_params
|
62
62
|
{}
|
63
63
|
end
|
64
64
|
|
65
|
-
def
|
65
|
+
def default_message_params
|
66
66
|
{'to' => @jid.domain, 'id' => "em-xmpp.#{rand(65535)}"}
|
67
67
|
end
|
68
68
|
|
69
|
-
def
|
69
|
+
def default_iq_params
|
70
70
|
{'type' => 'get', 'id' => "em-xmpp.#{rand(65535)}"}
|
71
71
|
end
|
72
72
|
|
73
|
-
def presence_stanza(
|
74
|
-
|
75
|
-
build_xml do |x|
|
76
|
-
x.presence(
|
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(
|
81
|
-
|
82
|
-
build_xml do |x|
|
83
|
-
x.message(
|
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(
|
88
|
-
|
89
|
-
build_xml do |x|
|
90
|
-
x.iq(
|
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
|
-
|
113
|
+
@handler.send meth, *args, &blk
|
97
114
|
end
|
98
115
|
end
|
99
116
|
|
data/lib/em-xmpp/context.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
data/lib/em-xmpp/handler.rb
CHANGED
@@ -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
|
-
@
|
21
|
-
@
|
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
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
-
|
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
|
111
|
+
# runs all handlers against the stanza context
|
73
112
|
# catches all exception (in which case, the context gets passed to all
|
74
|
-
#
|
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
|
-
@
|
122
|
+
@handlers = run_xpath_handlers ctx, @handlers.dup
|
84
123
|
end
|
85
124
|
rescue => err
|
86
125
|
ctx['error'] = err
|
87
|
-
@
|
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
|
-
|
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.
|
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.
|
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
|
data/lib/em-xmpp/namespaces.rb
CHANGED
@@ -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
|
data/lib/em-xmpp/version.rb
CHANGED
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
|
-
|
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
|
-
|
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.
|
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-
|
12
|
+
date: 2012-05-05 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: eventmachine
|
16
|
-
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: *
|
24
|
+
version_requirements: *2160613180
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: nokogiri
|
27
|
-
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: *
|
35
|
+
version_requirements: *2160612760
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: ruby-sasl
|
38
|
-
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: *
|
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:
|