blather 0.2.1 → 0.2.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 (84) hide show
  1. data/LICENSE +2 -0
  2. data/README.rdoc +54 -29
  3. data/Rakefile +94 -13
  4. data/VERSION.yml +4 -0
  5. data/examples/drb_client.rb +2 -4
  6. data/examples/echo.rb +13 -8
  7. data/examples/pubsub/cli.rb +64 -0
  8. data/examples/pubsub/ping_pong.rb +18 -0
  9. data/examples/pubsub/pubsub_dsl.rb +52 -0
  10. data/examples/pubsub_client.rb +39 -0
  11. data/examples/rosterprint.rb +14 -0
  12. data/examples/xmpp4r/echo.rb +35 -0
  13. data/ext/extconf.rb +65 -0
  14. data/lib/blather.rb +18 -121
  15. data/lib/blather/client.rb +13 -0
  16. data/lib/blather/client/client.rb +165 -0
  17. data/lib/blather/client/dsl.rb +99 -0
  18. data/lib/blather/client/pubsub.rb +53 -0
  19. data/lib/blather/client/pubsub/node.rb +27 -0
  20. data/lib/blather/core_ext/active_support.rb +1 -0
  21. data/lib/blather/core_ext/libxml.rb +7 -1
  22. data/lib/blather/errors.rb +39 -18
  23. data/lib/blather/errors/sasl_error.rb +87 -0
  24. data/lib/blather/errors/stanza_error.rb +262 -0
  25. data/lib/blather/errors/stream_error.rb +253 -0
  26. data/lib/blather/jid.rb +9 -16
  27. data/lib/blather/roster.rb +9 -0
  28. data/lib/blather/roster_item.rb +7 -4
  29. data/lib/blather/stanza.rb +19 -25
  30. data/lib/blather/stanza/disco.rb +9 -0
  31. data/lib/blather/stanza/disco/disco_info.rb +84 -0
  32. data/lib/blather/stanza/disco/disco_items.rb +59 -0
  33. data/lib/blather/stanza/iq.rb +16 -4
  34. data/lib/blather/stanza/iq/query.rb +6 -4
  35. data/lib/blather/stanza/iq/roster.rb +38 -38
  36. data/lib/blather/stanza/pubsub.rb +33 -0
  37. data/lib/blather/stanza/pubsub/affiliations.rb +52 -0
  38. data/lib/blather/stanza/pubsub/errors.rb +9 -0
  39. data/lib/blather/stanza/pubsub/event.rb +21 -0
  40. data/lib/blather/stanza/pubsub/items.rb +59 -0
  41. data/lib/blather/stanza/pubsub/owner.rb +9 -0
  42. data/lib/blather/stanza/pubsub/subscriptions.rb +57 -0
  43. data/lib/blather/stream.rb +125 -57
  44. data/lib/blather/stream/client.rb +26 -0
  45. data/lib/blather/stream/component.rb +34 -0
  46. data/lib/blather/stream/parser.rb +17 -27
  47. data/lib/blather/stream/resource.rb +21 -24
  48. data/lib/blather/stream/sasl.rb +60 -37
  49. data/lib/blather/stream/session.rb +12 -19
  50. data/lib/blather/stream/stream_handler.rb +39 -0
  51. data/lib/blather/stream/tls.rb +22 -18
  52. data/lib/blather/xmpp_node.rb +91 -17
  53. data/spec/blather/core_ext/libxml_spec.rb +58 -0
  54. data/spec/blather/errors/sasl_error_spec.rb +56 -0
  55. data/spec/blather/errors/stanza_error_spec.rb +148 -0
  56. data/spec/blather/errors/stream_error_spec.rb +114 -0
  57. data/spec/blather/errors_spec.rb +40 -0
  58. data/spec/blather/jid_spec.rb +0 -7
  59. data/spec/blather/roster_item_spec.rb +5 -0
  60. data/spec/blather/roster_spec.rb +6 -6
  61. data/spec/blather/stanza/discos/disco_info_spec.rb +207 -0
  62. data/spec/blather/stanza/discos/disco_items_spec.rb +136 -0
  63. data/spec/blather/stanza/iq/query_spec.rb +9 -2
  64. data/spec/blather/stanza/iq/roster_spec.rb +117 -1
  65. data/spec/blather/stanza/iq_spec.rb +29 -0
  66. data/spec/blather/stanza/presence/subscription_spec.rb +12 -1
  67. data/spec/blather/stanza/presence_spec.rb +29 -0
  68. data/spec/blather/stanza/pubsub/affiliations_spec.rb +46 -0
  69. data/spec/blather/stanza/pubsub/items_spec.rb +59 -0
  70. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +63 -0
  71. data/spec/blather/stanza/pubsub_spec.rb +26 -0
  72. data/spec/blather/stanza_spec.rb +13 -1
  73. data/spec/blather/stream/client_spec.rb +787 -0
  74. data/spec/blather/stream/component_spec.rb +86 -0
  75. data/spec/blather/xmpp_node_spec.rb +75 -22
  76. data/spec/fixtures/pubsub.rb +157 -0
  77. data/spec/spec_helper.rb +6 -14
  78. metadata +86 -74
  79. data/CHANGELOG +0 -5
  80. data/Manifest +0 -47
  81. data/blather.gemspec +0 -41
  82. data/lib/blather/stanza/error.rb +0 -31
  83. data/spec/blather/stream_spec.rb +0 -462
  84. data/spec/build_safe.rb +0 -20
