tp-blather 0.8.2

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 (150) hide show
  1. data/.autotest +13 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +8 -0
  6. data/CHANGELOG.md +249 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +5 -0
  9. data/LICENSE +22 -0
  10. data/README.md +413 -0
  11. data/Rakefile +20 -0
  12. data/TODO.md +2 -0
  13. data/blather.gemspec +51 -0
  14. data/examples/certs/README +20 -0
  15. data/examples/certs/ca-bundle.crt +3987 -0
  16. data/examples/echo.rb +19 -0
  17. data/examples/execute.rb +17 -0
  18. data/examples/ping_pong.rb +38 -0
  19. data/examples/print_hierarchy.rb +77 -0
  20. data/examples/rosterprint.rb +15 -0
  21. data/examples/stream_only.rb +28 -0
  22. data/examples/trusted_echo.rb +21 -0
  23. data/examples/xmpp4r/echo.rb +36 -0
  24. data/lib/blather.rb +112 -0
  25. data/lib/blather/cert_store.rb +53 -0
  26. data/lib/blather/client.rb +95 -0
  27. data/lib/blather/client/client.rb +345 -0
  28. data/lib/blather/client/dsl.rb +320 -0
  29. data/lib/blather/client/dsl/pubsub.rb +174 -0
  30. data/lib/blather/core_ext/eventmachine.rb +125 -0
  31. data/lib/blather/core_ext/ipaddr.rb +20 -0
  32. data/lib/blather/errors.rb +69 -0
  33. data/lib/blather/errors/sasl_error.rb +44 -0
  34. data/lib/blather/errors/stanza_error.rb +110 -0
  35. data/lib/blather/errors/stream_error.rb +84 -0
  36. data/lib/blather/file_transfer.rb +107 -0
  37. data/lib/blather/file_transfer/ibb.rb +68 -0
  38. data/lib/blather/file_transfer/s5b.rb +114 -0
  39. data/lib/blather/jid.rb +141 -0
  40. data/lib/blather/roster.rb +118 -0
  41. data/lib/blather/roster_item.rb +146 -0
  42. data/lib/blather/stanza.rb +167 -0
  43. data/lib/blather/stanza/disco.rb +32 -0
  44. data/lib/blather/stanza/disco/capabilities.rb +161 -0
  45. data/lib/blather/stanza/disco/disco_info.rb +205 -0
  46. data/lib/blather/stanza/disco/disco_items.rb +134 -0
  47. data/lib/blather/stanza/iq.rb +144 -0
  48. data/lib/blather/stanza/iq/command.rb +339 -0
  49. data/lib/blather/stanza/iq/ibb.rb +86 -0
  50. data/lib/blather/stanza/iq/ping.rb +50 -0
  51. data/lib/blather/stanza/iq/query.rb +53 -0
  52. data/lib/blather/stanza/iq/roster.rb +185 -0
  53. data/lib/blather/stanza/iq/s5b.rb +208 -0
  54. data/lib/blather/stanza/iq/si.rb +415 -0
  55. data/lib/blather/stanza/iq/vcard.rb +149 -0
  56. data/lib/blather/stanza/message.rb +428 -0
  57. data/lib/blather/stanza/message/muc_user.rb +119 -0
  58. data/lib/blather/stanza/muc/muc_user_base.rb +54 -0
  59. data/lib/blather/stanza/presence.rb +172 -0
  60. data/lib/blather/stanza/presence/c.rb +100 -0
  61. data/lib/blather/stanza/presence/muc.rb +35 -0
  62. data/lib/blather/stanza/presence/muc_user.rb +147 -0
  63. data/lib/blather/stanza/presence/status.rb +218 -0
  64. data/lib/blather/stanza/presence/subscription.rb +100 -0
  65. data/lib/blather/stanza/pubsub.rb +119 -0
  66. data/lib/blather/stanza/pubsub/affiliations.rb +79 -0
  67. data/lib/blather/stanza/pubsub/create.rb +65 -0
  68. data/lib/blather/stanza/pubsub/errors.rb +18 -0
  69. data/lib/blather/stanza/pubsub/event.rb +139 -0
  70. data/lib/blather/stanza/pubsub/items.rb +103 -0
  71. data/lib/blather/stanza/pubsub/publish.rb +103 -0
  72. data/lib/blather/stanza/pubsub/retract.rb +92 -0
  73. data/lib/blather/stanza/pubsub/subscribe.rb +68 -0
  74. data/lib/blather/stanza/pubsub/subscription.rb +135 -0
  75. data/lib/blather/stanza/pubsub/subscriptions.rb +83 -0
  76. data/lib/blather/stanza/pubsub/unsubscribe.rb +84 -0
  77. data/lib/blather/stanza/pubsub_owner.rb +51 -0
  78. data/lib/blather/stanza/pubsub_owner/delete.rb +52 -0
  79. data/lib/blather/stanza/pubsub_owner/purge.rb +52 -0
  80. data/lib/blather/stanza/x.rb +416 -0
  81. data/lib/blather/stream.rb +266 -0
  82. data/lib/blather/stream/client.rb +32 -0
  83. data/lib/blather/stream/component.rb +39 -0
  84. data/lib/blather/stream/features.rb +70 -0
  85. data/lib/blather/stream/features/register.rb +38 -0
  86. data/lib/blather/stream/features/resource.rb +63 -0
  87. data/lib/blather/stream/features/sasl.rb +190 -0
  88. data/lib/blather/stream/features/session.rb +45 -0
  89. data/lib/blather/stream/features/tls.rb +29 -0
  90. data/lib/blather/stream/parser.rb +102 -0
  91. data/lib/blather/version.rb +3 -0
  92. data/lib/blather/xmpp_node.rb +94 -0
  93. data/spec/blather/client/client_spec.rb +687 -0
  94. data/spec/blather/client/dsl/pubsub_spec.rb +492 -0
  95. data/spec/blather/client/dsl_spec.rb +266 -0
  96. data/spec/blather/errors/sasl_error_spec.rb +33 -0
  97. data/spec/blather/errors/stanza_error_spec.rb +129 -0
  98. data/spec/blather/errors/stream_error_spec.rb +108 -0
  99. data/spec/blather/errors_spec.rb +33 -0
  100. data/spec/blather/file_transfer_spec.rb +135 -0
  101. data/spec/blather/jid_spec.rb +87 -0
  102. data/spec/blather/roster_item_spec.rb +134 -0
  103. data/spec/blather/roster_spec.rb +107 -0
  104. data/spec/blather/stanza/discos/disco_info_spec.rb +247 -0
  105. data/spec/blather/stanza/discos/disco_items_spec.rb +154 -0
  106. data/spec/blather/stanza/iq/command_spec.rb +206 -0
  107. data/spec/blather/stanza/iq/ibb_spec.rb +124 -0
  108. data/spec/blather/stanza/iq/ping_spec.rb +45 -0
  109. data/spec/blather/stanza/iq/query_spec.rb +64 -0
  110. data/spec/blather/stanza/iq/roster_spec.rb +139 -0
  111. data/spec/blather/stanza/iq/s5b_spec.rb +57 -0
  112. data/spec/blather/stanza/iq/si_spec.rb +98 -0
  113. data/spec/blather/stanza/iq/vcard_spec.rb +93 -0
  114. data/spec/blather/stanza/iq_spec.rb +61 -0
  115. data/spec/blather/stanza/message/muc_user_spec.rb +152 -0
  116. data/spec/blather/stanza/message_spec.rb +282 -0
  117. data/spec/blather/stanza/presence/c_spec.rb +56 -0
  118. data/spec/blather/stanza/presence/muc_spec.rb +37 -0
  119. data/spec/blather/stanza/presence/muc_user_spec.rb +83 -0
  120. data/spec/blather/stanza/presence/status_spec.rb +144 -0
  121. data/spec/blather/stanza/presence/subscription_spec.rb +102 -0
  122. data/spec/blather/stanza/presence_spec.rb +125 -0
  123. data/spec/blather/stanza/pubsub/affiliations_spec.rb +57 -0
  124. data/spec/blather/stanza/pubsub/create_spec.rb +56 -0
  125. data/spec/blather/stanza/pubsub/event_spec.rb +98 -0
  126. data/spec/blather/stanza/pubsub/items_spec.rb +79 -0
  127. data/spec/blather/stanza/pubsub/publish_spec.rb +83 -0
  128. data/spec/blather/stanza/pubsub/retract_spec.rb +75 -0
  129. data/spec/blather/stanza/pubsub/subscribe_spec.rb +61 -0
  130. data/spec/blather/stanza/pubsub/subscription_spec.rb +97 -0
  131. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +59 -0
  132. data/spec/blather/stanza/pubsub/unsubscribe_spec.rb +74 -0
  133. data/spec/blather/stanza/pubsub_owner/delete_spec.rb +50 -0
  134. data/spec/blather/stanza/pubsub_owner/purge_spec.rb +50 -0
  135. data/spec/blather/stanza/pubsub_owner_spec.rb +27 -0
  136. data/spec/blather/stanza/pubsub_spec.rb +68 -0
  137. data/spec/blather/stanza/x_spec.rb +231 -0
  138. data/spec/blather/stanza_spec.rb +134 -0
  139. data/spec/blather/stream/client_spec.rb +1090 -0
  140. data/spec/blather/stream/component_spec.rb +108 -0
  141. data/spec/blather/stream/parser_spec.rb +152 -0
  142. data/spec/blather/stream/ssl_spec.rb +32 -0
  143. data/spec/blather/xmpp_node_spec.rb +47 -0
  144. data/spec/blather_spec.rb +34 -0
  145. data/spec/fixtures/pubsub.rb +311 -0
  146. data/spec/spec_helper.rb +17 -0
  147. data/yard/templates/default/class/html/handlers.erb +18 -0
  148. data/yard/templates/default/class/setup.rb +10 -0
  149. data/yard/templates/default/class/text/handlers.erb +1 -0
  150. metadata +459 -0
