blather 0.1 → 0.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 (50) hide show
  1. data/CHANGELOG +2 -0
  2. data/Manifest +36 -33
  3. data/README.rdoc +11 -17
  4. data/Rakefile +4 -3
  5. data/blather.gemspec +9 -9
  6. data/examples/drb_client.rb +7 -0
  7. data/examples/echo.rb +9 -18
  8. data/lib/blather.rb +140 -31
  9. data/lib/blather/{core/errors.rb → errors.rb} +0 -0
  10. data/lib/blather/{core/jid.rb → jid.rb} +0 -0
  11. data/lib/blather/{core/roster.rb → roster.rb} +0 -0
  12. data/lib/blather/{core/roster_item.rb → roster_item.rb} +0 -0
  13. data/lib/blather/{core/stanza.rb → stanza.rb} +6 -14
  14. data/lib/blather/stanza/error.rb +31 -0
  15. data/lib/blather/{core/stanza → stanza}/iq.rb +2 -2
  16. data/lib/blather/stanza/iq/query.rb +50 -0
  17. data/lib/blather/{core/stanza → stanza}/iq/roster.rb +3 -3
  18. data/lib/blather/{core/stanza → stanza}/message.rb +6 -2
  19. data/lib/blather/{core/stanza → stanza}/presence.rb +10 -0
  20. data/lib/blather/{core/stanza → stanza}/presence/status.rb +8 -6
  21. data/lib/blather/{core/stanza → stanza}/presence/subscription.rb +1 -1
  22. data/lib/blather/{core/stream.rb → stream.rb} +1 -1
  23. data/lib/blather/{core/stream → stream}/parser.rb +0 -5
  24. data/lib/blather/{core/stream → stream}/resource.rb +0 -0
  25. data/lib/blather/{core/stream → stream}/sasl.rb +16 -5
  26. data/lib/blather/{core/stream → stream}/session.rb +0 -0
  27. data/lib/blather/{core/stream → stream}/tls.rb +0 -0
  28. data/lib/blather/{core/sugar.rb → sugar.rb} +4 -0
  29. data/lib/blather/{core/xmpp_node.rb → xmpp_node.rb} +1 -3
  30. data/spec/blather/{core/jid_spec.rb → jid_spec.rb} +16 -1
  31. data/spec/blather/{core/roster_item_spec.rb → roster_item_spec.rb} +1 -1
  32. data/spec/blather/{core/roster_spec.rb → roster_spec.rb} +11 -1
  33. data/spec/blather/stanza/iq/query_spec.rb +34 -0
  34. data/spec/blather/stanza/iq/roster_spec.rb +7 -0
  35. data/spec/blather/stanza/iq_spec.rb +11 -0
  36. data/spec/blather/stanza/message_spec.rb +52 -0
  37. data/spec/blather/stanza/presence/status_spec.rb +102 -0
  38. data/spec/blather/stanza/presence/subscription_spec.rb +74 -0
  39. data/spec/blather/stanza/presence_spec.rb +24 -0
  40. data/spec/blather/{core/stanza_spec.rb → stanza_spec.rb} +1 -1
  41. data/spec/blather/{core/stream_spec.rb → stream_spec.rb} +208 -9
  42. data/spec/blather/{core/xmpp_node_spec.rb → xmpp_node_spec.rb} +1 -1
  43. metadata +75 -69
  44. data/examples/shell_client.rb +0 -28
  45. data/lib/blather/callback.rb +0 -24
  46. data/lib/blather/client.rb +0 -81
  47. data/lib/blather/core/stanza/iq/query.rb +0 -42
  48. data/lib/blather/extensions.rb +0 -4
  49. data/lib/blather/extensions/last_activity.rb +0 -55
  50. data/lib/blather/extensions/version.rb +0 -85
