vines 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README +34 -0
- data/Rakefile +55 -0
- data/bin/vines +95 -0
- data/conf/certs/README +32 -0
- data/conf/certs/ca-bundle.crt +3987 -0
- data/conf/config.rb +114 -0
- data/lib/vines.rb +155 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +49 -0
- data/lib/vines/command/init.rb +58 -0
- data/lib/vines/command/ldap.rb +35 -0
- data/lib/vines/command/restart.rb +12 -0
- data/lib/vines/command/schema.rb +24 -0
- data/lib/vines/command/start.rb +28 -0
- data/lib/vines/command/stop.rb +18 -0
- data/lib/vines/config.rb +191 -0
- data/lib/vines/contact.rb +99 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/jid.rb +56 -0
- data/lib/vines/kit.rb +23 -0
- data/lib/vines/router.rb +125 -0
- data/lib/vines/stanza.rb +55 -0
- data/lib/vines/stanza/iq.rb +50 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +25 -0
- data/lib/vines/stanza/iq/disco_items.rb +23 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +153 -0
- data/lib/vines/stanza/iq/session.rb +22 -0
- data/lib/vines/stanza/iq/vcard.rb +58 -0
- data/lib/vines/stanza/message.rb +41 -0
- data/lib/vines/stanza/presence.rb +119 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +38 -0
- data/lib/vines/stanza/presence/subscribe.rb +66 -0
- data/lib/vines/stanza/presence/subscribed.rb +64 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +57 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +50 -0
- data/lib/vines/storage.rb +216 -0
- data/lib/vines/storage/couchdb.rb +119 -0
- data/lib/vines/storage/ldap.rb +59 -0
- data/lib/vines/storage/local.rb +66 -0
- data/lib/vines/storage/redis.rb +108 -0
- data/lib/vines/storage/sql.rb +174 -0
- data/lib/vines/store.rb +51 -0
- data/lib/vines/stream.rb +198 -0
- data/lib/vines/stream/client.rb +131 -0
- data/lib/vines/stream/client/auth.rb +94 -0
- data/lib/vines/stream/client/auth_restart.rb +33 -0
- data/lib/vines/stream/client/bind.rb +58 -0
- data/lib/vines/stream/client/bind_restart.rb +25 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +15 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +37 -0
- data/lib/vines/stream/component.rb +53 -0
- data/lib/vines/stream/component/handshake.rb +25 -0
- data/lib/vines/stream/component/ready.rb +24 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/http.rb +111 -0
- data/lib/vines/stream/http/http_request.rb +22 -0
- data/lib/vines/stream/http/http_state.rb +139 -0
- data/lib/vines/stream/http/http_states.rb +53 -0
- data/lib/vines/stream/parser.rb +78 -0
- data/lib/vines/stream/server.rb +126 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +19 -0
- data/lib/vines/stream/server/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth.rb +31 -0
- data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth_result.rb +28 -0
- data/lib/vines/stream/server/outbound/final_features.rb +27 -0
- data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/start.rb +20 -0
- data/lib/vines/stream/server/outbound/tls.rb +30 -0
- data/lib/vines/stream/server/outbound/tls_result.rb +31 -0
- data/lib/vines/stream/server/ready.rb +20 -0
- data/lib/vines/stream/server/start.rb +13 -0
- data/lib/vines/stream/server/tls.rb +13 -0
- data/lib/vines/stream/state.rb +55 -0
- data/lib/vines/token_bucket.rb +46 -0
- data/lib/vines/user.rb +124 -0
- data/lib/vines/version.rb +5 -0
- data/lib/vines/xmpp_server.rb +25 -0
- data/test/config_test.rb +396 -0
- data/test/error_test.rb +59 -0
- data/test/ext/nokogiri.rb +14 -0
- data/test/jid_test.rb +71 -0
- data/test/kit_test.rb +21 -0
- data/test/router_test.rb +60 -0
- data/test/stanza/iq/roster_test.rb +198 -0
- data/test/stanza/iq/session_test.rb +30 -0
- data/test/stanza/iq/vcard_test.rb +159 -0
- data/test/stanza/message_test.rb +124 -0
- data/test/stanza/presence/subscribe_test.rb +75 -0
- data/test/storage/couchdb_test.rb +102 -0
- data/test/storage/ldap_test.rb +207 -0
- data/test/storage/local_test.rb +54 -0
- data/test/storage/redis_test.rb +75 -0
- data/test/storage/sql_test.rb +55 -0
- data/test/storage/storage_tests.rb +134 -0
- data/test/storage_test.rb +90 -0
- data/test/stream/client/auth_test.rb +127 -0
- data/test/stream/client/ready_test.rb +47 -0
- data/test/stream/component/handshake_test.rb +46 -0
- data/test/stream/component/ready_test.rb +105 -0
- data/test/stream/component/start_test.rb +41 -0
- data/test/stream/parser_test.rb +121 -0
- data/test/stream/server/outbound/auth_test.rb +77 -0
- data/test/stream/server/ready_test.rb +100 -0
- data/test/token_bucket_test.rb +24 -0
- data/test/user_test.rb +64 -0
- metadata +318 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class FinalRestart < State
|
7
|
+
def initialize(stream, success=Ready)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def node(node)
|
12
|
+
raise StreamErrors::NotAuthorized unless stream?(node)
|
13
|
+
stream.start(node)
|
14
|
+
stream.write('<stream:features/>')
|
15
|
+
advance
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Outbound
|
7
|
+
class Auth < State
|
8
|
+
NS = NAMESPACES[:sasl]
|
9
|
+
|
10
|
+
def initialize(stream, success=AuthResult)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def node(node)
|
15
|
+
raise StreamErrors::NotAuthorized unless external?(node)
|
16
|
+
authz = Base64.encode64(stream.domain).chomp
|
17
|
+
stream.write(%Q{<auth xmlns="#{NS}" mechanism="EXTERNAL">#{authz}</auth>})
|
18
|
+
advance
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def external?(node)
|
24
|
+
external = node.xpath("ns:mechanisms/ns:mechanism[text()='EXTERNAL']", 'ns' => NS).any?
|
25
|
+
node.name == 'features' && namespace(node) == NAMESPACES[:stream] && external
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Outbound
|
7
|
+
class AuthRestart < State
|
8
|
+
def initialize(stream, success=Auth)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def node(node)
|
13
|
+
raise StreamErrors::NotAuthorized unless stream?(node)
|
14
|
+
advance
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Outbound
|
7
|
+
class AuthResult < State
|
8
|
+
def initialize(stream, success=FinalRestart)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def node(node)
|
13
|
+
raise StreamErrors::NotAuthorized unless namespace(node) == NAMESPACES[:sasl]
|
14
|
+
case node.name
|
15
|
+
when 'success'
|
16
|
+
stream.start(node)
|
17
|
+
advance
|
18
|
+
when 'failure'
|
19
|
+
stream.close_connection
|
20
|
+
else
|
21
|
+
raise StreamErrors::NotAuthorized
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Outbound
|
7
|
+
class FinalFeatures < State
|
8
|
+
def initialize(stream, success=Server::Ready)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def node(node)
|
13
|
+
raise StreamErrors::NotAuthorized unless empty_features?(node)
|
14
|
+
advance
|
15
|
+
stream.notify_connected
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def empty_features?(node)
|
21
|
+
node.name == 'features' && namespace(node) == NAMESPACES[:stream] && node.elements.empty?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Outbound
|
7
|
+
class FinalRestart < State
|
8
|
+
def initialize(stream, success=FinalFeatures)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def node(node)
|
13
|
+
raise StreamErrors::NotAuthorized unless stream?(node)
|
14
|
+
advance
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Outbound
|
7
|
+
class Start < State
|
8
|
+
def initialize(stream, success=TLS)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def node(node)
|
13
|
+
raise StreamErrors::NotAuthorized unless stream?(node)
|
14
|
+
advance
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Outbound
|
7
|
+
class TLS < State
|
8
|
+
NS = NAMESPACES[:tls]
|
9
|
+
|
10
|
+
def initialize(stream, success=TLSResult)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def node(node)
|
15
|
+
raise StreamErrors::NotAuthorized unless tls?(node)
|
16
|
+
stream.write("<starttls xmlns='#{NS}'/>")
|
17
|
+
advance
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def tls?(node)
|
23
|
+
tls = node.xpath('ns:starttls', 'ns' => NS).any?
|
24
|
+
node.name == 'features' && namespace(node) == NAMESPACES[:stream] && tls
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Outbound
|
7
|
+
class TLSResult < State
|
8
|
+
NS = NAMESPACES[:tls]
|
9
|
+
|
10
|
+
def initialize(stream, success=AuthRestart)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def node(node)
|
15
|
+
raise StreamErrors::NotAuthorized unless namespace(node) == NS
|
16
|
+
case node.name
|
17
|
+
when 'proceed'
|
18
|
+
stream.encrypt
|
19
|
+
stream.start(node)
|
20
|
+
advance
|
21
|
+
when 'failure'
|
22
|
+
stream.close_connection
|
23
|
+
else
|
24
|
+
raise StreamErrors::NotAuthorized
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Server
|
6
|
+
class Ready < State
|
7
|
+
def node(node)
|
8
|
+
stanza = to_stanza(node)
|
9
|
+
raise StreamErrors::UnsupportedStanzaType unless stanza
|
10
|
+
to, from = %w[to from].map {|attr| JID.new(stanza[attr] || '') }
|
11
|
+
raise StreamErrors::ImproperAddressing if [to, from].any? {|addr| (addr.domain || '').strip.empty? }
|
12
|
+
raise StreamErrors::InvalidFrom unless from.domain == stream.remote_domain
|
13
|
+
raise StreamErrors::HostUnknown unless to.domain == stream.domain
|
14
|
+
stream.user = User.new(:jid => from)
|
15
|
+
stanza.process
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
|
6
|
+
# The base class of Stream state machines. States know how to process XML
|
7
|
+
# nodes and advance to their next valid state or fail the stream.
|
8
|
+
class State
|
9
|
+
include Nokogiri::XML
|
10
|
+
include Vines::Log
|
11
|
+
|
12
|
+
attr_reader :stream
|
13
|
+
|
14
|
+
STREAM = 'stream'.freeze
|
15
|
+
|
16
|
+
def initialize(stream, success=nil)
|
17
|
+
@stream, @success = stream, success
|
18
|
+
end
|
19
|
+
|
20
|
+
def node(node)
|
21
|
+
raise 'subclass must implement'
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(state)
|
25
|
+
self.class == state.class
|
26
|
+
end
|
27
|
+
|
28
|
+
def eql?(state)
|
29
|
+
state.is_a?(State) && self == state
|
30
|
+
end
|
31
|
+
|
32
|
+
def hash
|
33
|
+
self.class.hash
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def advance
|
39
|
+
stream.advance(@success.new(stream))
|
40
|
+
end
|
41
|
+
|
42
|
+
def stream?(node)
|
43
|
+
node.name == STREAM && namespace(node) == NAMESPACES[:stream]
|
44
|
+
end
|
45
|
+
|
46
|
+
def namespace(node)
|
47
|
+
node.namespace ? node.namespace.href : nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_stanza(node)
|
51
|
+
Stanza.from_node(node, stream)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
|
5
|
+
# The token bucket algorithm is useful for rate limiting.
|
6
|
+
# Before an operation can be completed, a token is taken from
|
7
|
+
# the bucket. If no tokens are available, the operation fails.
|
8
|
+
# The bucket is refilled with tokens at the maximum allowed rate
|
9
|
+
# of operations.
|
10
|
+
class TokenBucket
|
11
|
+
|
12
|
+
# Create a full bucket with capacity number of tokens to be filled
|
13
|
+
# at the given rate of tokens/second.
|
14
|
+
def initialize(capacity, rate)
|
15
|
+
raise ArgumentError.new('capacity must be > 0') unless capacity > 0
|
16
|
+
raise ArgumentError.new('rate must be > 0') unless rate > 0
|
17
|
+
@capacity = capacity
|
18
|
+
@tokens = capacity
|
19
|
+
@rate = rate
|
20
|
+
@timestamp = Time.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns true if tokens can be taken from the bucket.
|
24
|
+
def take(tokens)
|
25
|
+
raise ArgumentError.new('tokens must be > 0') unless tokens > 0
|
26
|
+
if tokens <= fill
|
27
|
+
@tokens -= tokens
|
28
|
+
true
|
29
|
+
else
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def fill
|
37
|
+
if @tokens < @capacity
|
38
|
+
now = Time.new
|
39
|
+
delta = (@rate * (now - @timestamp)).round
|
40
|
+
@tokens = [@capacity, @tokens + delta].min
|
41
|
+
@timestamp = now
|
42
|
+
end
|
43
|
+
@tokens
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/vines/user.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class User
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
attr_accessor :name, :password, :roster
|
8
|
+
attr_reader :jid
|
9
|
+
|
10
|
+
def initialize(args={})
|
11
|
+
@jid = JID.new(args[:jid])
|
12
|
+
raise ArgumentError, 'invalid jid' unless @jid.node && !@jid.domain.empty?
|
13
|
+
@name = args[:name]
|
14
|
+
@password = args[:password]
|
15
|
+
@roster = args[:roster] || []
|
16
|
+
end
|
17
|
+
|
18
|
+
def <=>(user)
|
19
|
+
self.jid.to_s <=> user.jid.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def eql?(user)
|
23
|
+
user.is_a?(User) && self == user
|
24
|
+
end
|
25
|
+
|
26
|
+
def hash
|
27
|
+
jid.to_s.hash
|
28
|
+
end
|
29
|
+
|
30
|
+
# Update this user's information from the given user object.
|
31
|
+
def update_from(user)
|
32
|
+
@name = user.name
|
33
|
+
@password = user.password
|
34
|
+
@roster = user.roster.map {|c| c.clone }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return true if the jid is on this user's roster.
|
38
|
+
def contact?(jid)
|
39
|
+
!contact(jid).nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the contact with this jid or nil if not found.
|
43
|
+
def contact(jid)
|
44
|
+
bare = JID.new(jid).bare
|
45
|
+
@roster.find {|c| c.jid.bare == bare }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns true if the user is subscribed to this contact's
|
49
|
+
# presence updates.
|
50
|
+
def subscribed_to?(jid)
|
51
|
+
contact = contact(jid)
|
52
|
+
contact && contact.subscribed_to?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns true if the user has a presence subscription from this contact.
|
56
|
+
# The contact is subscribed to this user's presence.
|
57
|
+
def subscribed_from?(jid)
|
58
|
+
contact = contact(jid)
|
59
|
+
contact && contact.subscribed_from?
|
60
|
+
end
|
61
|
+
|
62
|
+
# Removes the contact with this jid from the user's roster.
|
63
|
+
def remove_contact(jid)
|
64
|
+
bare = JID.new(jid).bare
|
65
|
+
@roster.reject! {|c| c.jid.bare == bare }
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns a list of the contacts to which this user has
|
69
|
+
# successfully subscribed.
|
70
|
+
def subscribed_to_contacts
|
71
|
+
@roster.select {|c| c.subscribed_to? }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns a list of the contacts that are subscribed to this user's
|
75
|
+
# presence updates.
|
76
|
+
def subscribed_from_contacts
|
77
|
+
@roster.select {|c| c.subscribed_from? }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Update the contact's jid on this user's roster to signal that this user
|
81
|
+
# has requested the contact's permission to receive their presence updates.
|
82
|
+
def request_subscription(jid)
|
83
|
+
unless contact = contact(jid)
|
84
|
+
contact = Contact.new(:jid => jid)
|
85
|
+
@roster << contact
|
86
|
+
end
|
87
|
+
contact.ask = 'subscribe' if %w[none from].include?(contact.subscription)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add the user's jid to this contact's roster with a subscription state of
|
91
|
+
# 'from.' This signals that this contact has approved a user's subscription.
|
92
|
+
def add_subscription_from(jid)
|
93
|
+
unless contact = contact(jid)
|
94
|
+
contact = Contact.new(:jid => jid)
|
95
|
+
@roster << contact
|
96
|
+
end
|
97
|
+
contact.subscribe_from
|
98
|
+
end
|
99
|
+
|
100
|
+
def remove_subscription_to(jid)
|
101
|
+
if contact = contact(jid)
|
102
|
+
contact.unsubscribe_to
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def remove_subscription_from(jid)
|
107
|
+
if contact = contact(jid)
|
108
|
+
contact.unsubscribe_from
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns this user's roster contacts as an iq query element.
|
113
|
+
def to_roster_xml(id)
|
114
|
+
doc = Nokogiri::XML::Document.new
|
115
|
+
doc.create_element('iq', 'id' => id, 'type' => 'result') do |el|
|
116
|
+
el << doc.create_element('query', 'xmlns' => 'jabber:iq:roster') do |query|
|
117
|
+
@roster.sort!.each do |contact|
|
118
|
+
query << contact.to_roster_xml
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|