@@ -0,0 +1,253 @@
1
+ module Blather
2
+
3
+ ##
4
+ # Stream Errors
5
+ # RFC3920 Section 9.3 (http://xmpp.org/rfcs/rfc3920.html#streams-error-rules)
6
+ class StreamError < BlatherError
7
+ class_inheritable_accessor :err_name
8
+ @@registrations = {}
9
+
10
+ register :stream_error
11
+
12
+ attr_reader :text, :extras
13
+
14
+ ##
15
+ # Register the handler and type to simplify importing
16
+ def self.register(handler, err_name)
17
+ super handler
18
+ self.err_name = err_name
19
+ @@registrations[err_name] = self
20
+ end
21
+
22
+ ##
23
+ # Retreive an error class from a given name
24
+ def self.class_from_registration(err_name)
25
+ @@registrations[err_name.to_s] || self
26
+ end
27
+
28
+ ##
29
+ # Factory method for instantiating the proper class
30
+ # for the error
31
+ def self.import(node)
32
+ name = node.find_first('descendant::*[name()!="text"]', 'urn:ietf:params:xml:ns:xmpp-streams').element_name
33
+ text = node.find_first 'descendant::text', 'urn:ietf:params:xml:ns:xmpp-streams'
34
+ text = text.content if text
35
+
36
+ extras = node.find("descendant::*[name()!='text' and name()!='#{name}']").map { |n| n }
37
+
38
+ class_from_registration(name).new text, extras
39
+ end
40
+
41
+ ##
42
+ # <tt>text</tt> is the (optional) error message.
43
+ # <tt>extras</tt> should be an array of nodes to attach to the error
44
+ # each extra should be in an application specific namespace
45
+ # see RFC3920 Section 4.7.2 (http://xmpp.org/rfcs/rfc3920.html#rfc.section.4.7.2)
46
+ def initialize(text = nil, extras = [])
47
+ @text = text
48
+ @extras = extras
49
+ end
50
+
51
+ ##
52
+ # XMPP defined error name
53
+ def err_name
54
+ self.class.err_name
55
+ end
56
+
57
+ ##
58
+ # Creates an XML node from the error
59
+ def to_node
60
+ node = XMPPNode.new('stream:error')
61
+
62
+ err = XMPPNode.new(self.err_name)
63
+ err.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
64
+ node << err
65
+
66
+ if self.text
67
+ text = XMPPNode.new('text')
68
+ text.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
69
+ text << self.text
70
+ node << text
71
+ end
72
+
73
+ self.extras.each do |extra|
74
+ extra_copy = extra.copy
75
+ extra_copy.namespace = extra.namespace
76
+ node << extra_copy
77
+ end
78
+ node
79
+ end
80
+
81
+ ##
82
+ # Turns the object into XML fit to be sent over the stream
83
+ def to_xml
84
+ to_node.to_s
85
+ end
86
+
87
+ def inspect # :nodoc:
88
+ "Stream Error (#{self.err_name}): #{self.text}"
89
+ end
90
+ alias_method :to_s, :inspect # :nodoc:
91
+
92
+ ##
93
+ # The entity has sent XML that cannot be processed; this error MAY be used instead of the more specific XML-related errors,
94
+ # such as <bad-namespace-prefix/>, <invalid-xml/>, <restricted-xml/>, <unsupported-encoding/>, and <xml-not-well-formed/>,
95
+ # although the more specific errors are preferred.
96
+ class BadFormat < StreamError
97
+ register :stream_bad_format_error, 'bad-format'
98
+ end
99
+
100
+ ##
101
+ # The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires
102
+ # such a prefix (see XML Namespace Names and Prefixes).
103
+ class BadNamespacePrefix < StreamError
104
+ register :stream_bad_namespace_prefix_error, 'bad-namespace-prefix'
105
+ end
106
+
107
+ ##
108
+ # The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the
109
+ # existing stream.
110
+ class Conflict < StreamError
111
+ register :stream_conflict_error, 'conflict'
112
+ end
113
+
114
+ ##
115
+ # The entity has not generated any traffic over the stream for some period of time (configurable according to a local service policy).
116
+ class ConnectionTimeout < StreamError
117
+ register :stream_connection_timeout_error, 'connection-timeout'
118
+ end
119
+
120
+ ##
121
+ # The value of the 'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no
122
+ # longer hosted by the server.
123
+ class HostGone < StreamError
124
+ register :stream_host_gone_error, 'host-gone'
125
+ end
126
+
127
+ ##
128
+ # The value of the 'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that
129
+ # is hosted by the server.
130
+ class HostUnknown < StreamError
131
+ register :stream_host_unknown_error, 'host-unknown'
132
+ end
133
+
134
+ ##
135
+ # a stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value).
136
+ class ImproperAddressing < StreamError
137
+ register :stream_improper_addressing_error, 'improper-addressing'
138
+ end
139
+
140
+ ##
141
+ # The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.
142
+ class InternalServerError < StreamError
143
+ register :stream_internal_server_error, 'internal-server-error'
144
+ end
145
+
146
+ ##
147
+ # The JID or hostname provided in a 'from' address does not match an authorized JID or validated domain negotiated between
148
+ # servers via SASL or dialback, or between a client and a server via authentication and resource binding.
149
+ class InvalidFrom < StreamError
150
+ register :stream_invalid_from_error, 'invalid-from'
151
+ end
152
+
153
+ ##
154
+ # The stream ID or dialback ID is invalid or does not match an ID previously provided.
155
+ class InvalidId < StreamError
156
+ register :stream_invalid_id_error, 'invalid-id'
157
+ end
158
+
159
+ ##
160
+ # The streams namespace name is something other than "http://etherx.jabber.org/streams" or the dialback namespace name is something
161
+ # other than "jabber:server:dialback" (see XML Namespace Names and Prefixes).
162
+ class InvalidNamespace < StreamError
163
+ register :stream_invalid_namespace_error, 'invalid-namespace'
164
+ end
165
+
166
+ ##
167
+ # The entity has sent invalid XML over the stream to a server that performs validation (see Validation).
168
+ class InvalidXml < StreamError
169
+ register :stream_invalid_xml_error, 'invalid-xml'
170
+ end
171
+
172
+ ##
173
+ # The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action
174
+ # related to stream negotiation; the receiving entity MUST NOT process the offending stanza before sending the stream error.
175
+ class NotAuthorized < StreamError
176
+ register :stream_not_authorized_error, 'not-authorized'
177
+ end
178
+
179
+ ##
180
+ # The entity has violated some local service policy; the server MAY choose to specify the policy in the <text/> element or an
181
+ # application-specific condition element.
182
+ class PolicyViolation < StreamError
183
+ register :stream_policy_violation_error, 'policy-violation'
184
+ end
185
+
186
+ ##
187
+ # The server is unable to properly connect to a remote entity that is required for authentication or authorization.
188
+ class RemoteConnectionFailed < StreamError
189
+ register :stream_remote_connection_failed_error, 'remote-connection-failed'
190
+ end
191
+
192
+ ##
193
+ # The server lacks the system resources necessary to service the stream.
194
+ class ResourceConstraint < StreamError
195
+ register :stream_resource_constraint_error, 'resource-constraint'
196
+ end
197
+
198
+ ##
199
+ # The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference,
200
+ # or unescaped character (see Restrictions).
201
+ class RestrictedXml < StreamError
202
+ register :stream_restricted_xml_error, 'restricted-xml'
203
+ end
204
+
205
+ ##
206
+ # The server will not provide service to the initiating entity but is redirecting traffic to another host; the server SHOULD
207
+ # specify the alternate hostname or IP address (which MUST be a valid domain identifier) as the XML character data of the
208
+ # <see-other-host/> element.
209
+ class SeeOtherHost < StreamError
210
+ register :stream_see_other_host_error, 'see-other-host'
211
+ end
212
+
213
+ ##
214
+ # The server is being shut down and all active streams are being closed.
215
+ class SystemShutdown < StreamError
216
+ register :stream_system_shutdown_error, 'system-shutdown'
217
+ end
218
+
219
+ ##
220
+ # The error condition is not one of those defined by the other conditions in this list; this error condition SHOULD be used
221
+ # only in conjunction with an application-specific condition.
222
+ class UndefinedCondition < StreamError
223
+ register :stream_undefined_condition_error, 'undefined-condition'
224
+ end
225
+
226
+ ##
227
+ # The initiating entity has encoded the stream in an encoding that is not supported by the server (see Character Encoding).
228
+ class UnsupportedEncoding < StreamError
229
+ register :stream_unsupported_encoding_error, 'unsupported-encoding'
230
+ end
231
+
232
+ ##
233
+ # The initiating entity has sent a first-level child of the stream that is not supported by the server.
234
+ class UnsupportedStanzaType < StreamError
235
+ register :stream_unsupported_stanza_type_error, 'unsupported-stanza-type'
236
+ end
237
+
238
+ ##
239
+ # The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP
240
+ # That is not supported by the server; the server MAY specify the version(s) it supports in the <text/> element.
241
+ class UnsupportedVersion < StreamError
242
+ register :stream_unsupported_version_error, 'unsupported-version'
243
+ end
244
+
245
+ ##
246
+ # The initiating entity has sent XML that is not well-formed as defined by [XML].
247
+ class XmlNotWellFormed < StreamError
248
+ register :stream_xml_not_well_formed_error, 'xml-not-well-formed'
249
+ end
250
+
251
+ end #StreamError
252
+
253
+ end #Blather
@@ -5,24 +5,23 @@ module Blather
5
5
  class JID
