blather 0.3.4 → 0.4.0

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.
Files changed (110) hide show
  1. data/LICENSE +1 -1
  2. data/README.rdoc +41 -12
  3. data/examples/echo.rb +1 -1
  4. data/examples/execute.rb +0 -5
  5. data/examples/pubsub/cli.rb +64 -0
  6. data/examples/pubsub/ping_pong.rb +18 -0
  7. data/examples/rosterprint.rb +14 -0
  8. data/examples/xmpp4r/echo.rb +35 -0
  9. data/lib/blather.rb +35 -12
  10. data/lib/blather/client.rb +1 -1
  11. data/lib/blather/client/client.rb +19 -13
  12. data/lib/blather/client/dsl.rb +16 -0
  13. data/lib/blather/client/dsl/pubsub.rb +133 -0
  14. data/lib/blather/core_ext/active_support.rb +1 -117
  15. data/lib/blather/core_ext/active_support/inheritable_attributes.rb +117 -0
  16. data/lib/blather/core_ext/nokogiri.rb +35 -0
  17. data/lib/blather/errors.rb +3 -20
  18. data/lib/blather/errors/sasl_error.rb +3 -1
  19. data/lib/blather/errors/stanza_error.rb +10 -17
  20. data/lib/blather/errors/stream_error.rb +11 -14
  21. data/lib/blather/jid.rb +1 -0
  22. data/lib/blather/roster.rb +9 -0
  23. data/lib/blather/roster_item.rb +6 -1
  24. data/lib/blather/stanza.rb +35 -18
  25. data/lib/blather/stanza/disco.rb +7 -1
  26. data/lib/blather/stanza/disco/disco_info.rb +45 -33
  27. data/lib/blather/stanza/disco/disco_items.rb +32 -21
  28. data/lib/blather/stanza/iq.rb +13 -8
  29. data/lib/blather/stanza/iq/query.rb +16 -8
  30. data/lib/blather/stanza/iq/roster.rb +33 -22
  31. data/lib/blather/stanza/message.rb +20 -31
  32. data/lib/blather/stanza/presence.rb +3 -5
  33. data/lib/blather/stanza/presence/status.rb +13 -21
  34. data/lib/blather/stanza/presence/subscription.rb +11 -16
  35. data/lib/blather/stanza/pubsub.rb +63 -0
  36. data/lib/blather/stanza/pubsub/affiliations.rb +50 -0
  37. data/lib/blather/stanza/pubsub/create.rb +43 -0
  38. data/lib/blather/stanza/pubsub/errors.rb +9 -0
  39. data/lib/blather/stanza/pubsub/event.rb +77 -0
  40. data/lib/blather/stanza/pubsub/items.rb +63 -0
  41. data/lib/blather/stanza/pubsub/publish.rb +58 -0
  42. data/lib/blather/stanza/pubsub/retract.rb +53 -0
  43. data/lib/blather/stanza/pubsub/subscribe.rb +42 -0
  44. data/lib/blather/stanza/pubsub/subscription.rb +66 -0
  45. data/lib/blather/stanza/pubsub/subscriptions.rb +55 -0
  46. data/lib/blather/stanza/pubsub/unsubscribe.rb +42 -0
  47. data/lib/blather/stanza/pubsub_owner.rb +41 -0
  48. data/lib/blather/stanza/pubsub_owner/delete.rb +34 -0
  49. data/lib/blather/stanza/pubsub_owner/purge.rb +34 -0
  50. data/lib/blather/stream.rb +76 -168
  51. data/lib/blather/stream/client.rb +1 -2
  52. data/lib/blather/stream/component.rb +9 -5
  53. data/lib/blather/stream/features.rb +53 -0
  54. data/lib/blather/stream/features/resource.rb +63 -0
  55. data/lib/blather/stream/{sasl.rb → features/sasl.rb} +53 -52
  56. data/lib/blather/stream/features/session.rb +44 -0
  57. data/lib/blather/stream/features/tls.rb +28 -0
  58. data/lib/blather/stream/parser.rb +70 -46
  59. data/lib/blather/xmpp_node.rb +113 -52
  60. data/spec/blather/client/client_spec.rb +44 -58
  61. data/spec/blather/client/dsl/pubsub_spec.rb +465 -0
  62. data/spec/blather/client/dsl_spec.rb +19 -6
  63. data/spec/blather/core_ext/nokogiri_spec.rb +83 -0
  64. data/spec/blather/errors/sasl_error_spec.rb +8 -8
  65. data/spec/blather/errors/stanza_error_spec.rb +25 -33
  66. data/spec/blather/errors/stream_error_spec.rb +21 -16
  67. data/spec/blather/errors_spec.rb +4 -11
  68. data/spec/blather/jid_spec.rb +31 -30
  69. data/spec/blather/roster_item_spec.rb +34 -23
  70. data/spec/blather/roster_spec.rb +27 -12
  71. data/spec/blather/stanza/discos/disco_info_spec.rb +61 -42
  72. data/spec/blather/stanza/discos/disco_items_spec.rb +47 -35
  73. data/spec/blather/stanza/iq/query_spec.rb +34 -11
  74. data/spec/blather/stanza/iq/roster_spec.rb +47 -30
  75. data/spec/blather/stanza/iq_spec.rb +19 -14
  76. data/spec/blather/stanza/message_spec.rb +30 -17
  77. data/spec/blather/stanza/presence/status_spec.rb +43 -20
  78. data/spec/blather/stanza/presence/subscription_spec.rb +41 -21
  79. data/spec/blather/stanza/presence_spec.rb +34 -21
  80. data/spec/blather/stanza/pubsub/affiliations_spec.rb +57 -0
  81. data/spec/blather/stanza/pubsub/create_spec.rb +56 -0
  82. data/spec/blather/stanza/pubsub/event_spec.rb +84 -0
  83. data/spec/blather/stanza/pubsub/items_spec.rb +79 -0
  84. data/spec/blather/stanza/pubsub/publish_spec.rb +83 -0
  85. data/spec/blather/stanza/pubsub/retract_spec.rb +75 -0
  86. data/spec/blather/stanza/pubsub/subscribe_spec.rb +61 -0
  87. data/spec/blather/stanza/pubsub/subscription_spec.rb +97 -0
  88. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +59 -0
  89. data/spec/blather/stanza/pubsub/unsubscribe_spec.rb +61 -0
  90. data/spec/blather/stanza/pubsub_owner/delete_spec.rb +50 -0
  91. data/spec/blather/stanza/pubsub_owner/purge_spec.rb +50 -0
  92. data/spec/blather/stanza/pubsub_owner_spec.rb +27 -0
  93. data/spec/blather/stanza/pubsub_spec.rb +62 -0
  94. data/spec/blather/stanza_spec.rb +53 -38
  95. data/spec/blather/stream/client_spec.rb +231 -88
  96. data/spec/blather/stream/component_spec.rb +14 -5
  97. data/spec/blather/stream/parser_spec.rb +145 -0
  98. data/spec/blather/xmpp_node_spec.rb +192 -96
  99. data/spec/fixtures/pubsub.rb +311 -0
  100. data/spec/spec_helper.rb +5 -4
  101. metadata +54 -18
  102. data/Rakefile +0 -139
  103. data/ext/extconf.rb +0 -65
  104. data/ext/push_parser.c +0 -209
  105. data/lib/blather/core_ext/libxml.rb +0 -28
  106. data/lib/blather/stream/resource.rb +0 -48
  107. data/lib/blather/stream/session.rb +0 -36
  108. data/lib/blather/stream/stream_handler.rb +0 -39
  109. data/lib/blather/stream/tls.rb +0 -33
  110. data/spec/blather/core_ext/libxml_spec.rb +0 -58