@@ -0,0 +1,31 @@
1
+ module Blather
2
+ class Stanza
3
+
4
+ ##
5
+ # Base Error stanza
6
+ class Error < Stanza
7
+ def self.new_from(stanza, defined_condition, type, text = nil)
8
+ err = XMPPNode.new(defined_condition)
9
+ err['type'] = type
10
+ err.xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
11
+
12
+ if text
13
+ text = XMPPNode.new('text', text)
14
+ text.xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
15
+ err << text
16
+ end
17
+
18
+ elem = stanza.copy(true)
19
+ elem.type = 'error'
20
+ elem << err
21
+
22
+ elem
23
+ end
24
+
25
+ def error?
26
+ true
27
+ end
28
+ end #ErrorStanza
29
+
30
+ end #Stanza
31
+ end
@@ -13,10 +13,10 @@ class Stanza
13
13
  (klass || self).new(node['type']).inherit(node)
14
14
  end
15
15
 
16
- def self.new(type, to = nil, id = nil)
16
+ def self.new(type = nil, to = nil, id = nil)
17
17
  elem = super :iq
18
18
  elem.xmlns = nil
19
- elem.type = type
19
+ elem.type = type || :get
20
20
  elem.to = to
21
21
  elem.id = id if id
22
22
  elem
@@ -0,0 +1,50 @@
1
+ module Blather
2
+ class Stanza
3
+ class Iq
4
+
5
+ class Query < Iq
6
+ register :query, :query
7
+
8
+ ##
9
+ # Ensure the namespace is set to the query node
10
+ def self.new(type = nil)
11
+ elem = super
12
+ elem.query.xmlns = self.xmlns
13
+ elem
14
+ end
15
+
16
+ ##
17
+ # Kill the query node before running inherit
18
+ def inherit(node)
19
+ query.remove!
20
+ @query = nil
21
+ super
22
+ end
23
+
24
+ ##
25
+ # Query node accessor
26
+ # This will ensure there actually is a query node
27
+ def query
28
+ (self << (q = XMPPNode.new('query'))) unless q = find_first('query')
29
+ q
30
+ end
31
+
32
+ ##
33
+ # A query reply should have type set to "result"
34
+ def reply
35
+ elem = super
36
+ elem.type = :result
37
+ elem
38
+ end
39
+
40
+ ##
41
+ # A query reply should have type set to "result"
42
+ def reply!
43
+ self.type = :result
44
+ super
45
+ end
46
+ end #Query
47
+
48
+ end #Iq
49
+ end #Stanza
50
+ end
@@ -5,9 +5,9 @@ class Iq
5
5
  class Roster < Query
6
6
  register :roster, nil, 'jabber:iq:roster'
7
7
 
8
- def self.new(type, item = nil)
8
+ def self.new(type = nil, item = nil)
9
9
  elem = super(type)
10
- elem.query << item
10
+ elem.query << item if item
11
11
  elem
12
12
  end
13
13
 
@@ -21,7 +21,7 @@ class Iq
21
21
  end
22
22
 
23
23
  def items
24
- @items ||= query.find('item')#.map { |g| RosterItem.new g }
24
+ query.find('item')#.map { |g| RosterItem.new g }
25
25
  end
26
26
 
27
27
  class RosterItem < XMPPNode
@@ -8,7 +8,7 @@ class Stanza
8
8
 
9
9
  register :message
10
10
 
11
- def self.new(to = nil, type = nil, body = nil)
11
+ def self.new(to = nil, body = nil, type = :chat)
12
12
  elem = super()
13
13
  elem.to = to
14
14
  elem.type = type
@@ -16,6 +16,10 @@ class Stanza
16
16
  elem
17
17
  end
18
18
 
19
+ VALID_TYPES.each do |valid_type|
20
+ define_method("#{valid_type}?") { self.type == valid_type }
21
+ end
22
+
19
23
  ##
20
24
  # Ensures type is :chat, :error, :groupchat, :headline or :normal
21
25
  def type=(type)
