blather 0.1

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 (45) hide show
  1. data/CHANGELOG +1 -0
  2. data/LICENSE +20 -0
  3. data/Manifest +43 -0
  4. data/README.rdoc +78 -0
  5. data/Rakefile +16 -0
  6. data/blather.gemspec +41 -0
  7. data/examples/echo.rb +22 -0
  8. data/examples/shell_client.rb +28 -0
  9. data/lib/autotest/discover.rb +1 -0
  10. data/lib/autotest/spec.rb +60 -0
  11. data/lib/blather.rb +46 -0
  12. data/lib/blather/callback.rb +24 -0
  13. data/lib/blather/client.rb +81 -0
  14. data/lib/blather/core/errors.rb +24 -0
  15. data/lib/blather/core/jid.rb +101 -0
  16. data/lib/blather/core/roster.rb +84 -0
  17. data/lib/blather/core/roster_item.rb +92 -0
  18. data/lib/blather/core/stanza.rb +116 -0
  19. data/lib/blather/core/stanza/iq.rb +27 -0
  20. data/lib/blather/core/stanza/iq/query.rb +42 -0
  21. data/lib/blather/core/stanza/iq/roster.rb +96 -0
  22. data/lib/blather/core/stanza/message.rb +55 -0
  23. data/lib/blather/core/stanza/presence.rb +35 -0
  24. data/lib/blather/core/stanza/presence/status.rb +77 -0
  25. data/lib/blather/core/stanza/presence/subscription.rb +73 -0
  26. data/lib/blather/core/stream.rb +181 -0
  27. data/lib/blather/core/stream/parser.rb +74 -0
  28. data/lib/blather/core/stream/resource.rb +51 -0
  29. data/lib/blather/core/stream/sasl.rb +135 -0
  30. data/lib/blather/core/stream/session.rb +43 -0
  31. data/lib/blather/core/stream/tls.rb +29 -0
  32. data/lib/blather/core/sugar.rb +150 -0
  33. data/lib/blather/core/xmpp_node.rb +132 -0
  34. data/lib/blather/extensions.rb +4 -0
  35. data/lib/blather/extensions/last_activity.rb +55 -0
  36. data/lib/blather/extensions/version.rb +85 -0
  37. data/spec/blather/core/jid_spec.rb +78 -0
  38. data/spec/blather/core/roster_item_spec.rb +80 -0
  39. data/spec/blather/core/roster_spec.rb +79 -0
  40. data/spec/blather/core/stanza_spec.rb +95 -0
  41. data/spec/blather/core/stream_spec.rb +263 -0
  42. data/spec/blather/core/xmpp_node_spec.rb +130 -0
  43. data/spec/build_safe.rb +20 -0
  44. data/spec/spec_helper.rb +49 -0
  45. metadata +172 -0