@@ -83,6 +83,7 @@ module Blather
83
83
  def <=>(o)
84
84
  to_s <=> o.to_s
85
85
  end
86
+ alias_method :eql?, :==
86
87
 
87
88
  ##
88
89
  # Test if JID is stripped
@@ -71,6 +71,15 @@ module Blather
71
71
  @items.dup
72
72
  end
73
73
 
74
+ ##
75
+ # A hash of items keyed by group
76
+ def grouped
77
+ self.inject(Hash.new{|h,k|h[k]=[]}) do |hash, item|
78
+ item[1].groups.each { |group| hash[group] << item[1] }
79
+ hash
80
+ end
81
+ end
82
+
74
83
  private
75
84
  def self.key(jid)
76
85
  JID.new(jid).stripped.to_s
@@ -13,6 +13,11 @@ module Blather
13
13
  attr_accessor :name,
14
14
  :groups
15
15
 
16
+ def self.new(item)
17
+ return item if item.is_a?(self)
18
+ super
19
+ end
20
+
16
21
  ##
17
22
  # item:: can be a JID, String (a@b) or a Stanza
18
23
  def initialize(item)
@@ -29,7 +34,7 @@ module Blather
29
34
  self.name = item[:name]
30
35
  self.subscription = item[:subscription]
31
36
  self.ask = item[:ask]