@@ -43,7 +47,7 @@ class Stanza
43
47
 
44
48
  def thread=(thread)
45
49
  remove_child :thread
46
- self << XMPPNode.new('body', body) if body
50
+ self << XMPPNode.new('thread', thread) if thread
47
51
  end
48
52
 
49
53
  def thread
@@ -8,6 +8,12 @@ class Stanza
8
8
 
9
9
  register :presence
10
10
 
11
+ ##
12
+ # Ensure element_name is "presence" for all subclasses
13
+ def self.new
14
+ super :presence
15
+ end
16
+
11
17
  ##
12
18
  # Creates a class based on the presence type
13
19
  # either a Status or Subscription object is created based
@@ -22,6 +28,10 @@ class Stanza
22
28
  klass.new.inherit(node)
23
29
  end
24
30
 
31
+ VALID_TYPES.each do |valid_type|
32
+ define_method("#{valid_type}?") { self.type == valid_type }
33
+ end
34
+
25
35
  ##
26
36
  # Ensures type is one of :unavailable, :subscribe, :subscribed, :unsubscribe, :unsubscribed, :probe or :error
27
37
  def type=(type)
@@ -7,7 +7,7 @@ class Presence
7
7
 
8
8
  include Comparable
9
9
 
10
- register :status
10
+ register :status, :status
11
11
 
12
12
  def self.new(state = nil, message = nil)
13
13
  elem = super()
@@ -42,15 +42,15 @@ class Presence
42
42
 
43
43
  ##
44
44
  # Ensure priority is between -128 and 127
45
- def priority=(priority)
46
- raise ArgumentError, 'Priority must be between -128 and +127' if priority && !(-128..127).include?(priority.to_i)
45
+ def priority=(new_priority)
46
+ raise ArgumentError, 'Priority must be between -128 and +127' if new_priority && !(-128..127).include?(new_priority.to_i)
47
47
 
48
48
  remove_child :priority
49
- self << XMPPNode.new('priority', priority) if priority
49
+ self << XMPPNode.new('priority', new_priority) if new_priority
50
50
  end
51
51
 
52
52
  def priority
53
- @priority ||= content_from(:priority).to_i
53
+ content_from(:priority).to_i
54
54
  end
55
55
 
56
56
  def message=(msg)
@@ -66,7 +66,9 @@ class Presence
66
66
  # Compare status based on priority
67
67
  # raises an error if the JIDs aren't the same
68
68
  def <=>(o)
69
- raise "Cannot compare status from different JIDs: #{[self.from, o.from].inspect}" unless self.from.stripped == o.from.stripped
69
+ unless self.from && o.from && self.from.stripped == o.from.stripped
70
+ raise ArgumentError, "Cannot compare status from different JIDs: #{[self.from, o.from].inspect}"
71
+ end
70
72
  self.priority <=> o.priority
71
73
  end
72
74
 
@@ -3,7 +3,7 @@ class Stanza
3
3
  class Presence
4
4
 
5
5
  class Subscription < Presence
6
- register :subscription
6
+ register :subscription, :subscription
7
7
 
8
8
  def self.new(to = nil, type = nil)
9
9
  elem = super()
@@ -42,7 +42,7 @@ module Blather
42
42
 
43
43
  def unbind # :nodoc:
44
44
  # @keepalive.cancel
45
- @state == :stopped
45
+ @state = :stopped
46
46
  end
47
47
 
48
48
  def receive(node) # :nodoc:
@@ -50,11 +50,6 @@ module Stream # :nodoc:
50
50
  @current << XML::Node.new_text(chars) if @current
51
51
  end
52
52
 
53
- def on_cdata_block(block)
54
- LOG.debug "CDATA: #{block}" if @@debug
55
- @current << XML::Node.new_cdata(block) if @current
56
- end
57
-
58
53
  def on_end_element(elem)
59
54
  return if elem =~ STREAM_REGEX
