em-xmpp 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  *.rbc
3
+ **.swp
3
4
  .bundle
4
5
  .config
5
6
  .yardoc
data/README.md CHANGED
@@ -27,6 +27,15 @@ Or install it yourself as:
27
27
 
28
28
  ## Usage
29
29
 
30
+ XMPP is a stateful asynchronous protocol. Hence, to operate an XMPP client, you
31
+ must be able to receive and send XMPP messages (called stanzas) as well as
32
+ maintain some sort of states at the same time. For this reason, EventMachine is
33
+ a good fit to write and XMPP-client in. EM::Xmpp implements a middleware to
34
+ write XMPP clients on top of EventMachine for the asyncrhonous network, and
35
+ uses Ruby fibers to encapsulate states. Most of the code could easily be
36
+ extracted to work with other backends than EventMachine (e.g., TCPSocket) but
37
+ it would be harder to remove Fibers.
38
+
30
39
  ### Connecting to an XMPP server
31
40
 
32
41
  Like many EventMachine libraries, you need to first create a module and pass it
@@ -60,6 +69,28 @@ Any handler can also throw :halt to interrupt the layering and all the handler
60
69
  removal operations. You should read the code to understand well what you skip
61
70
  by doing so.
62
71
 
72
+ Summarizing, when your connection receives a stanza, the stanza is encapsulated
73
+ in a context object and matched against context handlers. Default handlers
74
+ exist for the three main stanza types (presence, iq, and message). For example:
75
+
76
+ on_presence do |ctx|
77
+ some_operation1
78
+ ctx.env['foo'] = 'bar' #passes the 'bar' to the next stanza matcher
79
+ ctx #unmodified context
80
+ end
81
+
82
+ on_presence do |ctx|
83
+ some_operation2
84
+ ctx.env['foo'] #=> 'bar'
85
+ ctx.done! #next stanza matchers will not receive the context
86
+ end
87
+
88
+ on_presence do |ctx|
89
+ this_code_is_never_called
90
+ ctx
91
+ end
92
+
93
+
63
94
  You can use on(*xpath_args) to build a matcher for any XPath in the stanza.
64
95
  The arguments are passed verbatim to Nokogiri. A special argument is
65
96
  on(:anything) that will match any stanza (e.g., for logging). This is useful
@@ -72,13 +103,65 @@ can use the on_exception(:anything) method.
72
103
 
73
104
  See the ./samples directory for basic examples.
74
105
 
106
+ ### Interpreting incoming stanzas
107
+
108
+ Now that you know how to receive contexts, you also want to read content inside
109
+ the stanza. Contexts have a stanza method to get the Nokogiri representation of
110
+ the stanza XML node (remember that stanzas are XML nodes of an XML stream).
111
+ Therefore, you can read any node/attribute in any XML namespace of the original
112
+ XML node. This way, you have a large control on what to read and you can
113
+ implement XEPs not covered in this piece of code (please share your code).
114
+
115
+ EM::Xmpp provides some level of abstraction to handle incoming stanzas that can
116
+ support multiple XEPs. Since a single stanza can carry lots of different XEPs,
117
+ single inheritence is not a beautiful option. There are two solutions with a
118
+ slightly different cost-model (expressive+slow and slightly-verbose+fast).
119
+
120
+ A first solution is to extend each context with methods (one Ruby module per
121
+ XEP). Unfortunately, extending Ruby objects is expensive in terms of method
122
+ cache. Extend lets you write code that clearly expresses your intention at the
123
+ expense of some slowness.
124
+
125
+ on_message do |ctx|
126
+ ctx.with(:message) # extends with EM::Xmpp::Context::Contexts::Message
127
+ ctx.with(:mood) # Mood
128
+ #then lets you write:
129
+ puts ctx.from
130
+ puts ctx.body
131
+ puts ctx.mood
132
+ ctx
133
+ end
134
+ This is the "Contexts" method.
135
+
136
+ A less expensive technique is to create, on demand, some delegators objects for
137
+ every XEPs. Therefore you must always prepend a method call to name the XEP you
138
+ use. We call this method the "Bits" method. Because we support XEPs by bits.
139
+
140
+ on_message do |ctx|
141
+ message = ctx.bit!(:message) # delegate to a EM::Xmpp::Context::Bits::Mood
142
+ mood = ctx.bit!(:mood) # Mood
143
+ #then lets you write:
144
+ puts message.from
145
+ puts message.body
146
+ puts mood.mood
147
+ ctx
148
+ end
149
+
150
+ My preference now goes for the Bits method. Hence, ctx.with will also generate
151
+ and cache a Bits object. The reasons why I keep both APIs are (a) backward
152
+ compatibility (b) forces implementers of XEPs to write the methods for
153
+ Context::Bits in clean modules. In the future, we might implement ctx.with
154
+ with Ruby refinements.
155
+
156
+
75
157
  ### Sending stanzas
76
158
 
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.
159
+ It is good to receive stanza and interpret them, but sometimes you also want to
160
+ send data. EM::XMPP for now builds stanzas with Nokogiri::XML::Builder in the
161
+ form with an explicit block argument. Then you send raw_strings with
162
+ Connection#send_raw or pre-built stanzas with Connection#send_stanza. Note
163
+ that if you send malformed XML, your server will disconnect you. Hence, take
164
+ care when writing XML without and XML builder.
82
165
 
83
166
  Contexts for message/iq/presence provide a "reply" method that will pre-fill
84
167
  some fields for you. Otherwise, you can use
@@ -96,23 +179,79 @@ other XEPs, do not forget to set your namespaces.
96
179
  The XML::Builder uses method_missing and hence this building scheme may be slow
97
180
  if you need a large throughput of outgoing stanzas.
98
181
 
99
- See the ./samples directory for basic examples.
100
-
101
- ## Features and Missing
182
+ Sometimes, you expect an answer when sending a stanza. For example, an IQ
183
+ result will come back with the same "id" attribute than the IQ query triggering
184
+ the result. For this specific use case, send_stanza can take a callback as
185
+ block parameter.
186
+ The syntax becomes:
102
187
 
103
- This library does not manage the roster for you. You will have to
104
- do this by hand.
188
+ one_time_handler = send_stanza data do |response_ctx|
189
+ ...
190
+ end
105
191
 
106
- We do not support but plan to support anonymous login yet.
192
+ Using this syntax will install a one-time handler in the connection handler.
193
+ Currently, there is no timeout on this timer. Therfore, you should get the
194
+ value of the one-time handler and you should remove it yourself if the handler
195
+ never matches any stanza fires.
107
196
 
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. .
197
+ See the ./samples directory for basic examples.
115
198
 