32
- item.groups.each { |g| self.groups << g }
37
+ item.groups.each { |g| @groups << g }
33
38
  end
34
39
 
35
40
  @groups = [nil] if @groups.empty?
@@ -14,12 +14,12 @@ module Blather
14
14
  # that inherits Stanza can register a callback for itself
15
15
  # which is added to a list and iterated over when looking for
16
16
  # a callback to use
17
- def self.register(type, name = nil, ns = nil)
18
- @@handler_list << type
17
+ def self.register(handler, name = nil, ns = nil)
18
+ @@handler_list << handler
19
19
  self.handler_heirarchy ||= []
20
- self.handler_heirarchy.unshift type
20
+ self.handler_heirarchy.unshift handler
21
21
 
22
- name = name || self.name || type
22
+ name = name || self.registered_name || handler
23
23
  super name, ns
24
24
  end
25
25
 
@@ -44,24 +44,32 @@ module Blather
44
44
  ##
45
45
  # Automatically set the stanza's ID
46
46
  # and attach it to a document so XPath searching works
47
- def initialize(name = nil)
48
- super
49
- XML::Document.new.root = self
50
- self.name = name.to_s if name
51
- self.id = self.class.next_id
47
+ def self.new(name = nil)
48
+ node = super
49
+ node.name = name.to_s if name
50
+ node
52
51
  end
53
52
 
54
53
  ##
55
- # Helper method to ask the object if it's an error
56
- def error?
57
- self.type == :error
54
+ # Helper method to generate stanza guard methods
55
+ #
56
+ # attribute_helpers_for(:type, [:subscribe, :unsubscribe])
57
+ #
58
+ # This generates "subscribe?" and "unsubscribe?" methods that return
59
+ # true if self.type == :subscribe or :unsubscribe, respectively.
60
+ def self.attribute_helpers_for(attr, values)
61
+ [values].flatten.each do |v|
62
+ define_method("#{v}?") { __send__(attr) == v }
63
+ end
58
64
  end
59
65
 
66
+ attribute_helpers_for(:type, :error)
67
+
60
68
  ##
61
69
  # Copies itself then swaps from and to
62
70
  # then returns the new stanza
63
71
  def reply
64
- self.copy(true).reply!
72
+ self.dup.reply!
65
73
  end
66
74
 
67
75
  ##
@@ -71,23 +79,23 @@ module Blather
71
79
  self
72
80
  end
73
81
 
74
- attribute_accessor :id, :to_sym => false
82
+ attribute_accessor :id
75
83
 
76
84
  attribute_writer :to, :from
77
85
 
78
86
  ##
79
87
  # returns:: JID created from the "to" value of the stanza
80
88
  def to
81
- JID.new(attributes[:to]) if attributes[:to]
89
+ JID.new(self[:to]) if self[:to]
82
90
  end
83
91
 
84
92
  ##
85
93
  # returns:: JID created from the "from" value of the stanza
86
94
  def from
87
- JID.new(attributes[:from]) if attributes[:from]
95
+ JID.new(self[:from]) if self[:from]
88
96
  end
89
97
 
90
- attribute_accessor :type
98
+ attribute_accessor :type, :call => :to_sym
91
99
 
92
100
  ##
93
101
  # Transform the stanza into a stanza error
@@ -96,5 +104,14 @@ module Blather
96
104
  def as_error(name, type, text = nil, extras = [])
97
105
  StanzaError.new self, name, type, text, extras
98
106
  end
107
+
108
+ protected
109
+ def reply_if_needed!
110
+ unless @reversed_endpoints
111
+ reply!
112
+ @reversed_endpoints = true
113
+ end
114
+ self
115
+ end
99
116
  end
100
- end
117
+ end
@@ -2,7 +2,13 @@ module Blather
2
2
  class Stanza
3
3
 
4
4
  class Disco < Iq::Query
5
- attribute_accessor :node, :to_sym => false
5
+ def node
6
+ query[:node]
7
+ end
8
+
9
+ def node=(node)
10
+ query[:node] = node
11
+ end
6
12
  end