60
55
 
@@ -2,6 +2,8 @@ module Blather # :nodoc:
2
2
  module Stream # :nodoc:
3
3
 
4
4
  class SASL # :nodoc:
5
+ class UnknownMechanism < BlatherError; end
6
+
5
7
  SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'
6
8
 
7
9
  def initialize(stream, jid, pass = nil)
@@ -9,20 +11,24 @@ module Stream # :nodoc:
9
11
  @jid = jid
10
12
  @pass = pass
11
13
  @callbacks = {}
14
+ @mechanism = 0
15
+ @mechanisms = []
12
16
 
13
17
  init_callbacks
14
18
  end
15
19
 
16
20
  def init_callbacks
17
- @callbacks['mechanisms'] = proc { set_mechanism; authenticate }
21
+ @callbacks['mechanisms'] = proc { @mechanisms = @node.children; set_mechanism; authenticate }
18
22
  end
19
23
 
20
24
  def set_mechanism
21
- mod = case (mechanism = @node.first.content)
25
+ mod = case (mechanism = @mechanisms[@mechanism].content)
22
26
  when 'DIGEST-MD5' then DigestMD5
23
27
  when 'PLAIN' then Plain
24
28
  when 'ANONYMOUS' then Anonymous
25
- else raise "Unknown SASL mechanism (#{mechanism})"
29
+ else
30
+ @stream.send "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><invalid-mechanism/></failure>"
31
+ raise UnknownMechanism, "Unknown SASL mechanism (#{mechanism})"
26
32
  end
27
33
 
28
34
  extend mod
@@ -30,7 +36,12 @@ module Stream # :nodoc:
30
36
 
31
37
  def receive(node)
32
38
  @node = node
33
- @callbacks[@node.element_name].call if @callbacks[@node.element_name]
39
+ if @node.element_name == 'failure' && @mechanisms[@mechanism += 1]
40
+ set_mechanism
41
+ authenticate
42
+ else
43
+ @callbacks[@node.element_name].call if @callbacks[@node.element_name]
44
+ end
34
45
  end
35
46
 
36
47
  def success(&callback)
@@ -106,7 +117,7 @@ module Stream # :nodoc:
106
117
  LOG.debug "CH RESP TXT: #{@response.map { |k,v| "#{k}=#{v}" } * ','}"
107
118
 
108
119
  # order is to simplify testing
109
- order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri']
120
+ order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri', :response]
110
121
  node.content = b64(order.map { |k| v = @response[k]; "#{k}=#{v}" } * ',')
111
122
  end
112
123
 
@@ -1,6 +1,10 @@
1
1
  module LibXML # :nodoc:
2
2
  module XML # :nodoc:
3
3
 
4
+ class Node
5
+ alias_method :element_name, :name
6
+ end
7
+
4
8
  class Attributes
5
9
  # Helper method for removing attributes
6
10
  def remove(name)
@@ -7,8 +7,6 @@ module Blather
7
7
  class XMPPNode < XML::Node
8
8
  @@registrations = {}
9
9
 
10
- alias_method :element_name, :name
11
-
12
10
  class_inheritable_accessor :xmlns,
13
11
  :name
14
12
 
@@ -35,7 +33,7 @@ module Blather
35
33
  def self.register(name, xmlns = nil)
36
34
  self.name = name.to_s
37
35
  self.xmlns = xmlns
38
- @@registrations[[name, xmlns]] = self
36
+ @@registrations[[self.name, self.xmlns]] = self
39
37
  end
40
38
 
41
39
  ##
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
1
+ require File.join(File.dirname(__FILE__), *%w[.. spec_helper])
2
2
 
3
3
  describe 'Blather::JID' do
4
4
  it 'does nothing if creaded from JID' do
@@ -75,4 +75,19 @@ describe 'Blather::JID' do
75
75
  JID.new('n', 'd', 'r').to_s.must_equal 'n@d/r'