6
6
  include Comparable
7
7
 
8
- PATTERN = /^(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?$/
9
-
10
- begin
11
- require 'idn'
12
- USE_STRINGPREP = true
13
- rescue LoadError
14
- USE_STRINGPREP = false
15
- end
8
+ PATTERN = /^(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?$/.freeze
16
9
 
10
+ ##
17
11
  # Get the JID's node
18
12
  attr_reader :node
19
13
 
14
+ ##
20
15
  # Get the JID's domain
21
16
  attr_reader :domain
22
17
 
18
+ ##
23
19
  # Get the JID's resource
24
20
  attr_reader :resource
25
21
 
22
+ ##
23
+ # If a JID is passed in just return it.
24
+ # No need to copy out all the values
26
25
  def self.new(node, domain = nil, resource = nil)
27
26
  node.is_a?(JID) ? node : super
28
27
  end
@@ -39,14 +38,8 @@ module Blather
39
38
  @node, @domain, @resource = @node.to_s.scan(PATTERN).first
40
39
  end
41
40
 
42
- if USE_STRINGPREP
43
- @node = IDN::Stringprep.nodeprep(@node) if @node
44
- @domain = IDN::Stringprep.nameprep(@domain) if @domain
45
- @resource = IDN::Stringprep.resourceprep(@resource) if @resource
46
- else
47
- @node.downcase! if @node
48
- @domain.downcase! if @domain
49
- end
41
+ @node.downcase! if @node
42
+ @domain.downcase! if @domain
50
43
 
51
44
  raise ArgumentError, 'Node too long' if (@node || '').length > 1023
52
45
  raise ArgumentError, 'Domain too long' if (@domain || '').length > 1023
@@ -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
@@ -17,6 +17,7 @@ module Blather
17
17
  # item:: can be a JID, String (a@b) or a Stanza
18
18
  def initialize(item)
19
19
  @statuses = []
20
+ @groups = []
20
21
 
21
22
  case item
22
23
  when JID
@@ -24,12 +25,14 @@ module Blather
24
25
  when String
25
26
  self.jid = JID.new(item).stripped
26
27
  when XMPPNode
27
- self.jid = JID.new(item['jid']).stripped
28
- self.name = item['name']
29
- self.subscription = item['subscription']
30
- self.ask = item['ask']
28
+ self.jid = JID.new(item[:jid]).stripped
29
+ self.name = item[:name]
30
+ self.subscription = item[:subscription]
31
+ self.ask = item[:ask]
31
32
  item.groups.each { |g| self.groups << g }
32
33
  end
34
+
35
+ @groups = [nil] if @groups.empty?
33
36
  end
34
37
 
35
38
  ##
@@ -3,6 +3,7 @@ module Blather
3
3
  # Base XMPP Stanza
4
4
  class Stanza < XMPPNode
5
5
  @@last_id = 0
6
+ @@handler_list = []
6
7
 
7
8
  class_inheritable_array :handler_heirarchy
8
9
 
@@ -13,12 +14,17 @@ module Blather
13
14
  # that inherits Stanza can register a callback for itself
14
15
  # which is added to a list and iterated over when looking for
15
16
  # a callback to use
16
- def self.register(type, name = nil, xmlns = nil)
17
+ def self.register(type, name = nil, ns = nil)
18
+ @@handler_list << type
17
19
  self.handler_heirarchy ||= []
18
20
  self.handler_heirarchy.unshift type
19
21
 
20
22
  name = name || self.name || type
21
- super name, xmlns
23
+ super name, ns
24
+ end
25
+
26
+ def self.handler_list
27
+ @@handler_list
22
28
  end
23
29
 
24
30
  ##
@@ -65,43 +71,31 @@ module Blather
65
71
  self
66
72
  end
67
73
 
68
- def id=(id)
69
- attributes['id'] = id
70
- end
71
-
72
- def id
73
- attributes['id']
74
- end
74
+ attribute_accessor :id, :to_sym => false
75
75
 
76
- def to=(to)
77
- attributes['to'] = to
78
- end
76
+ attribute_writer :to, :from
79
77
 
80
78
  ##
81
79
  # returns:: JID created from the "to" value of the stanza
82
80
  def to
83
- JID.new(attributes['to']) if attributes['to']
84
- end
85
-
86
- def from=(from)
87
- attributes['from'] = from
81
+ JID.new(attributes[:to]) if attributes[:to]
88
82
  end
89
83
 
90
84
  ##
91
85
  # returns:: JID created from the "from" value of the stanza
92
86
  def from
93
- JID.new(attributes['from']) if attributes['from']
87
+ JID.new(attributes[:from]) if attributes[:from]
94
88
  end
95
89
 
96
- def type=(type)
97
- attributes['type'] = type
98
- end
90
+ attribute_accessor :type
99
91
 
100
92
  ##
101
- # returns:: a symbol of the type
102
- def type
103
- attributes['type'].to_sym unless attributes['type'].blank?
93
+ # Transform the stanza into a stanza error
94
+ # <tt>err_name_or_class</tt> can be the name of the error or the error class to use
95
+ # <tt>type</tt>, <tt>text</tt>, <tt>extras</tt> are the same as for StanzaError#new
96
+ def as_error(err_name_or_class, type, text = nil, extras = [])
97
+ klass = (err_name_or_class.is_a?(Class) ? err_name_or_class : StanzaError.class_from_registration(err_name_or_class))
98
+ klass.new self, type, text, extras
104
99
  end
105
-
106
100
  end
107
101
  end
@@ -0,0 +1,9 @@
1
+ module Blather
2
+ class Stanza
3
+
4
+ class Disco < Iq::Query
5
+ attribute_accessor :node, :to_sym => false
6
+ end
7
+
8
+ end #Stanza
9
+ end #Blather
@@ -0,0 +1,84 @@
1
+ module Blather
2
+ class Stanza
3
+
4
+ ##
5
+ # DiscoInfo object ()
6
+ #
7
+ class DiscoInfo < Disco
8
+ register :disco_info, nil, 'http://jabber.org/protocol/disco#info'
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
22
+ end
23
+
24
+ ##
25
+ # List of identity objects
26
+ 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 }
30
+ end
31
+
32
+ ##
33
+ # List of feature objects
34
+ 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 }
38
+ end
39
+
40
+ class Identity < XMPPNode
41
+ attribute_accessor :category, :type
42
+ attribute_accessor :name, :to_sym => false
43
+
44
+ def initialize(name, type = nil, category = nil)
45
+ super :identity
46
+
47
+ if name.is_a?(XML::Node)
48
+ self.inherit name
49
+ else
50
+ self.name = name
51
+ self.type = type
52
+ self.category = category
53
+ end
54
+ end
55
+
56
+ def eql?(other)
57
+ other.kind_of?(self.class) &&
58
+ other.name == self.name &&
59
+ other.type == self.type &&
60
+ other.category == self.category
61
+ end
62
+ end
63
+
64
+ class Feature < XMPPNode
65
+ attribute_accessor :var, :to_sym => false
66
+
67
+ def initialize(var)
68
+ super :feature
69
+ if var.is_a?(XML::Node)
70
+ self.inherit var
71
+ else
72
+ self.var = var
73
+ end
74
+ end
75
+
76
+ def eql?(other)
77
+ other.kind_of?(self.class) &&
78
+ other.var == self.var
79
+ end
80
+ end
81
+ end
82
+
83
+ end #Stanza
84
+ end #Blather