diaspora-vines 0.1.21 → 0.1.22
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.
- checksums.yaml +4 -4
- data/conf/config.rb +1 -0
- data/lib/vines/config.rb +4 -1
- data/lib/vines/contact.rb +5 -1
- data/lib/vines/storage/sql.rb +91 -26
- data/lib/vines/stream.rb +1 -8
- data/lib/vines/version.rb +1 -1
- data/test/config_test.rb +0 -8
- data/test/contact_test.rb +2 -2
- data/test/stanza/iq/roster_test.rb +3 -3
- data/test/stanza/presence/subscribe_test.rb +1 -1
- data/test/storage/sql.rb +67 -0
- data/test/storage/sql_schema.rb +32 -0
- data/test/user_test.rb +7 -2
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a46173efffc5217b488d5f6c5976884ccc2f708
|
4
|
+
data.tar.gz: aa7dd385af0c647ff67c13c2342dec210f04e131
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b07389956b5f47fc57f9b79afa0a6e28c013da900800e526c31247773877681bcc54a4940356370ebfe3fb2c36a0a62c667b25e71c593e4f151b03ef1b25f0f
|
7
|
+
data.tar.gz: 7196743cd8ee5505f72d05b662b10fc691187437e202bb9e8aef9341a338a7d477d299e1d951d1c2d5f0ef75833d2896a165e68e7a5606aaed802c3dd77d1ae4
|
data/conf/config.rb
CHANGED
data/lib/vines/config.rb
CHANGED
@@ -158,7 +158,10 @@ module Vines
|
|
158
158
|
# Returns true if server-to-server connections are allowed with the
|
159
159
|
# given domain.
|
160
160
|
def s2s?(domain)
|
161
|
-
|
161
|
+
# NOTE: Disabled whitelisting; It is necessary to allow
|
162
|
+
# anonymous hosts, otherwise everyone has to add all hosts manually
|
163
|
+
# TODO: Create a blacklist in case we have to block a malicious host
|
164
|
+
@ports[:server] # && @ports[:server].hosts.include?(domain.to_s)
|
162
165
|
end
|
163
166
|
|
164
167
|
# Return true if the server is a member of a cluster, serving the same
|
data/lib/vines/contact.rb
CHANGED
@@ -4,7 +4,7 @@ module Vines
|
|
4
4
|
class Contact
|
5
5
|
include Comparable
|
6
6
|
|
7
|
-
attr_accessor :name, :subscription, :ask, :groups
|
7
|
+
attr_accessor :name, :subscription, :ask, :groups, :from_diaspora
|
8
8
|
attr_reader :jid
|
9
9
|
|
10
10
|
def initialize(args={})
|
@@ -12,6 +12,7 @@ module Vines
|
|
12
12
|
raise ArgumentError, 'invalid jid' if @jid.empty?
|
13
13
|
@name = args[:name]
|
14
14
|
@subscription = args[:subscription] || 'none'
|
15
|
+
@from_diaspora = args[:from_diaspora] || false
|
15
16
|
@ask = args[:ask]
|
16
17
|
@groups = args[:groups] || []
|
17
18
|
end
|
@@ -29,6 +30,7 @@ module Vines
|
|
29
30
|
def update_from(contact)
|
30
31
|
@name = contact.name
|
31
32
|
@subscription = contact.subscription
|
33
|
+
@from_diaspora = contact.from_diaspora
|
32
34
|
@ask = contact.ask
|
33
35
|
@groups = contact.groups.clone
|
34
36
|
end
|
@@ -75,6 +77,7 @@ module Vines
|
|
75
77
|
{
|
76
78
|
'name' => @name,
|
77
79
|
'subscription' => @subscription,
|
80
|
+
'from_diaspora' => @from_diaspora,
|
78
81
|
'ask' => @ask,
|
79
82
|
'groups' => @groups.sort!
|
80
83
|
}
|
@@ -102,6 +105,7 @@ module Vines
|
|
102
105
|
el['jid'] = @jid.bare.to_s
|
103
106
|
el['name'] = @name unless @name.nil? || @name.empty?
|
104
107
|
el['subscription'] = @subscription
|
108
|
+
el['from_diaspora'] = @from_diaspora
|
105
109
|
@groups.sort!.each do |group|
|
106
110
|
el << doc.create_element('group', group)
|
107
111
|
end
|
data/lib/vines/storage/sql.rb
CHANGED
@@ -33,10 +33,22 @@ module Vines
|
|
33
33
|
|
34
34
|
class User < ActiveRecord::Base
|
35
35
|
has_many :contacts
|
36
|
+
has_many :chat_contacts, :dependent => :destroy
|
37
|
+
has_many :fragments, :dependent => :delete_all
|
36
38
|
|
37
39
|
has_one :person, :foreign_key => :owner_id
|
38
40
|
end
|
39
41
|
|
42
|
+
class ChatContact < ActiveRecord::Base
|
43
|
+
belongs_to :users
|
44
|
+
|
45
|
+
serialize :groups, JSON
|
46
|
+
end
|
47
|
+
|
48
|
+
class ChatFragment < ActiveRecord::Base
|
49
|
+
belongs_to :users
|
50
|
+
end
|
51
|
+
|
40
52
|
# Wrap the method with ActiveRecord connection pool logic, so we properly
|
41
53
|
# return connections to the pool when we're finished with them. This also
|
42
54
|
# defers the original method by pushing it onto the EM thread pool because
|
@@ -80,11 +92,27 @@ module Vines
|
|
80
92
|
xuser.encrypted_password,
|
81
93
|
xuser.authentication_token
|
82
94
|
|
95
|
+
# add diaspora contacts
|
83
96
|
xuser.contacts.each do |contact|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
97
|
+
handle = contact.person.diaspora_handle
|
98
|
+
ask, subscription, groups = get_diaspora_flags(contact)
|
99
|
+
user.roster << Vines::Contact.new(
|
100
|
+
jid: handle,
|
101
|
+
name: handle.gsub(/\@.*?$/, ''),
|
102
|
+
subscription: subscription,
|
103
|
+
from_diaspora: true,
|
104
|
+
groups: groups,
|
105
|
+
ask: ask)
|
106
|
+
end
|
107
|
+
|
108
|
+
# add external contacts
|
109
|
+
xuser.chat_contacts.each do |contact|
|
110
|
+
user.roster << Vines::Contact.new(
|
111
|
+
jid: contact.jid,
|
112
|
+
name: contact.name,
|
113
|
+
subscription: contact.subscription,
|
114
|
+
groups: contact.groups,
|
115
|
+
ask: contact.ask)
|
88
116
|
end
|
89
117
|
end if xuser
|
90
118
|
end
|
@@ -93,8 +121,7 @@ module Vines
|
|
93
121
|
def authenticate(username, password)
|
94
122
|
user = find_user(username)
|
95
123
|
|
96
|
-
pepper = password
|
97
|
-
pepper << Config.instance.pepper unless Config
|
124
|
+
pepper = "#{password}#{Config.instance.pepper}" rescue password
|
98
125
|
dbhash = BCrypt::Password.new(user.password) rescue nil
|
99
126
|
hash = BCrypt::Engine.hash_secret(pepper, dbhash.salt) rescue nil
|
100
127
|
|
@@ -104,29 +131,72 @@ module Vines
|
|
104
131
|
end
|
105
132
|
|
106
133
|
def save_user(user)
|
107
|
-
#
|
134
|
+
# it is not possible to register an account via xmpp server
|
135
|
+
xuser = user_by_jid(user.jid) || return
|
136
|
+
|
137
|
+
# remove deleted contacts from roster
|
138
|
+
xuser.chat_contacts.delete(xuser.chat_contacts.select do |contact|
|
139
|
+
!user.contact?(contact.jid)
|
140
|
+
end)
|
141
|
+
|
142
|
+
# update contacts
|
143
|
+
xuser.chat_contacts.each do |contact|
|
144
|
+
fresh = user.contact(contact.jid)
|
145
|
+
contact.update_attributes(
|
146
|
+
name: fresh.name,
|
147
|
+
ask: fresh.ask,
|
148
|
+
subscription: fresh.subscription,
|
149
|
+
groups: fresh.groups)
|
150
|
+
end
|
151
|
+
|
152
|
+
# add new contacts to roster
|
153
|
+
jids = xuser.chat_contacts.map {|c|
|
154
|
+
c.jid if (c.user_id == xuser.id)
|
155
|
+
}.compact
|
156
|
+
user.roster.select {|contact|
|
157
|
+
unless contact.from_diaspora
|
158
|
+
xuser.chat_contacts.build(
|
159
|
+
user_id: xuser.id,
|
160
|
+
jid: contact.jid.bare.to_s,
|
161
|
+
name: contact.name,
|
162
|
+
ask: contact.ask,
|
163
|
+
subscription: contact.subscription,
|
164
|
+
groups: contact.groups) unless jids.include?(contact.jid.bare.to_s)
|
165
|
+
end
|
166
|
+
}
|
167
|
+
xuser.save
|
108
168
|
end
|
109
169
|
with_connection :save_user
|
110
170
|
|
111
171
|
def find_vcard(jid)
|
112
|
-
#
|
172
|
+
# not supported yet
|
113
173
|
nil
|
114
174
|
end
|
115
175
|
with_connection :find_vcard
|
116
176
|
|
117
177
|
def save_vcard(jid, card)
|
118
|
-
#
|
178
|
+
# not supported yet
|
119
179
|
end
|
120
180
|
with_connection :save_vcard
|
121
181
|
|
122
182
|
def find_fragment(jid, node)
|
123
|
-
|
124
|
-
|
183
|
+
jid = JID.new(jid).bare.to_s
|
184
|
+
return if jid.empty?
|
185
|
+
if fragment = fragment_by_jid(jid, node)
|
186
|
+
Nokogiri::XML(fragment.xml).root rescue nil
|
187
|
+
end
|
125
188
|
end
|
126
189
|
with_connection :find_fragment
|
127
190
|
|
128
191
|
def save_fragment(jid, node)
|
129
|
-
|
192
|
+
jid = JID.new(jid).bare.to_s
|
193
|
+
fragment = fragment_by_jid(jid, node) ||
|
194
|
+
Sql::ChatFragment.new(
|
195
|
+
user: user_by_jid(jid),
|
196
|
+
root: node.name,
|
197
|
+
namespace: node.namespace.href)
|
198
|
+
fragment.xml = node.to_xml
|
199
|
+
fragment.save
|
130
200
|
end
|
131
201
|
with_connection :save_fragment
|
132
202
|
|
@@ -141,16 +211,19 @@ module Vines
|
|
141
211
|
Sql::User.find_by_username(name)
|
142
212
|
end
|
143
213
|
|
144
|
-
def
|
214
|
+
def fragment_by_jid(jid, node)
|
215
|
+
jid = JID.new(jid).bare.to_s
|
216
|
+
clause = 'user_id=(select id from users where jid=?) and root=? and namespace=?'
|
217
|
+
Sql::ChatFragment.where(clause, jid, node.name, node.namespace.href).first
|
218
|
+
end
|
219
|
+
|
220
|
+
def get_diaspora_flags(contact)
|
145
221
|
groups = Array.new
|
222
|
+
ask, subscription = 'none', 'none'
|
146
223
|
contact.aspects.each do |aspect|
|
147
224
|
groups.push(aspect.name)
|
148
225
|
end
|
149
226
|
|
150
|
-
handle = contact.person.diaspora_handle
|
151
|
-
ask = 'none'
|
152
|
-
subscription = 'none'
|
153
|
-
|
154
227
|
if contact.sharing && contact.receiving
|
155
228
|
subscription = 'both'
|
156
229
|
elsif contact.sharing && !contact.receiving
|
@@ -161,15 +234,7 @@ module Vines
|
|
161
234
|
else
|
162
235
|
ask = 'suscribe'
|
163
236
|
end
|
164
|
-
|
165
|
-
# finally build the roster entry
|
166
|
-
return Vines::Contact.new(
|
167
|
-
jid: handle,
|
168
|
-
name: handle.gsub(/\@.*?$/, ''),
|
169
|
-
subscription: subscription,
|
170
|
-
groups: groups,
|
171
|
-
ask: ask
|
172
|
-
) || nil
|
237
|
+
return ask, subscription, groups
|
173
238
|
end
|
174
239
|
end
|
175
240
|
end
|
data/lib/vines/stream.rb
CHANGED
@@ -98,14 +98,7 @@ module Vines
|
|
98
98
|
router.interested_resources(*jid, user.jid)
|
99
99
|
end
|
100
100
|
|
101
|
-
def ssl_verify_peer(pem)
|
102
|
-
# EM is supposed to close the connection when this returns false,
|
103
|
-
# but it only does that for inbound connections, not when we
|
104
|
-
# make a connection to another server.
|
105
|
-
@store.trusted?(pem).tap do |trusted|
|
106
|
-
close_connection unless trusted
|
107
|
-
end
|
108
|
-
end
|
101
|
+
def ssl_verify_peer(pem); true; end
|
109
102
|
|
110
103
|
def cert_domain_matches?(domain)
|
111
104
|
@store.domain?(get_peer_cert, domain)
|
data/lib/vines/version.rb
CHANGED
data/test/config_test.rb
CHANGED
@@ -231,7 +231,6 @@ describe Vines::Config do
|
|
231
231
|
end
|
232
232
|
port = config.ports.first
|
233
233
|
refute_nil port
|
234
|
-
assert !config.s2s?('verona.lit')
|
235
234
|
assert_equal Vines::Config::ServerPort, port.class
|
236
235
|
assert_equal '0.0.0.0', port.host
|
237
236
|
assert_equal 5269, port.port
|
@@ -248,17 +247,10 @@ describe Vines::Config do
|
|
248
247
|
end
|
249
248
|
server '0.0.0.1', 42 do
|
250
249
|
max_stanza_size 60_000
|
251
|
-
hosts ['verona.lit', 'denmark.lit']
|
252
250
|
end
|
253
251
|
end
|
254
252
|
port = config.ports.first
|
255
253
|
refute_nil port
|
256
|
-
assert config.s2s?('verona.lit')
|
257
|
-
assert config.s2s?('denmark.lit')
|
258
|
-
assert config.s2s?(Vines::JID.new('denmark.lit'))
|
259
|
-
refute config.s2s?(Vines::JID.new('hamlet@denmark.lit'))
|
260
|
-
refute config.s2s?('bogus')
|
261
|
-
refute config.s2s?(nil)
|
262
254
|
assert_equal Vines::Config::ServerPort, port.class
|
263
255
|
assert_equal '0.0.0.1', port.host
|
264
256
|
assert_equal 42, port.port
|
data/test/contact_test.rb
CHANGED
@@ -53,7 +53,7 @@ describe Vines::Contact do
|
|
53
53
|
describe '#to_roster_xml' do
|
54
54
|
let(:expected) do
|
55
55
|
node(%q{
|
56
|
-
<item jid="alice@wonderland.lit" name="Alice" subscription="from">
|
56
|
+
<item jid="alice@wonderland.lit" name="Alice" subscription="from" from_diaspora="false">
|
57
57
|
<group>Buddies</group>
|
58
58
|
<group>Friends</group>
|
59
59
|
</item>
|
@@ -71,7 +71,7 @@ describe Vines::Contact do
|
|
71
71
|
node(%q{
|
72
72
|
<iq to="hatter@wonderland.lit" type="set">
|
73
73
|
<query xmlns="jabber:iq:roster">
|
74
|
-
<item jid="alice@wonderland.lit" name="Alice" subscription="from">
|
74
|
+
<item jid="alice@wonderland.lit" name="Alice" subscription="from" from_diaspora="false">
|
75
75
|
<group>Buddies</group>
|
76
76
|
<group>Friends</group>
|
77
77
|
</item>
|
@@ -36,11 +36,11 @@ describe Vines::Stanza::Iq::Roster do
|
|
36
36
|
node(%q{
|
37
37
|
<iq id="42" type="result">
|
38
38
|
<query xmlns="jabber:iq:roster">
|
39
|
-
<item jid="cat@wonderland.lit" subscription="none">
|
39
|
+
<item jid="cat@wonderland.lit" subscription="none" from_diaspora="false">
|
40
40
|
<group>Cats</group>
|
41
41
|
<group>Friends</group>
|
42
42
|
</item>
|
43
|
-
<item jid="hatter@wonderland.lit" subscription="none"/>
|
43
|
+
<item jid="hatter@wonderland.lit" subscription="none" from_diaspora="false"/>
|
44
44
|
</query>
|
45
45
|
</iq>})
|
46
46
|
end
|
@@ -187,7 +187,7 @@ describe Vines::Stanza::Iq::Roster do
|
|
187
187
|
node(%q{
|
188
188
|
<iq to="alice@wonderland.lit/tea" type="set">
|
189
189
|
<query xmlns="jabber:iq:roster">
|
190
|
-
<item jid="hatter@wonderland.lit" name="Mad Hatter" subscription="none">
|
190
|
+
<item jid="hatter@wonderland.lit" name="Mad Hatter" subscription="none" from_diaspora="false">
|
191
191
|
<group>Friends</group>
|
192
192
|
</item>
|
193
193
|
</query>
|
@@ -74,7 +74,7 @@ describe Vines::Stanza::Presence::Subscribe do
|
|
74
74
|
subject.process
|
75
75
|
recipient.nodes.size.must_equal 1
|
76
76
|
|
77
|
-
query = %q{<query xmlns="jabber:iq:roster"><item jid="hatter@wonderland.lit" subscription="none"/></query>}
|
77
|
+
query = %q{<query xmlns="jabber:iq:roster"><item jid="hatter@wonderland.lit" subscription="none" from_diaspora="false"/></query>}
|
78
78
|
expected = node(%Q{<iq to="alice@wonderland.lit/tea" type="set">#{query}</iq>})
|
79
79
|
recipient.nodes.first.remove_attribute('id') # id is random
|
80
80
|
recipient.nodes.first.must_equal expected
|
data/test/storage/sql.rb
CHANGED
@@ -34,6 +34,28 @@ describe Vines::Storage::Sql do
|
|
34
34
|
db = Rails.application.config.database_configuration["development"]["database"]
|
35
35
|
File.delete(db) if File.exist?(db)
|
36
36
|
end
|
37
|
+
|
38
|
+
def test_save_user
|
39
|
+
fibered do
|
40
|
+
db = storage
|
41
|
+
user = Vines::User.new(
|
42
|
+
jid: 'test@test.de',
|
43
|
+
name: 'test@test.de',
|
44
|
+
password: 'secret')
|
45
|
+
user.roster << Vines::Contact.new(
|
46
|
+
jid: 'contact1@domain.tld/resource2',
|
47
|
+
name: 'Contact 1')
|
48
|
+
db.save_user(user)
|
49
|
+
user = db.find_user('test@test.de')
|
50
|
+
|
51
|
+
assert (user != nil), "no user found"
|
52
|
+
assert_equal "test@test.de", user.jid.to_s
|
53
|
+
|
54
|
+
assert_equal 1, user.roster.length
|
55
|
+
assert_equal "contact1@domain.tld", user.roster[0].jid.to_s
|
56
|
+
assert_equal "Contact 1", user.roster[0].name
|
57
|
+
end
|
58
|
+
end
|
37
59
|
|
38
60
|
def test_find_user
|
39
61
|
fibered do
|
@@ -75,4 +97,49 @@ describe Vines::Storage::Sql do
|
|
75
97
|
assert_equal "test", user.name
|
76
98
|
end
|
77
99
|
end
|
100
|
+
|
101
|
+
def test_find_fragment
|
102
|
+
skip("not working probably")
|
103
|
+
|
104
|
+
fibered do
|
105
|
+
db = storage
|
106
|
+
root = Nokogiri::XML(%q{<characters xmlns="urn:wonderland"/>}).root
|
107
|
+
bad_name = Nokogiri::XML(%q{<not_characters xmlns="urn:wonderland"/>}).root
|
108
|
+
bad_ns = Nokogiri::XML(%q{<characters xmlns="not:wonderland"/>}).root
|
109
|
+
|
110
|
+
node = db.find_fragment(nil, nil)
|
111
|
+
assert_nil node
|
112
|
+
|
113
|
+
node = db.find_fragment('full@wonderland.lit', bad_name)
|
114
|
+
assert_nil node
|
115
|
+
|
116
|
+
node = db.find_fragment('full@wonderland.lit', bad_ns)
|
117
|
+
assert_nil node
|
118
|
+
|
119
|
+
node = db.find_fragment('full@wonderland.lit', root)
|
120
|
+
assert (node != nil), "node should include fragment"
|
121
|
+
assert_equal fragment.to_s, node.to_s
|
122
|
+
|
123
|
+
node = db.find_fragment(Vines::JID.new('full@wonderland.lit'), root)
|
124
|
+
assert (node != nil), "node should include fragment"
|
125
|
+
assert_equal fragment.to_s, node.to_s
|
126
|
+
|
127
|
+
node = db.find_fragment(Vines::JID.new('full@wonderland.lit/resource'), root)
|
128
|
+
assert (node != nil), "node should include fragment"
|
129
|
+
assert_equal fragment.to_s, node.to_s
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_save_fragment
|
134
|
+
skip("not working probably")
|
135
|
+
|
136
|
+
fibered do
|
137
|
+
db = storage
|
138
|
+
root = Nokogiri::XML(%q{<characters xmlns="urn:wonderland"/>}).root
|
139
|
+
db.save_fragment('test@test.de/resource1', fragment)
|
140
|
+
node = db.find_fragment('test@test.de', root)
|
141
|
+
assert (node != nil), "node should include fragment"
|
142
|
+
assert_equal fragment.to_s, node.to_s
|
143
|
+
end
|
144
|
+
end
|
78
145
|
end
|
data/test/storage/sql_schema.rb
CHANGED
@@ -8,6 +8,18 @@ module SqlSchema
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
def fragment_id
|
12
|
+
Digest::SHA1.hexdigest("characters:urn:wonderland")
|
13
|
+
end
|
14
|
+
|
15
|
+
def fragment
|
16
|
+
Nokogiri::XML(%q{
|
17
|
+
<characters xmlns="urn:wonderland">
|
18
|
+
<character>Alice</character>
|
19
|
+
</characters>
|
20
|
+
}.strip).root
|
21
|
+
end
|
22
|
+
|
11
23
|
def storage
|
12
24
|
Vines::Storage::Sql.new
|
13
25
|
end
|
@@ -67,6 +79,26 @@ module SqlSchema
|
|
67
79
|
add_index "contacts", ["person_id"], :name => "index_contacts_on_person_id"
|
68
80
|
add_index "contacts", ["user_id", "person_id"], :name => "index_contacts_on_user_id_and_person_id", :unique => true
|
69
81
|
|
82
|
+
create_table "chat_contacts", :force => true do |t|
|
83
|
+
t.integer "user_id", :null => false
|
84
|
+
t.string "jid", :null => false
|
85
|
+
t.string "name"
|
86
|
+
t.string "ask", :limit => 128
|
87
|
+
t.string "subscription", :limit => 128, :null => false
|
88
|
+
t.text "groups"
|
89
|
+
end
|
90
|
+
|
91
|
+
add_index "chat_contacts", ["user_id", "jid"], :name => "index_chat_contacts_on_user_id_and_jid", :unique => true
|
92
|
+
|
93
|
+
create_table "chat_fragments", :force => true do |t|
|
94
|
+
t.integer "user_id", :null => false
|
95
|
+
t.string "root", :limit => 256, :null => false
|
96
|
+
t.string "namespace", :limit => 256, :null => false
|
97
|
+
t.text "xml", :null => false
|
98
|
+
end
|
99
|
+
|
100
|
+
add_index "chat_fragments", ["user_id"], :name => "index_chat_fragments_on_user_id", :unique => true
|
101
|
+
|
70
102
|
create_table "users", :force => true do |t|
|
71
103
|
t.string "username"
|
72
104
|
t.text "serialized_private_key"
|
data/test/user_test.rb
CHANGED
@@ -77,8 +77,13 @@ describe Vines::User do
|
|
77
77
|
node(%q{
|
78
78
|
<iq id="42" type="result">
|
79
79
|
<query xmlns="jabber:iq:roster">
|
80
|
-
<item jid="a@wonderland.lit" name="Contact 1" subscription="none"
|
81
|
-
|
80
|
+
<item jid="a@wonderland.lit" name="Contact 1" subscription="none" from_diaspora="false">
|
81
|
+
<group>A</group>
|
82
|
+
<group>B</group>
|
83
|
+
</item>
|
84
|
+
<item jid="b@wonderland.lit" name="Contact 2" subscription="none" from_diaspora="false">
|
85
|
+
<group>C</group>
|
86
|
+
</item>
|
82
87
|
</query>
|
83
88
|
</iq>
|
84
89
|
})
|