7
13
 
8
14
  end #Stanza
@@ -7,64 +7,76 @@ class Stanza
7
7
  class DiscoInfo < Disco
8
8
  register :disco_info, nil, 'http://jabber.org/protocol/disco#info'
9
9
 
10
- def initialize(type = nil, node = nil, identities = [], features = [])
11
- super type
12
-
13
- self.node = node
14
-
15
- [identities].flatten.each do |id|
16
- query << (id.is_a?(Identity) ? id : Identity.new(id[:name], id[:type], id[:category]))
17
- end
18
-
19
- [features].flatten.each do |feature|
20
- query << (feature.is_a?(Feature) ? feature : Feature.new(feature))
21
- end
10
+ def self.new(type = nil, node = nil, identities = [], features = [])
11
+ new_node = super type
12
+ new_node.node = node
13
+ [identities].flatten.each { |id| new_node.query << Identity.new(id) }
14
+ [features].flatten.each { |feature| new_node.query << Feature.new(feature) }
15
+ new_node
22
16
  end
23
17
 
24
18
  ##
25
19
  # List of identity objects
26
20
  def identities
27
- identities = query.find('identity')
28
- identities = query.find('query_ns:identity', :query_ns => self.class.ns) if identities.empty?
29
- identities.map { |i| Identity.new i }
21
+ query.find('//query_ns:identity', :query_ns => self.class.registered_ns).map { |i| Identity.new i }
30
22
  end
31
23
 
32
24
  ##
33
25
  # List of feature objects
34
26
  def features
35
- features = query.find('feature')
36
- features = query.find('query_ns:feature', :query_ns => self.class.ns) if features.empty?
37
- features.map { |i| Feature.new i }
27
+ query.find('//query_ns:feature', :query_ns => self.class.registered_ns).map { |i| Feature.new i }
38
28
  end
39
29
 
40
30
  class Identity < XMPPNode
41
- attribute_accessor :category, :type
42
- attribute_accessor :name, :to_sym => false
31
+ attribute_accessor :category, :type, :call => :to_sym
32
+ attribute_accessor :name
43
33
 
44
- def initialize(name, type = nil, category = nil)
45
- super :identity
34
+ def self.new(name, type = nil, category = nil)
35
+ new_node = super :identity
46
36
 
47
- if name.is_a?(XML::Node)
48
- self.inherit name
37
+ case name
38
+ when Nokogiri::XML::Node
39
+ new_node.inherit name
40
+ when Hash
41
+ new_node.name = name[:name]
42
+ new_node.type = name[:type]
43
+ new_node.category = name[:category]
49
44
  else
50
- self.name = name
51
- self.type = type
52
- self.category = category
45
+ new_node.name = name
46
+ new_node.type = type
47
+ new_node.category = category
53
48
  end
49
+ new_node
54
50
  end
51
+
52
+ def eql?(o)
53
+ raise "Cannot compare #{self.class} with #{o.class}" unless o.is_a?(self.class)
54
+ o.name == self.name &&
55
+ o.type == self.type &&
56
+ o.category == self.category
57
+ end
58
+ alias_method :==, :eql?
55
59
  end
56
60
 
57
61
  class Feature < XMPPNode
58
- attribute_accessor :var, :to_sym => false
62
+ attribute_accessor :var
59
63
 
60
- def initialize(var)
61
- super :feature
62
- if var.is_a?(XML::Node)
63
- self.inherit var
64
+ def self.new(var)
65
+ new_node = super :feature
66
+ case var
67
+ when Nokogiri::XML::Node
68
+ new_node.inherit var
64
69
  else
65
- self.var = var
70
+ new_node.var = var
66
71
  end
72
+ new_node
73
+ end
74
+
75
+ def eql?(o)
76
+ raise "Cannot compare #{self.class} with #{o.class}" unless o.is_a?(self.class)
77
+ o.var == self.var
67
78
  end
79
+ alias_method :==, :eql?
68
80
  end
69
81
  end
70
82
 
@@ -4,47 +4,58 @@ class Stanza
4
4
  class DiscoItems < Disco
5
5
  register :disco_items, nil, 'http://jabber.org/protocol/disco#items'
6
6
 
