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 +1 -0
- data/README.md +157 -18
- data/bin/xmig +1099 -0
- data/lib/em-xmpp/connection.rb +1 -0
- data/lib/em-xmpp/context.rb +444 -38
- data/lib/em-xmpp/conversation.rb +105 -0
- data/lib/em-xmpp/entity.rb +759 -31
- data/lib/em-xmpp/handler.rb +16 -0
- data/lib/em-xmpp/helpers.rb +207 -0
- data/lib/em-xmpp/jid.rb +2 -1
- data/lib/em-xmpp/namespaces.rb +25 -0
- data/lib/em-xmpp/version.rb +1 -1
- data/samples/hello.rb +25 -4
- metadata +12 -9
data/.gitignore
CHANGED
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
104
|
-
|
188
|
+
one_time_handler = send_stanza data do |response_ctx|
|
189
|
+
...
|
190
|
+
end
|
105
191
|
|
106
|
-
|
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
|
-
|
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
|
|
data/bin/xmig
ADDED
@@ -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
|