199
+ ## Features
200
+
201
+ ### Entities
202
+ XMPP defines entities as something with a JID and with which you can interact with.
203
+ In EM::Xmpp, each "from" or "to" field encapsulates the JID into an entity object.
204
+ This lets you write something such as:
205
+
206
+ on_presence do |ctx|
207
+ pre = ctx.bit!(:presence)
208
+ entity = pre.from #here is your entity object
209
+ if pre.subscription_request?
210
+ send_stanza pre.reply('type' => 'subscribed')
211
+ #here are the nice helper methods
212
+ entity.subscribe
213
+ entity.say "hello my new friend"
214
+ entity.add_to_roster
215
+ end
216
+ ctx
217
+ end
218
+
219
+ ### Stateful Conversations
220
+ XMPP is inherently asynchronous. Hence, handling
221
+ stateful actions (for example, some XEPs have long request/response flow
222
+ charts) can become tricky. Fortunately, EM::Xmpp proposes an abstraction called
223
+ Conversation to manage. Under the hood, a Conversation is not much more than a
224
+ Fiber plus some wiring. So far, EM::Xmpp does not route stanza to the
225
+ conversations automagically. You must do this by hand.
226
+
227
+ For short-lived conversations, when you know that an entity should answer to
228
+ your stanza with a reply stanza (and with same "id" attribute), use the block
229
+ argument of send_stanza.
230
+
231
+ ### Roster management
232
+ This library provides helpers to add/remove entities from your roster as well
233
+ as helpers to get the roster as a list of contacts.
234
+
235
+ ### Muc
236
+ Good support to join/leave/invite/kick/ban/voice/unvoice/chat with people in MUC rooms.
237
+
238
+ ### PubSub
239
+ Partial support. You can subscribe/unsubscribe/publish/receive items, configure
240
+ subscription or node parameters. The missing part is the on accepting PubSub
241
+ subscriptions.
242
+
243
+ ### File Transfer
244
+ There is enough to support for IBB file transfers.
245
+
246
+ ## Missing
247
+
248
+ * anonymous login
249
+ * login as XMPP component
250
+ * obnoxious SASL schemes such as X-GOOGLE-TOKEN (should patch ruby-sasl gem)
251
+
252
+ ## FAQ
253
+
254
+ Ask your questions via GitHub "issues".
116
255
 
117
256
  ## Contributing
118
257
 