7
- def initialize(type = nil, node = nil, items = [])
8
- super type
9
- self.node = node
10
- [items].flatten.each do |item|
11
- query << (item.is_a?(Item) ? item : Item.new(item[:jid], item[:node], item[:name]))
12
- end
7
+ def self.new(type = nil, node = nil, items = [])
8
+ new_node = super type
9
+ new_node.node = node
10
+ [items].flatten.each { |item| new_node.query << Item.new(item) }
11
+ new_node
13
12
  end
14
13
 
15
14
  def items
16
- items = query.find('item')
17
- items = query.find('query_ns:item', :query_ns => self.class.ns) if items.empty?
18
- items.map { |i| Item.new i }
15
+ query.find('//query_ns:item', :query_ns => self.class.registered_ns).map { |i| Item.new i }
19
16
  end
20
17
 
21
18
  def node=(node)
22
- query.attributes[:node] = node
19
+ query[:node] = node
23
20
  end
24
21
 
25
22
  def node
26
- query.attributes[:node]
23
+ query[:node]
27
24
  end
28
25
 
29
26
  class Item < XMPPNode
30
- def initialize(jid, node = nil, name = nil)
31
- super :item
32
-
33
- if jid.is_a?(XML::Node)
34
- self.inherit jid
27
+ def self.new(jid, node = nil, name = nil)
28
+ new_node = super :item
29
+
30
+ case jid
31
+ when Nokogiri::XML::Node
32
+ new_node.inherit jid
33
+ when Hash
34
+ new_node.jid = jid[:jid]
35
+ new_node.node = jid[:node]
36
+ new_node.name = jid[:name]
35
37
  else
36
- self.jid = jid
37
- self.node = node
38
- self.name = name
38
+ new_node.jid = jid
39
+ new_node.node = node
40
+ new_node.name = name
39
41
  end
42
+ new_node
40
43
  end
41
44
 
42
45
  def jid
43
- (j = attributes[:jid]) ? JID.new(j) : nil
46
+ (j = self[:jid]) ? JID.new(j) : nil
44
47
  end
45
48
  attribute_writer :jid
46
49
 
47
- attribute_accessor :node, :name, :to_sym => false
50
+ attribute_accessor :node, :name
51
+
52
+ def eql?(o)
53
+ raise "Cannot compare #{self.class} with #{o.class}" unless o.is_a?(self.class)
54
+ o.jid == self.jid &&
55
+ o.node == self.node &&
56
+ o.name == self.name
57
+ end
58
+ alias_method :==, :eql?
48
59
  end
49
60
  end
50
61
 
@@ -9,17 +9,22 @@ class Stanza
9
9
  register :iq
10
10
 
11
11
  def self.import(node)
12
- raise(ArgumentError, "Import missmatch #{[node.element_name, self.name].inspect}") if node.element_name != self.name.to_s
13
12
  klass = nil
14
- node.children.each { |e| break if klass = class_from_registration(e.element_name, e.namespace) }
15
- (klass || self).new(node.attributes[:type]).inherit(node)
13
+ node.children.each { |e| break if klass = class_from_registration(e.element_name, (e.namespace.href if e.namespace)) }
14
+
15
+ if klass && klass != self
16
+ klass.import(node)
17
+ else
18
+ new(node[:type]).inherit(node)
19
+ end
16
20
  end
17
21
 
18
- def initialize(type = nil, to = nil, id = nil)
19
- super :iq
20
- self.type = type || :get
21
- self.to = to
22
- self.id = id if id
22
+ def self.new(type = nil, to = nil, id = nil)
23
+ node = super :iq
24
+ node.type = type || :get
25
+ node.to = to
26
+ node.id = id || self.next_id
27
+ node
23
28
  end
24
29
 
25
30
  VALID_TYPES.each do |valid_type|
@@ -6,16 +6,17 @@ class Iq
6
6
  register :query, :query
7
7
 
8
8
  ##
9
- # Ensure the namespace is set to the query node
10
- def initialize(type = nil)
11
- super
12
- query.namespace = self.class.ns
9
+ # Ensure the query node is created
10
+ def self.new(type = nil)
11
+ node = super
12
+ node.query
13
+ node
13
14
  end
14
15
 
15
16
  ##
16
17
  # Kill the query node before running inherit
17
18
  def inherit(node)
18
- query.remove!
19
+ query.remove
19
20
  super
20
21
  end
21
22
 
@@ -23,9 +24,16 @@ class Iq
23
24
  # Query node accessor
24
25
  # This will ensure there actually is a query node
25
26
  def query
