blather 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
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