@@ -0,0 +1,1099 @@
1
+ $LOAD_PATH.unshift './lib'
2
+ require 'em-xmpp'
3
+ require 'em-xmpp/helpers'
4
+ require 'em-xmpp/conversation'
5
+
6
+
7
+ if ARGV.empty?
8
+ puts "usage: #{__FILE__} <jid> <pass|passfile> <cmd>"
9
+ exit 0
10
+ end
11
+
12
+ jid = ARGV.first
13
+ pass = if File.file? ARGV[1]
14
+ File.read ARGV[1]
15
+ else
16
+ ARGV[1]
17
+ end
18
+
19
+ class CommandParseError < ArgumentError
20
+ end
21
+
22
+ module Command extend self
23
+
24
+ def parse_formdata(allfields)
25
+ allfields.split('&').map do |onefield|
26
+ var,type,*values = onefield.split(',')
27
+ EM::Xmpp::Context::Contexts::Dataforms::Field.new(var,type,nil,values)
28
+ end
29
+ end
30
+
31
+ def print_dataform(form)
32
+ puts "# #{form.title}" if form.title
33
+ puts "#{form.instructions}" if form.instructions
34
+ form.fields.each_with_index do |field,fidx|
35
+ puts "#{fidx}) #{field.label || '_'} (#{field.var}/#{field.type})"
36
+ letter = 'a'
37
+ field.options.each do |opt|
38
+ puts "#{fidx}.#{letter}) #{opt.value} -- #{opt.label || '_'}"
39
+ letter = letter.succ
40
+ end
41
+ field.values.each do |val|
42
+ puts "> #{val}"
43
+ letter = letter.succ
44
+ end
45
+ end
46
+ end
47
+
48
+ def print_context_dataforms(ctx)
49
+ df = ctx.bit!(:dataforms)
50
+ df.x_forms.each do |form|
51
+ print_dataform form
52
+ end
53
+ end
54
+
55
+
56
+ def for(str)
57
+ cmd,param = str.split(':',2)
58
+ case cmd
59
+
60
+ when 'quit'
61
+ lambda do |client|
62
+ client.quit
63
+ end
64
+
65
+ when 'set'
66
+ key,val = param.split(':', 2)
67
+ lambda do |client|
68
+ client.set(key,val)
69
+ end
70
+
71
+ when 'roster','list-roster','show-roster'
72
+ lambda do |client|
73
+ roster = client.get_roster
74
+ puts "Buddy list:"
75
+ groups = roster.map(&:groups).flatten.uniq
76
+ puts "==== No Group ==="
77
+ items = roster.select{|i| i.groups.empty?}
78
+ items.each do |item|
79
+ puts "#{item.name||item.jid} -- #{item.jid} (#{item.type})"
80
+ end
81
+
82
+ groups.each do |group|
83
+ puts "==== #{group} === "
84
+ items = roster.select{|i| i.groups.include?(group)}
85
+ items.each do |item|
86
+ puts "#{item.jid} -- #{item.name} (#{item.type})"
87
+ end
88
+ end
89
+ end
90
+
91
+ when 'unsubscribe', 'unsubscribe-from'
92
+ lambda do |client|
93
+ puts "unsubscribing from: #{param}"
94
+ client.entity(param).unsubscribe
95
+ client.entity(param).remove_from_roster
96
+ end
97
+
98
+ when 'subscribe', 'subscribe-to'
99
+ lambda do |client|
100
+ puts "subscribing to: #{param}"
101
+ client.entity(param).subscribe
102
+ client.entity(param).add_to_roster
103
+ end
104
+
105
+ when 'subscribed', 'accept-subscription'
106
+ lambda do |client|
107
+ puts "accept subscription from: #{param}"
108
+ client.entity(param).accept_subscription
109
+ end
110
+
111
+ ### PUBSUB
112
+ ###### Service user
113
+
114
+ when 'pubsub-service-subscriptions'
115
+ jid,node = param.split(':',2)
116
+ lambda do |client|
117
+ puts "=== PubSub Subscriptions on #{jid} (#{node}) ==="
118
+ ctx = client.entity(jid).pubsub(node).service_subscriptions
119
+ ctx.bit!(:pubsub).subscriptions.each do |s|
120
+ puts "#{s.node} -- #{s.subscription} (#{s.sub_id})"
121
+ end
122
+ end
123
+ when 'pubsub-service-affiliations'
124
+ jid,node = param.split(':',2)
125
+ lambda do |client|
126
+ puts "=== PubSub Affiliations on #{jid} (#{node}) ==="
127
+ ctx = client.entity(jid).pubsub(node).service_affiliations
128
+ ctx.bit!(:pubsub).affiliations.each do |s|
129
+ puts "#{s.node} (#{s.affiliation})"
130
+ end
131
+ end
132
+ when 'psubscribe','pubsub-subscribe'
133
+ jid,node = param.split(':',2)
134
+ lambda do |client|
135
+ puts "subscribing to PubSub: #{jid} (#{node})"
136
+ client.entity(jid).pubsub(node).subscribe
137
+ end
138
+ when 'punsubscribe','pubsub-unsubscribe'
139
+ jid,node,subid = param.split(':',3)
140
+ lambda do |client|
141
+ puts "unsubscribing from PubSub: #{jid} (#{node}:#{subid})"
142
+ client.entity(jid).pubsub(node).unsubscribe(subid)
143
+ end
144
+ when 'subscription-options'
145
+ jid,node,subid = param.split(':',3)
146
+ lambda do |client|
147
+ puts "listing options of subscription on: #{jid} (#{node})"
148
+ ctx = client.entity(jid).pubsub(node).subscription_options(subid)
149
+ puts "=== PubSub subscription options form ==="
150
+ Command.print_context_dataforms ctx
151
+ end
152
+ when 'pubsub-subscription-default-options'
153
+ jid,node = param.split(':',2)
154
+ lambda do |client|
155
+ puts "listing default subscription options of: #{jid} (#{node})"
156
+ entity = client.entity(jid).pubsub
157
+ entity = entity.node(node) if node
158
+ ctx = entity.default_subscription_configuration
159
+ puts "=== PubSub default subscription options ==="
160
+ Command.print_context_dataforms ctx
161
+ end
162
+ when 'pubsub-configure-subscription'
163
+ jid,node,formdata = param.split(':',3)
164
+ lambda do |client|
165
+ fields = Command.parse_formdata formdata
166
+ form = EM::Xmpp::Context::Contexts::Dataforms::Form.new('submit', fields)
167
+ puts "configuring #{fields.size} fields of subscription: #{jid} (#{node})"
168
+ ctx = client.entity(jid).pubsub(node).configure_subscription(form)
169
+ end
170
+ when 'pubsub-items'
171
+ jid,node = param.split(':',2)
172
+ lambda do |client|
173
+ puts "request items from PubSub: #{jid} (#{node})"
174
+ client.entity(jid).pubsub(node).items
175
+ end
176
+
177
+ ###### Node owner
178
+
179
+ when 'publish'
180
+ jid,node,payload = param.split(':',3)
181
+ payload ||= "empty-payload"
182
+ lambda do |client|
183
+ puts "publishing #{payload.size} bytes to #{jid} (#{node})"
184
+ ctx = client.entity(jid).pubsub(node).publish(payload)
185
+ ctx.bit!(:pubsub).items.each do |item|
186
+ puts "published: #{item.item_id} at #{item.node}"
187
+ end
188
+ end
189
+ when 'retract'
190
+ jid,node,item_id = param.split(':',3)
191
+ lambda do |client|
192
+ puts "retracting #{item_id} from #{jid} (#{node})"
193
+ ctx = client.entity(jid).pubsub(node).retract(item_id)
194
+ end
195
+ when 'create'
196
+ jid,node = param.split(':',2)
197
+ lambda do |client|
198
+ puts "creating PubSub node: #{jid} (#{node})"
199
+ client.entity(jid).pubsub(node).create
200
+ end
201
+ when 'purge'
202
+ jid,node = param.split(':',2)
203
+ lambda do |client|
204
+ puts "purging PubSub node: #{jid} (#{node})"
205
+ client.entity(jid).pubsub(node).purge
206
+ end
207
+ when 'delete'
208
+ jid,node,uri = param.split(':',3)
209
+ lambda do |client|
210
+ puts "delete PubSub node: #{jid} (#{node})"
211
+ client.entity(jid).pubsub(node).delete(uri)
212
+ end
213
+ when 'pubsub-node-subscriptions'
214
+ jid,node = param.split(':',2)
215
+ lambda do |client|
216
+ puts "=== PubSub Subscriptions on #{jid} ==="
217
+ ctx = client.entity(jid).pubsub(node).subscriptions
218
+ ctx.bit!(:pubsubowner).subscriptions.each do |s|
219
+ puts "#{s.jid} -- #{s.subscription} (#{s.sub_id})"
220
+ end
221
+ end
222
+ when 'pubsub-node-affiliations'
223
+ jid,node = param.split(':',2)
224
+ lambda do |client|
225
+ puts "=== PubSub Affiliations on #{jid} ==="
226
+ ctx = client.entity(jid).pubsub(node).affiliations
227
+ ctx.bit!(:pubsubowner).affiliations.each do |s|
228
+ puts "#{s.jid} (#{s.affiliation})"
229
+ end
230
+ end
231
+
232
+ when 'pubsub-node-options'
233
+ jid,node = param.split(':',2)
234
+ lambda do |client|
235
+ puts "listing PubSub node configuration options: #{jid} (#{node})"
236
+ ctx = client.entity(jid).pubsub(node).configuration_options
237
+ puts "=== PubSub node configuration options form ==="
238
+ Command.print_context_dataforms ctx
239
+ end
240
+
241
+ when 'pubsub-configure-node'
242
+ jid,node = param.split(':',2)
243
+ lambda do |client|
244
+ ctx = client.entity(jid).pubsub(node).configuration_options
245
+ client.default_dataform_conversation(ctx,"=== Configuring PubSub node ===") do |proceed,state|
246
+ client.entity(jid).pubsub(node).configure(state.answers) if proceed
247
+ end
248
+ end
249
+
250
+ when 'pubsub-node-default-options'
251
+ jid,node = param.split(':',2)
252
+ lambda do |client|
253
+ puts "listing default PubSub node configuration options: #{jid} (#{node})"
254
+ ctx = client.entity(jid).pubsub(node).default_configuration
255
+ puts "=== PubSub default node configuration options form ==="
256
+ Command.print_context_dataforms ctx
257
+ end
258
+
259
+ when 'pubsub-send-node-configuration'
260
+ jid,node,formdata = param.split(':',3)
261
+ lambda do |client|
262
+ fields = Command.parse_formdata formdata
263
+ form = EM::Xmpp::Context::Contexts::Dataforms::Form.new('submit', fields)
264
+ puts "configuring #{fields.size} fields of node: #{jid} (#{node})"
265
+ ctx = client.entity(jid).pubsub(node).configure(form)
266
+ end
267
+
268
+ when 'pubsub-node-change-affiliation'
269
+ jid,node,affilee,aff = param.split(':',4)
270
+ lambda do |client|
271
+ puts "changing affiliation of #{affilee} to #{aff} on #{jid} (#{node})"
272
+ affiliation = EM::Xmpp::Context::Contexts::PubsubMain::Affiliation.new(affilee,node,aff)
273
+ ctx = client.entity(jid).pubsub(node).modify_affiliations([affiliation])
274
+ end
275
+
276
+ when 'pubsub-node-change-subscription'
277
+ jid,node,subscribee,sub_type,subid = param.split(':',5)
278
+ lambda do |client|
279
+ puts "changing subscription of #{subscribee} (#{subid}) to #{sub_type} on #{jid} (#{node})"
280
+ subscription = EM::Xmpp::Context::Contexts::PubsubMain::Subscription.new(subscribee,node,sub_type,subid)
281
+ ctx = client.entity(jid).pubsub(node).modify_subscriptions([subscription])
282
+ end
283
+
284
+ when 'pubsub-node-delete-affiliation'
285
+ jid,node,affilee = param.split(':',3)
286
+ lambda do |client|
287
+ puts "removing affiliation of #{affilee} on #{jid} (#{node})"
288
+ ctx = client.entity(jid).pubsub(node).delete_affiliations(affilee)
289
+ end
290
+
291
+ when 'pubsub-node-delete-subscription'
292
+ jid,node,subscribee,subid = param.split(':',4)
293
+ lambda do |client|
294
+ puts "deleting subscription of #{subscribee} (#{subid}) on #{jid} (#{node})"
295
+ ctx = client.entity(jid).pubsub(node).delete_subscriptions(subscribee,subid)
296
+ end
297
+
298
+ ### MUC
299
+
300
+ when 'join-muc'
301
+ muc,nick = param.split(':',2)
302
+ lambda do |client|
303
+ puts "joining MUC: #{muc} as #{nick}"
304
+ client.entity(muc).muc.join(nick)
305
+ end
306
+
307
+ when 'leave-muc', 'part'
308
+ muc,nick = param.split(':',2)
309
+ lambda do |client|
310
+ puts "leaving MUC: #{muc}"
311
+ client.entity(muc).muc.part(nick)
312
+ end
313
+
314
+ when 'invite-to-muc'
315
+ muc,nick = param.split(':',2)
316
+ lambda do |client|
317
+ puts "inviting #{nick} to #{muc}"
318
+ client.entity(muc).muc.invite(nick)
319
+ end
320
+
321
+ when 'say', 'tell', 'message', 'msg'
322
+ dst,msg = param.split(':',2)
323
+ lambda do |client|
324
+ puts "speaking to: #{dst}"
325
+ client.entity(dst).say(msg)
326
+ end
327
+
328
+ when 'buzzmsg'
329
+ dst,msg = param.split(':',2)
330
+ lambda do |client|
331
+ puts "speaking to: #{dst}"
332
+ client.entity(dst).say(msg,'chat',lambda{|xml| xml.attention(:xmlns => EM::Xmpp::Namespaces::Attention)})
333
+ end
334
+
335
+ when 'avatar'
336
+ path = param
337
+ lambda do |client|
338
+ mime ||= "image/png" #TODO get mime
339
+ dat = File.read(path)
340
+ item = EM::Xmpp::Entity::Avatar::Item.new(nil,dat,16,16,mime)
341
+ puts "sending avatar"
342
+ ctx = client.jid.bare.avatar.publish item
343
+ end
344
+ when 'delete-avatar'
345
+ lambda do |client|
346
+ puts "sending avatar"
347
+ ctx = client.jid.bare.avatar.remove
348
+ end
349
+
350
+ when 'smiley'
351
+ dst,path,mime = param.split(':',2)
352
+ lambda do |client|
353
+ mime ||= "image/png" #TODO get mime
354
+ dat = File.read(path)
355
+ item = EM::Xmpp::Context::Contexts::Bob::Item.new(client.jid,dat,mime,65535)
356
+
357
+ puts "sending smiley to: #{dst}"
358
+ xmlproc = lambda do |xml|
359
+ xml.body "smiley"
360
+ xml.html(:xmlns => EM::Xmpp::Namespaces::XhtmlIM) do |html|
361
+ html.body(:xmlns => 'http://www.w3.org/1999/xhtml') do |body|
362
+ body.img('alt' => 'smiley', 'src'=>"cid:#{item.cid}")
363
+ end
364
+ end
365
+ end
366
+
367
+ client.entity(dst).say("",'chat', xmlproc)
368
+ client.on_iq do |ctx|
369
+ bob = ctx.bit(:bob)
370
+ if bob and (bob.cid == item.cid)
371
+ puts "sending bob"
372
+ iq = bob.reply(item)
373
+ client.send_stanza iq
374
+ ctx.delete_xpath_handler!
375
+ ctx.done!
376
+ end
377
+ ctx
378
+ end
379
+ end
380
+
381
+
382
+ when 'gsay', 'gtell', 'gmsg'
383
+ dst,msg = param.split(':',2)
384
+ lambda do |client|
385
+ puts "speaking in MUC: #{dst}"
386
+ client.entity(dst).muc.say(msg)
387
+ end
388
+
389
+ when 'motd','subject'
390
+ dst,msg = param.split(':',2)
391
+ lambda do |client|
392
+ puts "setting motd of: #{dst}"
393
+ client.entity(dst).muc.motd(msg)
394
+ end
395
+
396
+ when 'nickname','nick'
397
+ dst,name = param.split(':',2)
398
+ lambda do |client|
399
+ puts "using nickname: #{dst}"
400
+ client.entity(dst).muc.change_nick(name)
401
+ end
402
+
403
+ when 'kick'
404
+ dst,user = param.split(':',2)
405
+ lambda do |client|
406
+ puts "kicking #{user}"
407
+ client.entity(dst).muc.kick(user)
408
+ end
409
+
410
+ when 'ban'
411
+ dst,user = param.split(':',2)
412
+ lambda do |client|
413
+ puts "banning #{user}"
414
+ client.entity(dst).muc.ban(user)
415
+ end
416
+
417
+ when 'unban'
418
+ dst,user = param.split(':',2)
419
+ lambda do |client|
420
+ puts "unbanning #{user}"
421
+ client.entity(dst).muc.unban(user)
422
+ end
423
+
424
+ when 'moderator'
425
+ dst,user = param.split(':',2)
426
+ lambda do |client|
427
+ puts "moddeerating #{user}"
428
+ client.entity(dst).muc.moderator(user)
429
+ end
430
+
431
+ when 'unmoderator'
432
+ dst,user = param.split(':',2)
433
+ lambda do |client|
434
+ puts "unmoderating #{user}"
435
+ client.entity(dst).muc.unmoderator(user)
436
+ end
437
+
438
+ when 'admin'
439
+ dst,user = param.split(':',2)
440
+ lambda do |client|
441
+ puts "admin #{user}"
442
+ client.entity(dst).muc.admin(user)
443
+ end
444
+
445
+ when 'unadmin'
446
+ dst,user = param.split(':',2)
447
+ lambda do |client|
448
+ puts "unadmin #{user}"
449
+ client.entity(dst).muc.unadmin(user)
450
+ end
451
+
452
+ when 'owner'
453
+ dst,user = param.split(':',2)
454
+ lambda do |client|
455
+ puts "new owner #{user}"
456
+ client.entity(dst).muc.owner(user)
457
+ end
458
+
459
+ when 'unowner'
460
+ dst,user = param.split(':',2)
461
+ lambda do |client|
462
+ puts "remove owner #{user}"
463
+ client.entity(dst).muc.unowner(user)
464
+ end
465
+
466
+ when 'voice'
467
+ dst,user = param.split(':',2)
468
+ lambda do |client|
469
+ puts "voicing #{user}"
470
+ client.entity(dst).muc.voice(user)
471
+ end
472
+
473
+ when 'unvoice'
474
+ dst,user = param.split(':',2)
475
+ lambda do |client|
476
+ puts "unvoicing #{user}"
477
+ client.entity(dst).muc.unvoice(user)
478
+ end
479
+
480
+ when 'disco-infos','infos'
481
+ dst,node = param.split(':',2)
482
+ lambda do |client|
483
+ puts "discovering infos for: #{dst} (node:#{node})"
484
+ disco = client.entity(dst).discover_infos(node)
485
+
486
+ puts "=== Identities ==="
487
+ disco.bit!(:discoinfos).identities.each do |i|
488
+ puts "#{i.category}/#{i.type}: #{i.name || "_"}"
489
+ end
490
+ puts "=== Features ==="
491
+ disco.bit!(:discoinfos).features.each do |f|
492
+ puts "#{f.var}"
493
+ end
494
+ end
495
+
496
+ when 'disco-items','items'
497
+ dst,node = param.split(':',2)
498
+ lambda do |client|
499
+ puts "discovering items for: #{dst} (node:#{node})"
500
+ disco = client.entity(dst).discover_items(node)
501
+
502
+ puts "=== Items ==="
503
+ disco.bit!(:discoitems).items.each do |i|
504
+ puts "#{i.entity} (#{i.node}) -- #{i.name || i.entity}"
505
+ end
506
+ end
507
+
508
+ # file-transfer
509
+ when 'send-file'
510
+ jid,path,txt,formdata = param.split(':',3)
511
+ sid = "ft_#{rand(65535)}"
512
+ lambda do |client|
513
+ form = if formdata
514
+ puts "configuring #{fields.size} fields to propose transfer: #{jid}"
515
+ fields = Command.parse_formdata formdata
516
+ EM::Xmpp::Context::Contexts::Dataforms::Form.new('submit', fields)
517
+ else
518
+ puts "using default form"
519
+ fields = [EM::Xmpp::Context::Contexts::Dataforms::Field.new(
520
+ 'stream-method',
521
+ 'list-single',
522
+ nil, #user-friendly label
523
+ [], #current values
524
+ [EM::Xmpp::Namespaces::IBB] #available options
525
+ )
526
+ ]
527
+ EM::Xmpp::Context::Contexts::Dataforms::Form.new('submit', fields)
528
+ end
529
+ desc = EM::Xmpp::Entity::Transfer.describe_file path
530
+ desc[:description] = txt
531
+ ctx = client.entity(jid).transfer.negotiation_request(desc,sid,form)
532
+ if ctx.bit(:iq).type == 'result'
533
+ method = ctx.bit(:dataforms).x_forms.first.fields.find{|f| f.var == 'stream-method'}
534
+ puts "accepted transfer via #{method.value}" if method
535
+ case method.value
536
+ when EM::Xmpp::Namespaces::IBB
537
+ key = client.ibb_conversation_key_for_sid(ctx.bit(:stanza), sid)
538
+ ctx.env['path'] = path
539
+ ctx.env['stream.id'] = sid
540
+ ctx.env['peer'] = ctx.bit(:iq).from
541
+ client.ibb_ul_conversation(ctx,key) do |step|
542
+ step.on(:start) do
543
+ puts "=== Starting Upload ==="
544
+ end
545
+ step.on(:cancel) do
546
+ puts "=== Upload canceled ==="
547
+ end
548
+ step.on(:chunk) do |state|
549
+ puts "=== Outgoing chunk #{state.last_chunk.size} bytes ==="
550
+ end
551
+ step.on(:done) do
552
+ puts "=== Upload Finished ==="
553
+ end
554
+ end
555
+ else
556
+ puts "unsupported method, will have to cancel"
557
+ end
558
+ else
559
+ puts "refused"
560
+ end
561
+ end
562
+ when 'receive-file'
563
+ jid,id,formdata = param.split(':',3)
564
+ lambda do |client|
565
+ fields = Command.parse_formdata formdata
566
+ form = EM::Xmpp::Context::Contexts::Dataforms::Form.new('submit', fields)
567
+ puts "configuring #{fields.size} fields to accept transfer: #{jid}"
568
+ client.entity(jid).transfer.negotiation_reply(id,form)
569
+ end
570
+ when 'iq'
571
+ jid,iq_type,iq_id = param.split(':',3)
572
+ lambda do |client|
573
+ puts "sending iq (#{iq_type}/#{iq_id})"
574
+ args = {'to' => jid, 'type' => iq_type, 'id' => iq_id}
575
+ iq = client.iq_stanza(args)
576
+ client.send_stanza iq
577
+ end
578
+ when 'conversations'
579
+ lambda do |client|
580
+ client.conversations.keys.each do |conv|
581
+ puts conv
582
+ end
583
+ end
584
+ else
585
+ raise CommandParseError, "doesn't know such command: #{cmd}"
586
+ end
587
+ end
588
+ end
589
+
590
+ commands = ARGV[2 .. -1]
591
+
592
+ module RosterClient
593
+ include EM::Xmpp::Helpers
594
+ attr_reader :queue
595
+
596
+ def ready
597
+ @show_xml = false
598
+ puts "***** #{@jid} ready"
599
+ user_data.each{|c| handle_command c}
600
+ EM.open_keyboard Kb, self
601
+
602
+ # Writes a stack-trace on error
603
+ on_exception(:anything) do |ctx|
604
+ raise ctx['error']
605
+ end
606
+
607
+
608
+ # Signal presence subscriptions
609
+ on_presence do |ctx|
610
+ pre = ctx.bit!(:presence)
611
+ if pre.subscription_request?
612
+ puts "=== Presence subscription request from: #{pre.from.bare}"
613
+ ctx.done!
614
+ end
615
+ ctx
616
+ end
617
+
618
+ # Signal people arriving and leaving
619
+ on_presence do |ctx|
620
+ pre = ctx.bit!(:presence)
621
+ puts "=== Presence #{pre.from}: #{pre.type}"
622
+ ctx.done!
623
+ end
624
+
625
+ # Acknowledge roster change
626
+ on_iq do |ctx|
627
+ roster = ctx.bit(:roster)
628
+ if roster
629
+ if roster.type == 'set'
630
+ puts "=== Roster change ==="
631
+ roster.items.each do |item|
632
+ puts "#{item.name||item.jid} -- #{item.jid} (#{item.type})"
633
+ end
634
+ send_stanza roster.reply
635
+ ctx.done!
636
+ end
637
+ end
638
+ ctx
639
+ end
640
+
641
+ # Replies to item queries
642
+ on_iq do |ctx|
643
+ query = ctx.bit(:discoitems)
644
+ if query
645
+ list = items(query.to,query.node)
646
+ puts "==== #{query.from} discovers #{list.size} items at node #{query.node} ==="
647
+ reply = query.reply do |iq|
648
+ iq.query(:xmlns => EM::Xmpp::Namespaces::DiscoverItems,
649
+ :node => EM::Xmpp::Namespaces::Commands) do |q|
650
+ list.each do |item|
651
+ q.item('jid' => item.entity, 'node'=> item.node, 'name'=>item.name)
652
+ end
653
+ end
654
+ end
655
+ send_stanza reply
656
+ ctx.done!
657
+ end
658
+ ctx
659
+ end
660
+
661
+
662
+ # Replies to command queries
663
+ on_iq do |ctx|
664
+ query = ctx.bit(:command)
665
+
666
+ if query
667
+ if query.sessionid
668
+ key = "command:#{query.from}:#{query.node}:#{query.sessionid}"
669
+ conv = conversation(key)
670
+ conv.resume ctx if conv
671
+ #else returns an error
672
+ else
673
+ puts "=== Calling command on behalf of #{query.from} ==="
674
+ sess_id = "cmd:#{ctx.object_id}"
675
+ key = "command:#{query.from}:#{query.node}:#{sess_id}"
676
+
677
+ fields = [EM::Xmpp::Context::Contexts::Dataforms::Field.new(
678
+ 'stream-method',
679
+ 'list-single',
680
+ "Stream Method", #user-friendly label
681
+ ['foo'], #current values
682
+ ['opt1','opt2','opt3'] #available options
683
+ ),
684
+ EM::Xmpp::Context::Contexts::Dataforms::Field.new(
685
+ 'bar',
686
+ 'boolean',
687
+ "True/False", #user-friendly label
688
+ ['0'], #current values
689
+ [],
690
+ )
691
+ ]
692
+ form = EM::Xmpp::Context::Contexts::Dataforms::Form.new('form', fields,"please answer", "just fill-in the values")
693
+ result = EM::Xmpp::Context::Contexts::Dataforms::Form.new('result', fields)
694
+
695
+ step1 = CommandStep.new(form)
696
+ spec = LinearCommandsSpec.new([step1]) #actually pick it from query. node
697
+
698
+ start_command_conversation(ctx,key,sess_id,spec) do |step|
699
+ step.on(:start) do |state|
700
+ state.flash = CommandFlash.new :info, "some info"
701
+ end
702
+ step.on(:answer) do |state|
703
+ action = state.last_answer.ctx.bit(:command).action
704
+ puts "user answered (#{action})"
705
+ Command.print_context_dataforms state.last_answer.ctx
706
+ state.flash = CommandFlash.new :info, "some other info"
707
+ state.status = :executing
708
+ state.current_idx += 1
709
+ state.result = result
710
+ end
711
+ step.on(:cancel) do |state|
712
+ puts "cancelled"
713
+ end
714
+ end
715
+ end
716
+ ctx.done!
717
+ end
718
+ ctx
719
+ end
720
+
721
+ # Prints Pubsub event
722
+ on_message do |ctx|
723
+ event = ctx.bit(:pubsubevent)
724
+ delay = ctx.bit(:delay)
725
+ stamp = delay.stamp if delay
726
+ if event
727
+ puts "=== PubSub event #{event.service} #{stamp} ==="
728
+ if event.items_node #oddly enough, retractions fall in items_node
729
+ items = event.items
730
+ retractions = event.retractions
731
+
732
+ if items.any? or retractions.any?
733
+ items.each do |i|
734
+ puts "+item> #{i.node} #{i.item_id} (by #{i.publisher || 'N/A'})"
735
+ puts i.payload
736
+ end
737
+
738
+ event.retractions.each do |r|
739
+ puts "-item> #{r.node} #{r.item_id}"
740
+ end
741
+ else
742
+ puts "empty items list for #{event.node_id}"
743
+ end
744
+ end
745
+
746
+
747
+ if event.purge
748
+ puts "node purged #{event.purge.node}"
749
+ end
750
+
751
+ if event.deletion
752
+ del = event.deletion
753
+ puts "node destroyed #{del.node}"
754
+ puts " now redirects to #{del.redirect}" if del.redirect
755
+ end
756
+
757
+ if event.configuration
758
+ cfg = event.configuration
759
+ puts "new node configuration for #{cfg.node}"
760
+ Command.print_context_dataforms(ctx)
761
+ end
762
+
763
+ ctx.done!
764
+ end
765
+
766
+ event = ctx.bit(:pubsub)
767
+ if event
768
+ puts "=== PubSub #{event.service} ==="
769
+ event.subscriptions.each do |sub|
770
+ puts "subscription of #{sub.jid} (#{sub.sub_id}) status is now #{sub.subscription} for #{sub.node}"
771
+ puts "expires on #{sub.expiry}" if sub.expiry
772
+ end
773
+ ctx.done!
774
+ end
775
+ ctx
776
+ end
777
+
778
+ on_iq do |ctx|
779
+ si = ctx.bit(:streaminitiation)
780
+ if si
781
+ puts "=== FileTransfer request from #{si.from}:#{si.id} ==="
782
+ puts "file details: #{si.file_name} (#{si.file_size} bytes) (mime:#{si.mime_type})"
783
+ puts "description: #{si.description}" if si.description
784
+
785
+ default_dataform_conversation(ctx, "Negotiating stream") do |proceed,state|
786
+ if proceed
787
+ si.from.transfer.negotiation_reply(si.id, state.answers)
788
+ else
789
+ send_stanza si.reply(:type => 'error')
790
+ end
791
+ end
792
+
793
+ ctx.done!
794
+ end
795
+ ctx
796
+ end
797
+
798
+ on_iq do |ctx|
799
+ ibb = ctx.bit(:ibb)
800
+ if ibb
801
+ key = ibb_conversation_key(ibb)
802
+
803
+ if ibb.open_node
804
+ ibb_dl_conversation(ctx,key) do |step|
805
+ step.on(:start) do
806
+ puts "=== FileTransfer IBB starting (sid: #{ibb.sid}) ==="
807
+ end
808
+ step.on(:cancel) do
809
+ puts "=== Canceled FileTransfer (sid: #{ibb.sid}) ==="
810
+ end
811
+ step.on(:chunk) do |state|
812
+ puts "=== Incoming chunk (#{state.last_chunk.size} bytes) (sid:#{ibb.sid})"
813
+ end
814
+ step.on(:done) do
815
+ puts "=== FileTransfer IBB done (sid: #{ibb.sid}) ==="
816
+ end
817
+ end
818
+ else
819
+ conv = conversation(key)
820
+ conv.resume ctx if conv
821
+ end
822
+ ctx.done!
823
+ end
824
+ ctx
825
+ end
826
+ end
827
+
828
+ # list of supported items for the given node
829
+ def items(jid,node)
830
+ #TODO: correctly pick items from the jid/node name
831
+ [EM::Xmpp::Context::Contexts::Discoitems::Item.new(jid.full, 'cmd:1','a first command'),
832
+ EM::Xmpp::Context::Contexts::Discoitems::Item.new(jid.full, 'cmd:2','anoter command')]
833
+ end
834
+
835
+ def ibb_conversation_key(ibb)
836
+ ibb_conversation_key_for_sid ibb, ibb.sid
837
+ end
838
+
839
+ def ibb_conversation_key_for_sid(x,sid)
840
+ [ 'ibb', x.from , sid ].map(&:to_s).join('|')
841
+ end
842
+
843
+ IBBDLState = Struct.new(:buffer, :last_iq, :done, :last_chunk)
844
+ IBBULState = Struct.new(:jid, :sid, :chunk_size, :ptr, :chunk_idx, :done, :last_chunk, :closed)
845
+
846
+ def ibb_ul_conversation(initial_ctx,key,&blk)
847
+ path = initial_ctx.env['path']
848
+ sid = initial_ctx.env['stream.id']
849
+ jid = initial_ctx.env['peer']
850
+
851
+ #encode data
852
+ raw_data = File.read path
853
+ data = Base64.strict_encode64 raw_data
854
+
855
+ state = IBBULState.new(jid,sid,4096,0,0,false,false,false)
856
+
857
+ start_conversation(initial_ctx,key,state) do |conv|
858
+ conv.prepare_callbacks(:start, :cancel, :chunk, :done, &blk)
859
+
860
+ catch :interrupt do
861
+ # opens transfer
862
+ iq = iq_stanza(:type => 'set', 'to' => state.jid, 'id' => "tx.#{sid}.#{state.ptr}") do |xml|
863
+ xml.open('xmlns' => EM::Xmpp::Namespaces::IBB, 'block-size' => state.chunk_size, 'sid' => state.sid, 'stanza' => 'iq')
864
+ end
865
+ ret = conv.send_stanza iq, 10
866
+
867
+ if ret.interrupted?
868
+ rsp = ret.ctx.bit(:iq)
869
+ if rsp.type == 'error'
870
+ conv.callback :cancel
871
+ throw :interrupt
872
+ else
873
+ conv.callback :start
874
+ end
875
+ else
876
+ conv.callback :cancel
877
+ throw :interrupt
878
+ end
879
+
880
+ # uploads all chunk
881
+ until state.done or state.closed do
882
+ # send a chunk of encoded data and wait for an ack
883
+ blk = data.slice(state.ptr, state.chunk_size)
884
+ state.last_chunk = blk
885
+ iq = iq_stanza(:type => 'set', 'to' => state.jid, 'id' => "tx.#{sid}.#{state.ptr}") do |xml|
886
+ xml.data({'xmlns' => EM::Xmpp::Namespaces::IBB, 'sid' => state.sid, 'seq' => state.chunk_idx}, blk)
887
+ end
888
+
889
+ ret = conv.send_stanza iq
890
+ iq = ret.ctx.bit(:iq)
891
+ case iq.type
892
+ when 'result'
893
+ #nothing
894
+ when 'set'
895
+ ibb = ret.ctx.bit(:ibb)
896
+ if ibb.close_node #other end closes, but there is still a non-acknowledge pending iq
897
+ conv.send_stanza iq.reply, 1
898
+ state.closed = true
899
+ end
900
+ else
901
+ conv.callback :cancel
902
+ throw :interrupt
903
+ end
904
+
905
+ conv.callback :chunk
906
+ state.ptr += state.chunk_size
907
+ state.chunk_idx += 1
908
+ state.done = true if state.ptr > data.size
909
+ end
910
+
911
+ unless state.closed
912
+ # notify the end of the IBB
913
+ iq = iq_stanza(:type => 'set', 'to' => state.jid) do |xml|
914
+ xml.close('xmlns' => EM::Xmpp::Namespaces::IBB, 'sid' => state.sid)
915
+ end
916
+ conv.send_stanza iq, 1
917
+ end
918
+
919
+ conv.callback :done
920
+ end # catch :interrupt
921
+ end
922
+ end
923
+
924
+ def ibb_dl_conversation(initial_ctx,key,&blk)
925
+ state = IBBDLState.new("", initial_ctx.bit(:iq), false, nil)
926
+
927
+ start_conversation(initial_ctx,key,state) do |conv|
928
+ conv.prepare_callbacks(:start, :cancel, :chunk, :done, &blk)
929
+
930
+ conv.callback :start
931
+
932
+ until state.done do
933
+ # ack last iq
934
+ send_stanza state.last_iq.reply
935
+
936
+ # expect some data chunk, may wait forever
937
+ chunk_ctx = conv.delay.ctx
938
+ chunk = chunk_ctx.bit(:ibb)
939
+ state.last_iq = chunk_ctx.bit(:iq)
940
+
941
+ # we should either have a final node or some data
942
+ if chunk.close_node
943
+ state.done = true
944
+ conv.callback :done
945
+ else
946
+ state.last_chunk = chunk.data
947
+ state.buffer << chunk.data
948
+ conv.callback :chunk
949
+ end
950
+ end
951
+
952
+ send_stanza state.last_iq.reply
953
+ end
954
+ end
955
+
956
+ def default_dataform_conversation(ctx, banner, &action)
957
+ ctx.env['dataform'] = ctx.bit!(:dataforms).form
958
+ dataform_conversation(ctx) do |step|
959
+ step.on(:start) do
960
+ puts banner
961
+ end
962
+
963
+ step.on(:confirm) do |state|
964
+ puts "Ready to submit:"
965
+ Command.print_dataform state.answers
966
+ puts "Submit? Y/n"
967
+ end
968
+
969
+ step.on(:user_answer) do |state|
970
+ txt = state.user_input
971
+ case txt
972
+ when ':cancel'
973
+ :cancel
974
+ when ':prev',':previous'
975
+ :previous
976
+ when ':next',''
977
+ values = state.current_response_values
978
+ response_field = EM::Xmpp::Context::Contexts::Dataforms::Field.new(state.current_field.var,nil,nil,values,[])
979
+ state.answers.fields[state.current_idx] = response_field
980
+ :next
981
+ when /^:\d+\.(\w+)$/ #answers to a question by response ID
982
+ option_code = Regexp.last_match[1]
983
+ field = state.current_field
984
+ letter = 'a'
985
+ opt = field.options.find do |o|
986
+ found = letter == option_code
987
+ letter = letter.succ
988
+ found
989
+ end
990
+
991
+ if opt
992
+ values = [opt.value]
993
+ response_field = EM::Xmpp::Context::Contexts::Dataforms::Field.new(state.current_field.var,nil,nil,values,[])
994
+ state.answers.fields[state.current_idx] = response_field
995
+ :modified
996
+ else
997
+ :repeat
998
+ end
999
+ else
1000
+ values = [txt]
1001
+ response_field = EM::Xmpp::Context::Contexts::Dataforms::Field.new(state.current_field.var,nil,nil,values,[])
1002
+ state.answers.fields[state.current_idx] = response_field
1003
+ :modified
1004
+ end
1005
+ end
1006
+
1007
+ step.on(:ask) do |state|
1008
+ fidx = state.current_idx
1009
+ field = state.current_field
1010
+
1011
+ puts "#{fidx+1}/#{state.form.fields.count}) #{field.label || '_'} (#{field.var}/#{field.type})"
1012
+ letter = 'a'
1013
+ field.options.each do |opt|
1014
+ puts "#{fidx+1}.#{letter}) #{opt.value} -- #{opt.label || '_'}"
1015
+ letter = letter.succ
1016
+ end
1017
+
1018
+ if state.current_response_values.any?
1019
+ puts "Current response value:"
1020
+ state.current_response_values.each do |val|
1021
+ puts "#{val}"
1022
+ letter = letter.succ
1023
+ end
1024
+ end
1025
+ puts "Enter your choice:"
1026
+ end
1027
+
1028
+ step.on(:submit) do |state|
1029
+ if state.user_input == 'n'
1030
+ puts "Cancelled"
1031
+ action.call(false, state)
1032
+ else
1033
+ puts "Proceeding"
1034
+ action.call(true, state)
1035
+ end
1036
+ end
1037
+
1038
+ step.on(:cancel) do |state|
1039
+ puts "Cancelled"
1040
+ action.call(false, state)
1041
+ end
1042
+ end
1043
+ end
1044
+
1045
+ def stanza_end(stanza)
1046
+ puts stanza if @show_xml
1047
+ super
1048
+ end
1049
+
1050
+ def handle_command(str)
1051
+ begin
1052
+ if conversation(:dataform)
1053
+ conversation(:dataform).resume str
1054
+ else
1055
+ Command.for(str).call self unless str.empty?
1056
+ end
1057
+ rescue CommandParseError
1058
+ puts "could not parse #{str}"
1059
+ end
1060
+ end
1061
+
1062
+ def quit
1063
+ close_xml_stream
1064
+ close_connection
1065
+ end
1066
+
1067
+ def set(key,val)
1068
+ case key
1069
+ when '+xml'
1070
+ @show_xml = true
1071
+ when '-xml'
1072
+ @show_xml = false
1073
+ when '+debug'
1074
+ @show_xml = false
1075
+ $DEBUG = true
1076
+ when '-debug'
1077
+ $DEBUG = false
1078
+ end
1079
+ end
1080
+
1081
+ def unbind
1082
+ EM.stop
1083
+ end
1084
+ end
1085
+
1086
+ class Kb < EM::Connection
1087
+ include EM::Protocols::LineText2
1088
+ def initialize(client)
1089
+ @client = client
1090
+ end
1091
+ def receive_line line
1092
+ Fiber.new { @client.handle_command line.chomp}.resume
1093
+ end
1094
+ end
1095
+
1096
+ EM.run do
1097
+ EM::Xmpp::Connection.start(jid, pass, RosterClient, {:data => commands})
1098
+ puts "***** connecting as #{jid}"
1099
+ end