@@ -0,0 +1,132 @@
1
+ module Blather
2
+
3
+ ##
4
+ # Base XML Node
5
+ # All XML classes subclass XMPPNode
6
+ # it allows the addition of helpers
7
+ class XMPPNode < XML::Node
8
+ @@registrations = {}
9
+
10
+ alias_method :element_name, :name
11
+
12
+ class_inheritable_accessor :xmlns,
13
+ :name
14
+
15
+ ##
16
+ # Automatically sets the namespace registered by the subclass
17
+ def self.new(name = nil, content = nil)
18
+ name ||= self.name
19
+
20
+ args = []
21
+ args << name.to_s if name
22
+ args << content if content
23
+
24
+ elem = super *args
25
+ elem.xmlns = xmlns
26
+ elem
27
+ end
28
+
29
+ ##
30
+ # Lets a subclass register itself
31
+ #
32
+ # This registers a namespace that is used when looking
33
+ # up the class name of the object to instantiate when a new
34
+ # stanza is received
35
+ def self.register(name, xmlns = nil)
36
+ self.name = name.to_s
37
+ self.xmlns = xmlns
38
+ @@registrations[[name, xmlns]] = self
39
+ end
40
+
41
+ ##
42
+ # Find the class to use given the name and namespace of a stanza
43
+ def self.class_from_registration(name, xmlns)
44
+ name = name.to_s
45
+ @@registrations[[name, xmlns]] || @@registrations[[name, nil]]
46
+ end
47
+
48
+ ##
49
+ # Looks up the class to use then instantiates an object
50
+ # of that class and imports all the <tt>node</tt>'s attributes
51
+ # and children into it.
52
+ def self.import(node)
53
+ klass = class_from_registration(node.element_name, node.xmlns)
54
+ if klass && klass != self
55
+ klass.import(node)
56
+ else
57
+ new(node.element_name).inherit(node)
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Quickway of turning itself into a proper object
63
+ def to_stanza
64
+ self.class.import self
65
+ end
66
+
67
+ def xmlns=(ns)
68
+ attributes.remove :xmlns
69
+ self['xmlns'] = ns if ns
70
+ end
71
+
72
+ def xmlns
73
+ self['xmlns']
74
+ end
75
+
76
+ ##
77
+ # Remove a child with the name and (optionally) namespace given
78
+ def remove_child(name, ns = nil)
79
+ name = name.to_s
80
+ self.each { |n| n.remove! if n.element_name == name && (!ns || n.xmlns == ns) }
81
+ end
82
+
83
+ ##
84
+ # Remove all children with a given name
85
+ def remove_children(name)
86
+ name = name.to_s
87
+ self.find(name).each { |n| n.remove! }
88
+ end
89
+
90
+ ##
91
+ # Pull the content from a child
92
+ def content_from(name)
93
+ name = name.to_s
94
+ (child = self.detect { |n| n.element_name == name }) ? child.content : nil
95
+ end
96
+
97
+ ##
98
+ # Create a copy
99
+ def copy(deep = true)
100
+ self.class.new(self.element_name).inherit(self)
101
+ end
102
+
103
+ ##
104
+ # Inherit all of <tt>stanza</tt>'s attributes and children
105
+ def inherit(stanza)
106
+ inherit_attrs stanza.attributes
107
+ stanza.children.each { |c| self << c.copy(true) }
108
+ self
109
+ end
110
+
111
+ ##
112
+ # Inherit only <tt>stanza</tt>'s attributes
113
+ def inherit_attrs(attrs)
114
+ attrs.each { |a| self[a.name] = a.value }
115
+ self
116
+ end
117
+
118
+ ##
119
+ # Turn itself into a string and remove all whitespace between nodes
120
+ def to_s
121
+ # TODO: Fix this for HTML nodes (and any other that might require whitespace)
122
+ super.gsub(">\n<", '><')
123
+ end
124
+
125
+ ##
126
+ # Override #find to work when a node isn't attached to a document
127
+ def find(what, nslist = nil)
128
+ (self.doc ? super(what, nslist) : select { |i| i.element_name == what})
129
+ end
130
+ end #XMPPNode
131
+
132
+ end
@@ -0,0 +1,4 @@
1
+ %w[
2
+ last_activity
3
+ version
4
+ ].each { |r| require "extensions/#{r}" }
@@ -0,0 +1,55 @@
1
+ module Blather
2
+ module Extensions #:nodoc:
3
+
4
+ module LastActivity #:nodoc:
5
+ def self.included(base)
6
+ base.class_eval do
7
+ @@last_activity = Time.now
8
+
9
+ alias_method :send_data_without_activity, :send_data
10
+ def send_data(data)
11
+ @@last_activity = Time.now
12
+ send_data_without_activity data
13
+ end
14
+ end
15
+ end
16
+
17
+ def last_activity
18
+ (Time.now - @@last_activity).to_i
19
+ end
20
+
21
+ def receive_last_activity(stanza)
22
+ send_data stanza.reply!(last_activity) if stanza.type == 'get'
23
+ end
24
+ end #LastActivity
25
+
26
+ class LastActivityStanza < Query #:nodoc:
27
+ register :last_activity, nil, 'jabber:iq:last'
28
+
29
+ def self.new(type = :get, seconds = nil)
30
+ elem = super type
31
+ elem.seconds = seconds
32
+ elem
33
+ end
34
+
35
+ def seconds=(seconds)
36
+ query.attributes.remove :seconds
37
+ query['seconds'] = seconds.to_i.to_s if seconds
38
+ end
39
+
40
+ def seconds
41
+ (query['seconds'] || 0).to_i
42
+ end
43
+
44
+ def reply(seconds)
45
+ elem = super()
46
+ elem.last_activity = seconds
47
+ end
48
+
49
+ def reply!(seconds)
50
+ self.last_activity = seconds
51
+ super()
52
+ end
53
+ end #LastActivityStanza
54
+ end
55
+ end
@@ -0,0 +1,85 @@
1
+ module Blather
2
+ module Extensions #:nodoc:
3
+
4
+ module Version #:nodoc:
5
+ def self.included(base)
6
+ base.class_eval do
7
+ @@version = {}
8
+
9
+ def self.version(name, ver, os = nil)
10
+ @@version = {:name => name, :version => ver, :os => os}
11
+ end
12
+
13
+ add_callback(:iq, 100) do |c, s|
14
+ if s.detect { |n| n['xmlns'] == 'jabber:iq:version' }
15
+ ver = VersionStanza.new('result', c.version)
16
+ ver.id = s.id
17
+ ver.from = c.jid
18
+ ver.to = s.from
19
+ c.send_data ver
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def version
26
+ @@version
27
+ end
28
+ end #Version
29
+
30
+ class VersionStanza < Iq #:nodoc:
31
+ def self.new(type = 'result', ver = {})
32
+ elem = super(type)
33
+
34
+ query = XML::Node.new('query')
35
+ query.xmlns = 'jabber:iq:version'
36
+ elem << query
37
+
38
+ elem.name = ver[:name] if ver[:name]
39
+ elem.version = ver[:version] if ver[:version]
40
+ elem.os = ver[:os] if ver[:os]
41
+
42
+ elem
43
+ end
44
+
45
+ def query
46
+ @query ||= self.detect { |n| n.name == 'query' }
47
+ end
48
+
49
+ def name=(name)
50
+ query.each { |n| n.remove! or break if n.name == 'name' }
51
+ query << XML::Node.new('name', name)
52
+ end
53
+
54
+ def name
55
+ if name = query.detect { |n| n.name == 'name' }
56
+ name.content
57
+ end
58
+ end
59
+
60
+ def version=(version)
61
+ query.each { |n| n.remove! or break if n.name == 'version' }
62
+ query << XML::Node.new('version', version)
63
+ end
64
+
65
+ def version
66
+ if version = query.detect { |n| n.version == 'version' }
67
+ version.content
68
+ end
69
+ end
70
+
71
+ def os=(os)
72
+ query.each { |n| n.remove! or break if n.name == 'os' }
73
+ query << XML::Node.new('os', os)
74
+ end
75
+
76
+ def os
77
+ if os = query.detect { |n| n.os == 'os' }
78
+ os.content
79
+ end
80
+ end
81
+ end #VersionStanza
82
+ end
83
+ end
84
+
85
+ Blather::Client.__send__ :include, Blather::Extensions::Version
@@ -0,0 +1,78 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ describe 'Blather::JID' do
4
+ it 'does nothing if creaded from JID' do
5
+ jid = JID.new 'n@d/r'
6
+ JID.new(jid).object_id.must_equal jid.object_id
7
+ end
8
+
9
+ it 'creates a new JID from (n,d,r)' do
10
+ jid = JID.new('n', 'd', 'r')
11
+ jid.node.must_equal 'n'
12
+ jid.domain.must_equal 'd'
13
+ jid.resource.must_equal 'r'
14
+ end
15
+
16
+ it 'creates a new JID from (n,d)' do
17
+ jid = JID.new('n', 'd')
18
+ jid.node.must_equal 'n'
19
+ jid.domain.must_equal 'd'
20
+ end
21
+
22
+ it 'creates a new JID from (n@d)' do
23
+ jid = JID.new('n@d')
24
+ jid.node.must_equal 'n'
25
+ jid.domain.must_equal 'd'
26
+ end
27
+
28
+ it 'creates a new JID from (n@d/r)' do
29
+ jid = JID.new('n@d/r')
30
+ jid.node.must_equal 'n'
31
+ jid.domain.must_equal 'd'
32
+ jid.resource.must_equal 'r'
33
+ end
34
+
35
+ it 'requires at least a node' do
36
+ proc { JID.new }.must_raise ArgumentError
37
+ end
38
+
39
+ it 'ensures length of node is no more than 1023 characters' do
40
+ proc { JID.new('n'*1024) }.must_raise Blather::ArgumentError
41
+ end
42
+
43
+ it 'ensures length of domain is no more than 1023 characters' do
44
+ proc { JID.new('n', 'd'*1024) }.must_raise Blather::ArgumentError
45
+ end
46
+
47
+ it 'ensures length of resource is no more than 1023 characters' do
48
+ proc { JID.new('n', 'd', 'r'*1024) }.must_raise Blather::ArgumentError
49
+ end
50
+
51
+ it 'compares JIDs' do
52
+ (JID.new('a@b/c') <=> JID.new('d@e/f')).must_equal -1
53
+ (JID.new('a@b/c') <=> JID.new('a@b/c')).must_equal 0
54
+ (JID.new('d@e/f') <=> JID.new('a@b/c')).must_equal 1
55
+ end
56
+
57
+ it 'checks for equality' do
58
+ (JID.new('n@d/r') == JID.new('n@d/r')).must_equal true
59
+ end
60
+
61
+ it 'will strip' do
62
+ jid = JID.new('n@d/r')
63
+ jid.stripped.must_equal JID.new('n@d')
64
+ jid.must_equal JID.new('n@d/r')
65
+ end
66
+
67
+ it 'will strip itself' do
68
+ jid = JID.new('n@d/r')
69
+ jid.strip!
70
+ jid.must_equal JID.new('n@d')
71
+ end
72
+
73
+ it 'has a string representation' do
74
+ JID.new('n@d/r').to_s.must_equal 'n@d/r'
75
+ JID.new('n', 'd', 'r').to_s.must_equal 'n@d/r'
76
+ JID.new('n', 'd').to_s.must_equal 'n@d'
77
+ end
78
+ end
@@ -0,0 +1,80 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ describe 'Blather::RosterItem' do
4
+ it 'initializes with JID' do
5
+ jid = JID.new(jid)
6
+ i = RosterItem.new jid
7
+ i.jid.must_equal jid
8
+ end
9
+
10
+ it 'initializes with an Iq::RosterItem' do
11
+ jid = 'n@d/r'
12
+ i = RosterItem.new Stanza::Iq::Roster::RosterItem.new(jid)
13
+ i.jid.must_equal JID.new(jid).stripped
14
+ end
15
+
16
+ it 'has a JID setter that strips the JID' do
17
+ jid = JID.new('n@d/r')
18
+ i = RosterItem.new nil
19
+ i.jid = jid
20
+ i.jid.must_equal jid.stripped
21
+ end
22
+
23
+ it 'has a subscription setter that forces a symbol' do
24
+ i = RosterItem.new nil
25
+ i.subscription = 'remove'
26
+ i.subscription.must_equal :remove
27
+ end
28
+
29
+ it 'forces the type of subscription' do
30
+ proc { RosterItem.new(nil).subscription = 'foo' }.must_raise Blather::ArgumentError
31
+ end
32
+
33
+ it 'returns :none if the subscription field is blank' do
34
+ RosterItem.new(nil).subscription.must_equal :none
35
+ end
36
+
37
+ it 'ensure #ask is a symbol' do
38
+ i = RosterItem.new(nil)
39
+ i.ask = 'subscribe'
40
+ i.ask.must_equal :subscribe
41
+ end
42
+
43
+ it 'forces #ask to be :subscribe or nothing at all' do
44
+ proc { RosterItem.new(nil).ask = 'foo' }.must_raise Blather::ArgumentError
45
+ end
46
+
47
+ it 'generates a stanza with #to_stanza' do
48
+ jid = JID.new('n@d/r')
49
+ i = RosterItem.new jid
50
+ s = i.to_stanza
51
+ s.must_be_kind_of Stanza::Iq::Roster
52
+ s.items.first.jid.must_equal jid.stripped
53
+ end
54
+
55
+ it 'returns status based on priority' do
56
+ setup_item_with_presences
57
+ @i.status.must_equal @p2
58
+ end
59
+
60
+ it 'returns status based on resource' do
61
+ setup_item_with_presences
62
+ @i.status('a').must_equal @p
63
+ end
64
+
65
+ def setup_item_with_presences
66
+ @jid = JID.new('n@d/r')
67
+ @i = RosterItem.new @jid
68
+
69
+ @p = Stanza::Presence::Status.new(:away)
70
+ @p.from = 'n@d/a'
71
+ @p.priority = 0
72
+
73
+ @p2 = Stanza::Presence::Status.new(:dnd)
74
+ @p2.from = 'n@d/b'
75
+ @p2.priority = -1
76
+
77
+ @i.status = @p
78
+ @i.status = @p2
79
+ end
80
+ end
@@ -0,0 +1,79 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ describe 'Blather::Roster' do
4
+ before do
5
+ @stream = mock()
6
+ @stream.stubs(:send_data)
7
+
8
+ @stanza = mock()
9
+ items = []; 4.times { |n| items << JID.new("n@d/#{n}r") }
10
+ @stanza.stubs(:items).returns(items)
11
+
12
+ @roster = Roster.new(@stream, @stanza)
13
+ end
14
+
15
+ it 'initializes with items' do
16
+ @roster.items.map { |_,i| i.jid.to_s }.must_equal(@stanza.items.map { |i| i.stripped.to_s }.uniq)
17
+ end
18
+
19
+ it 'processes @stanzas with remove requests' do
20
+ s = @roster['n@d/0r']
21
+ s.subscription = :remove
22
+ proc { @roster.process(s.to_stanza) }.must_change('@roster.items', :length, :by => -1)
23
+ end
24
+
25
+ it 'processes @stanzas with add requests' do
26
+ s = Stanza::Iq::Roster::RosterItem.new('a@b/c').to_stanza
27
+ proc { @roster.process(s) }.must_change('@roster.items', :length, :by => 1)
28
+ end
29
+
30
+ it 'allows a jid to be pushed' do
31
+ jid = 'a@b/c'
32
+ proc { @roster.push(jid) }.must_change('@roster.items', :length, :by => 1)
33
+ @roster[jid].wont_be_nil
34
+ end
35
+
36
+ it 'allows an item to be pushed' do
37
+ jid = 'a@b/c'
38
+ item = RosterItem.new(JID.new(jid))
39
+ proc { @roster.push(item) }.must_change('@roster.items', :length, :by => 1)
40
+ @roster[jid].wont_be_nil
41
+ end
42
+
43
+ it 'sends a @roster addition over the wire' do
44
+ stream = mock()
45
+ stream.expects(:send_data)
46
+ roster = Roster.new stream, @stanza
47
+ roster.push('a@b/c')
48
+ end
49
+
50
+ it 'removes a JID' do
51
+ proc { @roster.delete 'n@d' }.must_change('@roster.items', :length, :by => -1)
52
+ end
53
+
54
+ it 'sends a @roster removal over the wire' do
55
+ stream = mock(:send_data => nil)
56
+ roster = Roster.new stream, @stanza
57
+ roster.delete('a@b/c')
58
+ end
59
+
60
+ it 'returns an item through []' do
61
+ item = @roster['n@d']
62
+ item.must_be_kind_of RosterItem
63
+ item.jid.must_equal JID.new('n@d')
64
+ end
65
+
66
+ it 'responds to #each' do
67
+ @roster.must_respond_to :each
68
+ end
69
+
70
+ it 'cycles through the items using #each' do
71
+ @roster.map { |i| i }.sort.must_equal(@roster.items.sort)
72
+ end
73
+
74
+ it 'returns a duplicate of items through #items' do
75
+ items = @roster.items
76
+ items.delete 'n@d'
77
+ items.wont_equal @roster.items
78
+ end
79
+ end