26
- q = find_first('query')
27
- q = find_first('//query_ns:query', :query_ns => self.class.ns) if !q && self.class.ns
28
- (self << (q = XMPPNode.new('query'))) unless q
27
+ q = if self.class.registered_ns
28
+ find_first('query_ns:query', :query_ns => self.class.registered_ns)
29
+ else
30
+ find_first('query')
31
+ end
32
+
33
+ unless q
34
+ (self << (q = XMPPNode.new('query', self.document)))
35
+ q.namespace = self.class.registered_ns
36
+ end
29
37
  q
30
38
  end
31
39
 
@@ -7,9 +7,10 @@ class Iq
7
7
 
8
8
  ##
9
9
  # Any new items are added to the query
10
- def initialize(type = nil, item = nil)
11
- super type
12
- query << item if item
10
+ def self.new(type = nil, item = nil)
11
+ node = super type
12
+ node.query << item if item
13
+ node
13
14
  end
14
15
 
15
16
  ##
@@ -17,19 +18,17 @@ class Iq
17
18
  # Creates RosterItem objects out of each roster item as well.
18
19
  def inherit(node)
19
20
  # remove the current set of nodes
20
- items.each { |i| i.remove! }
21
+ remove_children :item
21
22
  super
22
23
  # transmogrify nodes into RosterItems
23
- items.each { |i| query << RosterItem.new(i); i.remove! }
24
+ items.each { |i| query << RosterItem.new(i); i.remove }
24
25
  self
25
26
  end
26
27
 
27
28
  ##
28
29
  # Roster items
29
30
  def items
30
- items = query.find('//item', self.class.ns)
31
- items = query.find('//query_ns:item', :query_ns => self.class.ns) if items.empty?
32
- items.map { |i| RosterItem.new(i) }
31
+ query.find('//ns:item', :ns => self.class.registered_ns).map { |i| RosterItem.new(i) }
33
32
  end
34
33
 
35
34
  class RosterItem < XMPPNode
@@ -38,42 +37,54 @@ class Iq
38
37
  # [name] name alias of the given JID
39
38
  # [subscription] subscription type
40
39
  # [ask] ask subscription sub-state
41
- def initialize(jid = nil, name = nil, subscription = nil, ask = nil)
42
- super :item
40
+ def self.new(jid = nil, name = nil, subscription = nil, ask = nil)
41
+ new_node = super :item
43
42
 
44
- if jid.is_a?(XML::Node)
45
- self.inherit jid
43
+ case jid
44
+ when Nokogiri::XML::Node
45
+ new_node.inherit jid
46
+ when Hash
47
+ new_node.jid = jid[:jid]
48
+ new_node.name = jid[:name]
49
+ new_node.subscription = jid[:subscription]
50
+ new_node.ask = jid[:ask]
46
51
  else
47
- self.jid = jid
48
- self.name = name
49
- self.subscription = subscription
50
- self.ask = ask
52
+ new_node.jid = jid
53
+ new_node.name = name
54
+ new_node.subscription = subscription
55
+ new_node.ask = ask
51
56
  end
57
+ new_node
52
58
  end
53
59
 
54
60
  ##
55
61
  # Roster item's JID
56
62
  def jid
57
- (j = attributes[:jid]) ? JID.new(j) : nil
63
+ (j = self[:jid]) ? JID.new(j) : nil
58
64
  end
59
65
  attribute_writer :jid
60
66
 
61
- attribute_accessor :name, :to_sym => false
67
+ attribute_accessor :name
62
68
 
63
- attribute_accessor :subscription, :ask
69
+ attribute_accessor :subscription, :ask, :call => :to_sym
64
70
 
65
71
  ##
66
72
  # The groups roster item belongs to
67
73
  def groups
68
- find(:group).map { |g| g.content }
74
+ find('child::*[local-name()="group"]').map { |g| g.content }
69
75
  end
70
76
 
71
77
  ##
72
78
  # Set the roster item's groups
73
79
  # must be an array
74
80
  def groups=(new_groups)
75
- find(:group).each { |g| g.remove! }
76
- new_groups.uniq.each { |g| self << XMPPNode.new(:group, g) } if new_groups
81
+ remove_children :group
82
+ if new_groups
83
+ new_groups.uniq.each do |g|
84
+ self << (group = XMPPNode.new(:group, self.document))
85
+ group.content = g
86
+ end
87
+ end
77
88
  end
78
89
 
79
90
  ##