em-xmpp 0.0.10 → 0.0.11
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/.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
|