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,63 @@
1
+ module Blather
2
+ class Stream
3
+
4
+ # @private
5
+ class Resource < Features
6
+ BIND_NS = 'urn:ietf:params:xml:ns:xmpp-bind'.freeze
7
+ register BIND_NS
8
+
9
+ def initialize(stream, succeed, fail)
10
+ super
11
+ @jid = stream.jid
12
+ end
13
+
14
+ def receive_data(stanza)
15
+ @node = stanza
16
+ case stanza.element_name
17
+ when 'bind' then bind
18
+ when 'iq' then result
19
+ else fail!(UnknownResponse.new(@node))
20
+ end
21
+ end
22
+
23
+ private
24
+ ##
25
+ # Respond to the bind request
26
+ # If @jid has a resource set already request it from the server
27
+ def bind
28
+ response = Stanza::Iq.new :set
29
+ @id = response.id
30
+
31
+ response << (binder = XMPPNode.new('bind', response.document))
32
+ binder.namespace = BIND_NS
33
+
34
+ if @jid.resource
35
+ binder << (resource = XMPPNode.new('resource', binder.document))
36
+ resource.content = @jid.resource
37
+ end
38
+
39
+ @stream.send response
40
+ end
41
+
42
+ ##
43
+ # Process the result from the server
44
+ # Sets the sends the JID (now bound to a resource)
45
+ # back to the stream
46
+ def result
47
+ if @node[:type] == 'error'
48
+ fail! StanzaError.import(@node)
49
+ return
50
+ end
51
+
52
+ # ensure this is a response to our original request
53
+ if @id == @node['id']
54
+ @stream.jid = JID.new @node.find_first('bind_ns:bind/bind_ns:jid', :bind_ns => BIND_NS).content
55
+ succeed!
56
+ else
57
+ fail!("BIND result ID mismatch. Expected: #{@id}. Received: #{@node['id']}")
58
+ end
59
+ end
60
+ end #Resource
61
+
62
+ end #Stream
63
+ end #Blather
@@ -0,0 +1,190 @@
1
+ module Blather
2
+ class Stream
3
+
4
+ # @private
5
+ class SASL < Features
6
+ class UnknownMechanism < BlatherError
7
+ register :sasl_unknown_mechanism
8
+ end
9
+
10
+ MECHANISMS = %w[
11
+ digest-md5
12
+ plain
13
+ anonymous
14
+ ].freeze
15
+
16
+ SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze
17
+ register SASL_NS
18
+
19
+ def initialize(stream, succeed, fail)
20
+ super
21
+ @jid = @stream.jid
22
+ @pass = @stream.password
23
+ @mechanisms = []
24
+ end
25
+
26
+ def receive_data(stanza)
27
+ @node = stanza
28
+ case stanza.element_name
29
+ when 'mechanisms'
30
+ available_mechanisms = stanza.children.map { |m| m.content.downcase }
31
+ @mechanisms = MECHANISMS.select { |m| available_mechanisms.include? m }
32
+ next!
33
+ when 'failure'
34
+ next!
35
+ when 'success'
36
+ @stream.start
37
+ else
38
+ if self.respond_to?(stanza.element_name)
39
+ self.__send__(stanza.element_name)
40
+ else
41
+ fail! UnknownResponse.new(stanza)
42
+ end
43
+ end
44
+ end
45
+
46
+ protected
47
+ def next!
48
+ if @jid.node == ''
49
+ process_anonymous
50
+ else
51
+ @idx = @idx ? @idx+1 : 0
52
+ authenticate_with @mechanisms[@idx]
53
+ end
54
+ end
55
+
56
+ def process_anonymous
57
+ if @mechanisms.include?('anonymous')
58
+ authenticate_with 'anonymous'
59
+ else
60
+ fail! BlatherError.new('The server does not support ANONYMOUS login. You must provide a node in the JID')
61
+ end
62
+ end
63
+
64
+ def authenticate_with(method)
65
+ method = case method
66
+ when 'digest-md5' then DigestMD5
67
+ when 'plain' then Plain
68
+ when 'anonymous' then Anonymous
69
+ when nil then fail!(SASLError.import(@node))
70
+ else next!
71
+ end
72
+
73
+ if method.is_a?(Module)
74
+ extend method
75
+ authenticate
76
+ end
77
+ end
78
+
79
+ ##
80
+ # Base64 Encoder
81
+ def b64(str)
82
+ [str].pack('m').gsub(/\s/,'')
83
+ end
84
+
85
+ ##
86
+ # Builds a standard auth node
87
+ def auth_node(mechanism, content = nil)
88
+ node = XMPPNode.new 'auth'
89
+ node.content = content if content
90
+ node.namespace = SASL_NS
91
+ node[:mechanism] = mechanism
92
+ node
93
+ end
94
+
95
+ ##
96
+ # Digest MD5 authentication
97
+ module DigestMD5 # :nodoc:
98
+ ##
99
+ # Lets the server know we're going to try DigestMD5 authentication
100
+ def authenticate
101
+ @stream.send auth_node('DIGEST-MD5')
102
+ end
103
+
104
+ ##
105
+ # Receive the challenge command.
106
+ def challenge
107
+ decode_challenge
108
+ respond
109
+ end
110
+
111
+ private
112
+ ##
113
+ # Decodes digest strings 'foo=bar,baz="faz"'
114
+ # into {'foo' => 'bar', 'baz' => 'faz'}
115
+ def decode_challenge
116
+ text = @node.content.unpack('m').first
117
+ res = {}
118
+
119
+ text.split(',').each do |statement|
120
+ key, value = statement.split('=')
121
+ res[key] = value.delete('"') unless key.empty?
122
+ end
123
+ Blather.log "CHALLENGE DECODE: #{res.inspect}"
124
+
125
+ @nonce ||= res['nonce']
126
+ @realm ||= res['realm']
127
+ end
128
+
129
+ ##
130
+ # Builds the properly encoded challenge response
131
+ def generate_response
132
+ a1 = "#{d("#{@response[:username]}:#{@response[:realm]}:#{@pass}")}:#{@response[:nonce]}:#{@response[:cnonce]}"
133
+ a2 = "AUTHENTICATE:#{@response[:'digest-uri']}"
134
+ h("#{h(a1)}:#{@response[:nonce]}:#{@response[:nc]}:#{@response[:cnonce]}:#{@response[:qop]}:#{h(a2)}")
135
+ end
136
+
137
+ ##
138
+ # Send challenge response
139
+ def respond
140
+ node = XMPPNode.new 'response'
141
+ node.namespace = SASL_NS
142
+
143
+ unless @initial_response_sent
144
+ @initial_response_sent = true
145
+ @response = {
146
+ :nonce => @nonce,
147
+ :charset => 'utf-8',
148
+ :username => @jid.node,
149
+ :realm => @realm || @jid.domain,
150
+ :cnonce => h(Time.new.to_f.to_s),
151
+ :nc => '00000001',
152
+ :qop => 'auth',
153
+ :'digest-uri' => "xmpp/#{@jid.domain}",
154
+ }
155
+ @response[:response] = generate_response
156
+ @response.each { |k,v| @response[k] = "\"#{v}\"" unless [:nc, :qop, :response, :charset].include?(k) }
157
+
158
+ Blather.log "CHALLENGE RESPONSE: #{@response.inspect}"
159
+ Blather.log "CH RESP TXT: #{@response.map { |k,v| "#{k}=#{v}" } * ','}"
160
+
161
+ # order is to simplify testing
162
+ # Ruby 1.9 eliminates the need for this with ordered hashes
163
+ order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri', :response]
164
+ node.content = b64(order.map { |k| v = @response[k]; "#{k}=#{v}" } * ',')
165
+ end
166
+
167
+ @stream.send node
168
+ end
169
+
170
+ def d(s); Digest::MD5.digest(s); end
171
+ def h(s); Digest::MD5.hexdigest(s); end
172
+ end #DigestMD5
173
+
174
+ # @private
175
+ module Plain
176
+ def authenticate
177
+ @stream.send auth_node('PLAIN', b64("#{@jid.stripped}\x00#{@jid.node}\x00#{@pass}"))
178
+ end
179
+ end #Plain
180
+
181
+ # @private
182
+ module Anonymous
183
+ def authenticate
184
+ @stream.send auth_node('ANONYMOUS')
185
+ end
186
+ end #Anonymous
187
+ end #SASL
188
+
189
+ end #Stream
190
+ end
@@ -0,0 +1,45 @@
1
+ module Blather
2
+ class Stream
3
+
4
+ # @private
5
+ class Session < Features
6
+ SESSION_NS = 'urn:ietf:params:xml:ns:xmpp-session'.freeze
7
+ register SESSION_NS
8
+
9
+ def initialize(stream, succeed, fail)
10
+ super
11
+ @to = @stream.jid.domain
12
+ end
13
+
14
+ def receive_data(stanza)
15
+ @node = stanza
16
+ case stanza.element_name
17
+ when 'session' then session
18
+ when 'iq' then check_response
19
+ else fail!(UnknownResponse.new(stanza))
20
+ end
21
+ end
22
+
23
+ private
24
+ def check_response
25
+ if @node[:type] == 'result'
26
+ succeed!
27
+ else
28
+ fail!(StanzaError.import(@node))
29
+ end
30
+ end
31
+
32
+ ##
33
+ # Send a start session command
34
+ def session
35
+ response = Stanza::Iq.new :set
36
+ response.to = @to
37
+ response << (sess = XMPPNode.new('session', response.document))
38
+ sess.namespace = SESSION_NS
39
+
40
+ @stream.send response
41
+ end
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ module Blather
2
+ class Stream
3
+
4
+ # @private
5
+ class TLS < Features
6
+ class TLSFailure < BlatherError
7
+ register :tls_failure
8
+ end
9
+
10
+ TLS_NS = 'urn:ietf:params:xml:ns:xmpp-tls'.freeze
11
+ register TLS_NS
12
+
13
+ def receive_data(stanza)
14
+ case stanza.element_name
15
+ when 'starttls'
16
+ @stream.send "<starttls xmlns='#{TLS_NS}'/>"
17
+ when 'proceed'
18
+ @stream.start_tls(:verify_peer => true)
19
+ @stream.start
20
+ # succeed!
21
+ else
22
+ fail! TLSFailure.new
23
+ end
24
+ end
25
+
26
+ end #TLS
27
+
28
+ end #Stream
29
+ end #Blather
@@ -0,0 +1,102 @@
1
+ module Blather
2
+ class Stream
3
+
4
+ # @private
5
+ class Parser < Nokogiri::XML::SAX::Document
6
+ NS_TO_IGNORE = %w[jabber:client jabber:component:accept]
7
+
8
+ @@debug = false
9
+ def self.debug; @@debug; end
10
+ def self.debug=(debug); @@debug = debug; end
11
+
12
+ def initialize(receiver)
13
+ @receiver = receiver
14
+ @current = nil
15
+ @namespaces = {}
16
+ @namespace_definitions = []
17
+ @parser = Nokogiri::XML::SAX::PushParser.new self
18
+ @parser.options = Nokogiri::XML::ParseOptions::DEFAULT_XML | Nokogiri::XML::ParseOptions::NOENT
19
+ end
20
+
21
+ def receive_data(string)
22
+ Blather.log "PARSING: (#{string})" if @@debug
23
+ @parser << string
24
+ self
25
+ end
26
+ alias_method :<<, :receive_data
27
+
28
+ def start_element_namespace(elem, attrs, prefix, uri, namespaces)
29
+ Blather.log "START ELEM: (#{{:elem => elem, :attrs => attrs, :prefix => prefix, :uri => uri, :ns => namespaces}.inspect})" if @@debug
30
+
31
+ args = [elem]
32
+ args << @current.document if @current
33
+ node = XMPPNode.new *args
34
+ node.document.root = node unless @current
35
+
36
+ attrs.each do |attr|
37
+ node[attr.localname] = attr.value
38
+ end
39
+
40
+ ns_keys = namespaces.map { |pre, href| pre }
41
+ namespaces.delete_if { |pre, href| NS_TO_IGNORE.include? href }
42
+ @namespace_definitions.push []
43
+ namespaces.each do |pre, href|
44
+ next if @namespace_definitions.flatten.include?(@namespaces[[pre, href]])
45
+ ns = node.add_namespace(pre, href)
46
+ @namespaces[[pre, href]] ||= ns
47
+ end
48
+ @namespaces[[prefix, uri]] ||= node.add_namespace(prefix, uri) if prefix && !ns_keys.include?(prefix)
49
+ node.namespace = @namespaces[[prefix, uri]]
50
+
51
+ unless @receiver.stopped?
52
+ @current << node if @current
53
+ @current = node
54
+ end
55
+
56
+ deliver(node) if elem == 'stream'
57
+
58
+ # $stderr.puts "\n\n"
59
+ # $stderr.puts [elem, attrs, prefix, uri, namespaces].inspect
60
+ # $stderr.puts @namespaces.inspect
61
+ # $stderr.puts [@namespaces[[prefix, uri]].prefix, @namespaces[[prefix, uri]].href].inspect if @namespaces[[prefix, uri]]
62
+ # $stderr.puts node.inspect
63
+ # $stderr.puts node.document.to_s.gsub(/\n\s*/,'')
64
+ end
65
+
66
+ def end_element_namespace(elem, prefix, uri)
67
+ Blather.log "END ELEM: #{{:elem => elem, :prefix => prefix, :uri => uri}.inspect}" if @@debug
68
+
69
+ if elem == 'stream'
70
+ node = XMPPNode.new('end')
71
+ node.namespace = {prefix => uri}
72
+ deliver node
73
+ elsif @current.parent != @current.document
74
+ @namespace_definitions.pop
75
+ @current = @current.parent
76
+ else
77
+ deliver @current
78
+ end
79
+ end
80
+
81
+ def characters(chars = '')
82
+ Blather.log "CHARS: #{chars}" if @@debug
83
+ @current << Nokogiri::XML::Text.new(chars, @current.document) if @current
84
+ end
85
+
86
+ def warning(msg)
87
+ Blather.log "PARSE WARNING: #{msg}" if @@debug
88
+ end
89
+
90
+ def error(msg)
91
+ raise ParseError.new(msg)
92
+ end
93
+
94
+ private
95
+ def deliver(node)
96
+ @current, @namespaces, @namespace_definitions = nil, {}, []
97
+ @receiver.receive node
98
+ end
99
+ end #Parser
100
+
101
+ end #Stream
102
+ end #Blather
@@ -0,0 +1,3 @@
1
+ module Blather
2
+ VERSION = '0.8.2'
3
+ end
@@ -0,0 +1,94 @@
1
+ module Blather
2
+
3
+ # Base XML Node
4
+ # All XML classes subclass XMPPNode it allows the addition of helpers
5
+ class XMPPNode < Niceogiri::XML::Node
6
+ # @private
7
+ BASE_NAMES = %w[presence message iq].freeze
8
+
9
+ # @private
10
+ @@registrations = {}
11
+
12
+ class_attribute :registered_ns, :registered_name
13
+
14
+ # Register a new stanza class to a name and/or namespace
15
+ #
16
+ # This registers a namespace that is used when looking
17
+ # up the class name of the object to instantiate when a new
18
+ # stanza is received
19
+ #
20
+ # @param [#to_s] name the name of the node
21
+ # @param [String, nil] ns the namespace the node belongs to
22
+ def self.register(name, ns = nil)
23
+ self.registered_name = name.to_s
24
+ self.registered_ns = ns
25
+ @@registrations[[self.registered_name, self.registered_ns]] = self
26
+ end
27
+
28
+ # Find the class to use given the name and namespace of a stanza
29
+ #
30
+ # @param [#to_s] name the name to lookup
31
+ # @param [String, nil] xmlns the namespace the node belongs to
32
+ # @return [Class, nil] the class appropriate for the name/ns combination
33
+ def self.class_from_registration(name, ns = nil)
34
+ @@registrations[[name.to_s, ns]]
35
+ end
36
+
37
+ # Import an XML::Node to the appropriate class
38
+ #
39
+ # Looks up the class the node should be then creates it based on the
40
+ # elements of the XML::Node
41
+ # @param [XML::Node] node the node to import
42
+ # @return the appropriate object based on the node name and namespace
43
+ def self.import(node, *decorators)
44
+ ns = (node.namespace.href if node.namespace)
45
+ klass = class_from_registration(node.element_name, ns)
46
+ if klass && klass != self
47
+ klass.import(node, *decorators)
48
+ else
49
+ new(node.element_name).decorate(*decorators).inherit(node)
50
+ end
51
+ end
52
+
53
+ # Parse a string as XML and import to the appropriate class
54
+ #
55
+ # @param [String] string the string to parse
56
+ # @return the appropriate object based on the node name and namespace
57
+ def self.parse(string)
58
+ import Nokogiri::XML(string).root
59
+ end
60
+
61
+ # Create a new Node object
62
+ #
63
+ # @param [String, nil] name the element name
64
+ # @param [XML::Document, nil] doc the document to attach the node to. If
65
+ # not provided one will be created
66
+ # @return a new object with the registered name and namespace
67
+ def self.new(name = registered_name, doc = nil)
68
+ super name, doc, BASE_NAMES.include?(name.to_s) ? nil : self.registered_ns
69
+ end
70
+
71
+ def self.decorator_modules
72
+ [self::InstanceMethods]
73
+ end
74
+
75
+ def decorate(*decorators)
76
+ decorators.each do |decorator|
77
+ decorator.decorator_modules.each do |mod|
78
+ extend mod
79
+ end
80
+
81
+ @handler_hierarchy.unshift decorator.handler_hierarchy.first
82
+ end
83
+ self
84
+ end
85
+
86
+ # Turn the object into a proper stanza
87
+ #
88
+ # @return a stanza object
89
+ def to_stanza
90
+ self.class.import self
91
+ end
92
+ end # XMPPNode
93
+
94
+ end # Blather