@@ -0,0 +1,118 @@
1
+ module Blather
2
+
3
+ # Local Roster
4
+ # Takes care of adding/removing JIDs through the stream
5
+ class Roster
6
+ include Enumerable
7
+
8
+ # Create a new roster
9
+ #
10
+ # @param [Blather::Stream] stream the stream the roster should use to
11
+ # update roster entries
12
+ # @param [Blather::Stanza::Roster] stanza a roster stanza used to preload
13
+ # the roster
14
+ # @return [Blather::Roster]
15
+ def initialize(stream, stanza = nil)
16
+ @stream = stream
17
+ @items = {}
18
+ stanza.items.each { |i| push i, false } if stanza
19
+ end
20
+
21
+ # Process any incoming stanzas and either adds or removes the
22
+ # corresponding RosterItem
23
+ #
24
+ # @param [Blather::Stanza::Roster] stanza a roster stanza
25
+ def process(stanza)
26
+ stanza.items.each do |i|
27
+ case i.subscription
28
+ when :remove then @items.delete(key(i.jid))
29
+ else @items[key(i.jid)] = RosterItem.new(i)
30
+ end
31
+ end
32
+ end
33
+
34
+ # Pushes a JID into the roster
35
+ #
36
+ # @param [String, Blather::JID, #jid] elem a JID to add to the roster
37
+ # @return [self]
38
+ # @see #push
39
+ def <<(elem)
40
+ push elem
41
+ self
42
+ end
43
+
44
+ # Push a JID into the roster and update the server
45
+ #
46
+ # @param [String, Blather::JID, #jid] elem a jid to add to the roster
47
+ # @param [true, false] send send the update over the wire
48
+ # @see Blather::JID
49
+ def push(elem, send = true)
50
+ jid = elem.respond_to?(:jid) ? elem.jid : JID.new(elem)
51
+ @items[key(jid)] = node = RosterItem.new(elem)
52
+
53
+ @stream.write(node.to_stanza(:set)) if send
54
+ end
55
+ alias_method :add, :push
56
+
57
+ # Remove a JID from the roster and update the server
58
+ #
59
+ # @param [String, Blather::JID] jid the JID to remove from the roster
60
+ def delete(jid)
61
+ @items.delete key(jid)
62
+ item = Stanza::Iq::Roster::RosterItem.new(jid, nil, :remove)
63
+ @stream.write Stanza::Iq::Roster.new(:set, item)
64
+ end
65
+ alias_method :remove, :delete
66
+
67
+ # Get a RosterItem by JID
68
+ #
69
+ # @param [String, Blather::JID] jid the jid of the item to return
70
+ # @return [Blather::RosterItem, nil] the associated RosterItem
71
+ def [](jid)
72
+ items[key(jid)]
73
+ end
74
+
75
+ # Iterate over all RosterItems
76
+ #
77
+ # @yield [Blather::RosterItem] yields each RosterItem
78
+ def each(&block)
79
+ items.values.each &block
80
+ end
81
+
82
+ # Get a duplicate of all RosterItems
83
+ #
84
+ # @return [Array<Blather::RosterItem>] a duplicate of all RosterItems
85
+ def items
86
+ @items.dup
87
+ end
88
+
89
+ # Number of items in the roster
90
+ #
91
+ # @return [Integer] the number of items in the roster
92
+ def length
93
+ @items.length
94
+ end
95
+
96
+ # A hash of items keyed by group
97
+ #
98
+ # @return [Hash<group => Array<RosterItem>>]
99
+ def grouped
100
+ @items.values.sort.inject(Hash.new{|h,k|h[k]=[]}) do |hash, item|
101
+ item.groups.each { |group| hash[group] << item }
102
+ hash
103
+ end
104
+ end
105
+
106
+ private
107
+ # Creates a stripped jid
108
+ def self.key(jid)
109
+ JID.new(jid).stripped.to_s
110
+ end
111
+
112
+ # Instance method to wrap around the class method
113
+ def key(jid)
114
+ self.class.key(jid)
115
+ end
116
+ end # Roster
117
+
118
+ end # Blather
@@ -0,0 +1,146 @@
1
+ module Blather
2
+
3
+ # RosterItems hold internal representations of the user's roster
4
+ # including each JID's status.
5
+ class RosterItem
6
+ # @private
7
+ VALID_SUBSCRIPTION_TYPES = [:both, :from, :none, :remove, :to].freeze
8
+
9
+ attr_reader :jid,
10
+ :ask,
11
+ :statuses
12
+
13
+ attr_accessor :name,
14
+ :groups
15
+
16
+ # @private
17
+ def self.new(item)
18
+ return item if item.is_a?(self)
19
+ super
20
+ end
21
+
22
+ # Create a new RosterItem
23
+ #
24
+ # @overload initialize(jid)
25
+ # Create a new RosterItem based on a JID
26
+ # @param [Blather::JID] jid the JID object
27
+ # @overload initialize(jid)
28
+ # Create a new RosterItem based on a JID string
29
+ # @param [String] jid a JID string
30
+ # @overload initialize(node)
31
+ # Create a new RosterItem based on a stanza
32
+ # @param [Blather::Stanza::Iq::Roster::RosterItem] node a RosterItem
33
+ # stanza
34
+ # @return [Blather::RosterItem] the new RosterItem
35
+ def initialize(item)
36
+ @statuses = []
37
+ @groups = []
38
+
39
+ case item
40
+ when JID
41
+ self.jid = item.stripped
42
+ when String
43
+ self.jid = JID.new(item).stripped
44
+ when XMPPNode
45
+ self.jid = JID.new(item[:jid]).stripped
46
+ self.name = item[:name]
47
+ self.subscription = item[:subscription]
48
+ self.ask = item[:ask]
49
+ item.groups.each { |g| @groups << g }
50
+ end
51
+
52
+ @groups = [nil] if @groups.empty?
53
+ end
54
+
55
+ # Set the jid
56
+ #
57
+ # @param [String, Blather::JID] jid the new jid
58
+ # @see Blather::JID
59
+ def jid=(jid)
60
+ @jid = JID.new(jid).stripped
61
+ end
62
+
63
+ # Set the subscription
64
+ # Ensures it is one of VALID_SUBSCRIPTION_TYPES
65
+ #
66
+ # @param [#to_sym] sub the new subscription
67
+ def subscription=(sub)
68
+ if sub && !VALID_SUBSCRIPTION_TYPES.include?(sub = sub.to_sym)
69
+ raise ArgumentError, "Invalid Type (#{sub}), use: #{VALID_SUBSCRIPTION_TYPES*' '}"
70
+ end
71
+ @subscription = sub ? sub : :none
72
+ end
73
+
74
+ # Get the current subscription
75
+ #
76
+ # @return [:both, :from, :none, :remove, :to]
77
+ def subscription
78
+ @subscription || :none
79
+ end
80
+
81
+ # Set the ask value
82
+ #
83
+ # @param [nil, :subscribe] ask the new ask
84
+ def ask=(ask)
85
+ if ask && (ask = ask.to_sym) != :subscribe
86
+ raise ArgumentError, "Invalid Type (#{ask}), can only be :subscribe"
87
+ end
88
+ @ask = ask ? ask : nil
89
+ end
90
+
91
+ # Set the status then sorts them according to priority
92
+ #
93
+ # @param [Blather::Stanza::Status] the new status
94
+ def status=(presence)
95
+ @statuses.delete_if { |s| s.from == presence.from || s.state == :unavailable }
96
+ @statuses << presence
97
+ @statuses.sort!
98
+ end
99
+
100
+ # The status with the highest priority
101
+ #
102
+ # @param [String, nil] resource the resource to get the status of
103
+ def status(resource = nil)
104
+ top = if resource
105
+ @statuses.detect { |s| s.from.resource == resource }
106
+ else
107
+ @statuses.last
108
+ end
109
+ end
110
+
111
+ # Translate the RosterItem into a proper stanza that can be sent over the
112
+ # stream
113
+ #
114
+ # @return [Blather::Stanza::Iq::Roster]
115
+ def to_stanza(type = nil)
116
+ Stanza::Iq::Roster.new type, to_node
117
+ end
118
+
119
+ def to_node
120
+ Stanza::Iq::Roster::RosterItem.new jid, name, subscription, ask, groups
121
+ end
122
+
123
+ # Compare two RosterItems by their JID
124
+ #
125
+ # @param [Blather::JID] other the JID to compare against
126
+ # @return [Fixnum<-1, 0, 1>]
127
+ def <=>(other)
128
+ JID.new(self.jid) <=> JID.new(other.jid)
129
+ end
130
+
131
+ # Compare two RosterItem objects by name, type and category
132
+ # @param [RosterItem] o the Identity object to compare against
133
+ # @return [true, false]
134
+ def eql?(o, *fields)
135
+ o.is_a?(self.class) &&
136
+ o.jid == self.jid &&
137
+ o.groups == self.groups
138
+ end
139
+
140
+ # @private
141
+ def ==(o)
142
+ eql?(o)
143
+ end
144
+ end #RosterItem
145
+
146
+ end
@@ -0,0 +1,167 @@
1
+ module Blather
2
+
3
+ # # Base XMPP Stanza
4
+ #
5
+ # All stanzas inherit this class. It provides a set of methods and helpers
6
+ # common to all XMPP Stanzas
7
+ #
8
+ # @handler :stanza
9
+ class Stanza < XMPPNode
10
+ # @private
11
+ @@last_id = 0
12
+ # @private
13
+ @@handler_list = []
14
+
15
+ class_attribute :handler_hierarchy
16
+ attr_writer :handler_hierarchy
17
+
18
+ # Registers a callback onto the callback stack
19
+ #
20
+ # @param [Symbol] handler the name of the handler
21
+ # @param [Symbol, String, nil] name the name of the first element in the
22
+ # stanza. If nil the inherited name will be used. If that's nil the
23
+ # handler name will be used.
24
+ # @param [String, nil] ns the namespace of the stanza
25
+ def self.register(handler, name = nil, ns = nil)
26
+ @@handler_list << handler
27
+ self.handler_hierarchy ||= [:stanza]
28
+ self.handler_hierarchy = [handler] + self.handler_hierarchy
29
+
30
+ name = name || self.registered_name || handler
31
+ super name, ns
32
+ end
33
+
34
+ def initialize(*args)
35
+ super
36
+ @handler_hierarchy = []
37
+ end
38
+
39
+ def handler_hierarchy
40
+ @handler_hierarchy + self.class.handler_hierarchy
41
+ end
42
+
43
+ # The handler stack for the current stanza class
44
+ #
45
+ # @return [Array<Symbol>]
46
+ def self.handler_list
47
+ @@handler_list
48
+ end
49
+
50
+ # Helper method that creates a unique ID for stanzas
51
+ #
52
+ # @return [String] a new unique ID
53
+ def self.next_id
54
+ @@last_id += 1
55
+ 'blather%04x' % @@last_id
56
+ end
57
+
58
+ # Check if the stanza is an error stanza
59
+ #
60
+ # @return [true, false]
61
+ def error?
62
+ self.type == :error
63
+ end
64
+
65
+ # Creates a copy with to and from swapped
66
+ #
67
+ # @param [Hash] opts options to pass to reply!
68
+ # @option opts [Boolean] :remove_children Wether or not to remove child nodes when replying
69
+ #
70
+ # @return [Blather::Stanza]
71
+ def reply(opts = {})
72
+ self.dup.reply! opts
73
+ end
74
+
75
+ # Swaps from and to
76
+ #
77
+ # @param [Hash] opts Misc options
78
+ # @option opts [Boolean] :remove_children Wether or not to remove child nodes when replying
79
+ #
80
+ # @return [self]
81
+ def reply!(opts = {})
82
+ opts = {:remove_children => false}.merge opts
83
+ self.to, self.from = self.from, self.to
84
+ self.children.remove if opts[:remove_children]
85
+ self
86
+ end
87
+
88
+ # Get the stanza's ID
89
+ #
90
+ # @return [String, nil]
91
+ def id
92
+ read_attr :id
93
+ end
94
+
95
+ # Set the stanza's ID
96
+ #
97
+ # @param [#to_s] id the new stanza ID
98
+ def id=(id)
99
+ write_attr :id, id
100
+ end
101
+
102
+ # Get the stanza's to
103
+ #
104
+ # @return [Blather::JID, nil]
105
+ def to
106
+ JID.new(self[:to]) if self[:to]
107
+ end
108
+
109
+ # Set the stanza's to field
110
+ #
111
+ # @param [#to_s] to the new JID for the to field
112
+ def to=(to)
113
+ write_attr :to, to
114
+ end
115
+
116
+ # Get the stanza's from
117
+ #
118
+ # @return [Blather::JID, nil]
119
+ def from
120
+ JID.new(self[:from]) if self[:from]
121
+ end
122
+
123
+ # Set the stanza's from field
124
+ #
125
+ # @param [#to_s] from the new JID for the from field
126
+ def from=(from)
127
+ write_attr :from, from
128
+ end
129
+
130
+ # Get the stanza's type
131
+ #
132
+ # @return [Symbol, nil]
133
+ def type
134
+ read_attr :type, :to_sym
135
+ end
136
+
137
+ # Set the stanza's type
138
+ #
139
+ # @param [#to_s] type the new stanza type
140
+ def type=(type)
141
+ write_attr :type, type
142
+ end
143
+
144
+ # Create an error stanza from the current stanza
145
+ #
146
+ # @param [String] name the error name
147
+ # @param [<Blather::StanzaError::VALID_TYPES>] type the error type
148
+ # @param [String, nil] text the error text
149
+ # @param [Array<XML::Node>] extras an array of extra nodes to attach to
150
+ # the error
151
+ #
152
+ # @return [Blather::StanzaError]
153
+ def as_error(name, type, text = nil, extras = [])
154
+ StanzaError.new self, name, type, text, extras
155
+ end
156
+
157
+ protected
158
+ # @private
159
+ def reply_if_needed!
160
+ unless @reversed_endpoints
161
+ reply!
162
+ @reversed_endpoints = true
163
+ end
164
+ self
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,32 @@
1
+ module Blather
2
+ class Stanza
3
+
4
+ # # Disco Base class
5
+ #
6
+ # Use Blather::Stanza::DiscoInfo or Blather::Stanza::DiscoItems
7
+ class Disco < Iq::Query
8
+
9
+ # Get the name of the node
10
+ #
11
+ # @return [String] the node name
12
+ def node
13
+ query[:node]
14
+ end
15
+
16
+ # Set the name of the node
17
+ #
18
+ # @param [#to_s] node the new node name
19
+ def node=(node)
20
+ query[:node] = node
21
+ end
22
+
23
+ # Compare two Disco objects by name, type and category
24
+ # @param [Disco] o the Identity object to compare against
25
+ # @return [true, false]
26
+ def eql?(o, *fields)
27
+ super o, *(fields + [:node])
28
+ end
29
+ end
30
+
31
+ end # Stanza
32
+ end # Blather