76
76
  JID.new('n', 'd').to_s.must_equal 'n@d'
77
77
  end
78
+
79
+ it 'falls back to basic ruby if idn gem is not present' do
80
+ JID.const_set 'USE_STRINGPREP', false
81
+ jid = JID.new 'AB#$cdE@&^FE'
82
+ jid.node.must_equal 'ab#$cde'
83
+ jid.domain.must_equal '&^fe'
84
+ end
85
+
86
+ it 'provides a #stripped? helper' do
87
+ jid = JID.new 'a@b/c'
88
+ jid.must_respond_to :stripped?
89
+ jid.stripped?.wont_equal true
90
+ jid.strip!
91
+ jid.stripped?.must_equal true
92
+ end
78
93
  end
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
1
+ require File.join(File.dirname(__FILE__), *%w[.. spec_helper])
2
2
 
3
3
  describe 'Blather::RosterItem' do
4
4
  it 'initializes with JID' do
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
1
+ require File.join(File.dirname(__FILE__), *%w[.. spec_helper])
2
2
 
3
3
  describe 'Blather::Roster' do
4
4
  before do
@@ -40,6 +40,16 @@ describe 'Blather::Roster' do
40
40
  @roster[jid].wont_be_nil
41
41
  end
42
42
 
43
+ it 'aliases #<< to #push and returns self to allow for chaining' do
44
+ jid = 'a@b/c'
45
+ item = RosterItem.new(JID.new(jid))
46
+ jid2 = 'd@e/f'
47
+ item2 = RosterItem.new(JID.new(jid2))
48
+ proc { @roster << item << item2 }.must_change('@roster.items', :length, :by => 2)
49
+ @roster[jid].wont_be_nil
50
+ @roster[jid2].wont_be_nil
51
+ end
52
+
43
53
  it 'sends a @roster addition over the wire' do
44
54
  stream = mock()
45
55
  stream.expects(:send_data)
@@ -0,0 +1,34 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. .. spec_helper])
2
+
3
+ describe 'Blather::Stanza::Iq::Query' do
4
+ it 'registers itself' do
5
+ XMPPNode.class_from_registration(:query, nil).must_equal Stanza::Iq::Query
6
+ end
7
+
8
+ it 'ensures a query node is present on create' do
9
+ query = Stanza::Iq::Query.new
10
+ query.children.detect { |n| n.element_name == 'query' }.wont_be_nil
11
+ end
12
+
13
+ it 'ensures a query node exists when calling #query' do
14
+ query = Stanza::Iq::Query.new
15
+ query.remove_child :query
16
+ query.children.detect { |n| n.element_name == 'query' }.must_be_nil
17
+
18
+ query.query.wont_be_nil
19
+ query.children.detect { |n| n.element_name == 'query' }.wont_be_nil
20
+ end
21
+
22
+ it 'sets type to "reslut" on reply' do
23
+ query = Stanza::Iq::Query.new
24
+ query.type.must_equal :get
25
+ reply = query.reply.type.must_equal :result
26
+ end
27
+
28
+ it 'sets type to "reslut" on reply!' do
29
+ query = Stanza::Iq::Query.new
30
+ query.type.must_equal :get
31
+ query.reply!
32
+ query.type.must_equal :result
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. .. spec_helper])
2
+
3
+ describe 'Blather::Stanza::Iq::Roster' do
4
+ it 'registers itself' do
5
+ XMPPNode.class_from_registration(:query, 'jabber:iq:roster').must_equal Stanza::Iq::Roster
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ describe 'Blather::Stanza::Iq' do
4
+ it 'registers itself' do
5
+ XMPPNode.class_from_registration(:iq, nil).must_equal Stanza::Iq
6
+ end
7
+
8
+ it 'creates a new Iq stanza defaulted as a get' do
9
+ Stanza::Iq.new.type.must_equal :get
10
+ end
11
+ end