blather 0.1

Sign up to get free protection for your applications and to get access to all the features.
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,24 @@
1
+ module Blather
2
+ # Main error class
3
+ class BlatherError < StandardError; end
4
+
5
+ # Stream errors
6
+ class StreamError < BlatherError
7
+ attr_accessor :type, :text
8
+
9
+ def initialize(node)
10
+ @type = node.detect { |n| n.name != 'text' && n['xmlns'] == 'urn:ietf:params:xml:ns:xmpp-streams' }
11
+ @text = node.detect { |n| n.name == 'text' }
12
+
13
+ @extra = node.detect { |n| n['xmlns'] != 'urn:ietf:params:xml:ns:xmpp-streams' }
14
+ end
15
+
16
+ def to_s
17
+ "Stream Error (#{type.name}) #{"[#{@extra.name}]" if @extra}: #{text.content if text}"
18
+ end
19
+ end
20
+
21
+ # Stanza errors
22
+ class StanzaError < BlatherError; end
23
+ class ArgumentError < StanzaError; end
24
+ end
@@ -0,0 +1,101 @@
1
+ module Blather
2
+
3
+ ##
4
+ # This is a simple modification of the JID class from XMPP4R
5
+ class JID
6
+ include Comparable
7
+
8
+ PATTERN = /^(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?$/
9
+
10
+ begin
11
+ require 'idn'
12
+ USE_STRINGPREP = true
13
+ rescue LoadError
14
+ USE_STRINGPREP = false
15
+ end
16
+
17
+ # Get the JID's node
18
+ attr_reader :node
19
+
20
+ # Get the JID's domain
21
+ attr_reader :domain
22
+
23
+ # Get the JID's resource
24
+ attr_reader :resource
25
+
26
+ def self.new(node, domain = nil, resource = nil)
27
+ node.is_a?(JID) ? node : super
28
+ end
29
+
30
+ ##
31
+ # Create a new JID. If called as new('a@b/c'), parse the string and
32
+ # split (node, domain, resource)
33
+ def initialize(node, domain = nil, resource = nil)
34
+ @resource = resource
35
+ @domain = domain
36
+ @node = node
37
+
38
+ if @domain.nil? && @resource.nil?
39
+ @node, @domain, @resource = @node.to_s.scan(PATTERN).first
40
+ end
41
+
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
50
+
51
+ raise ArgumentError, 'Node too long' if (@node || '').length > 1023
52
+ raise ArgumentError, 'Domain too long' if (@domain || '').length > 1023
53
+ raise ArgumentError, 'Resource too long' if (@resource || '').length > 1023
54
+ end
55
+
56
+ ##
57
+ # Returns a string representation of the JID
58
+ # * ""
59
+ # * "domain"
60
+ # * "node@domain"
61
+ # * "domain/resource"
62
+ # * "node@domain/resource"
63
+ def to_s
64
+ s = @domain
65
+ s = "#{@node}@#{s}" if @node
66
+ s = "#{s}/#{@resource}" if @resource
67
+ s
68
+ end
69
+
70
+ ##
71
+ # Returns a new JID with resource removed.
72
+ # return:: [JID]
73
+ def stripped
74
+ self.class.new @node, @domain
75
+ end
76
+
77
+ ##
78
+ # Removes the resource (sets it to nil)
79
+ # return:: [JID] self
80
+ def strip!
81
+ @resource = nil
82
+ self
83
+ end
84
+
85
+ ##
86
+ # Compare two JIDs,
87
+ # helpful for sorting etc.
88
+ #
89
+ # String representations are compared, see JID#to_s
90
+ def <=>(o)
91
+ to_s <=> o.to_s
92
+ end
93
+
94
+ ##
95
+ # Test if JID is stripped
96
+ def stripped?
97
+ @resource.nil?
98
+ end
99
+ end
100
+
101
+ end
@@ -0,0 +1,84 @@
1
+ module Blather
2
+
3
+ ##
4
+ # Local Roster
5
+ # Takes care of adding/removing JIDs through the stream
6
+ class Roster
7
+ include Enumerable
8
+
9
+ def initialize(stream, stanza = nil)
10
+ @stream = stream
11
+ @items = {}
12
+ stanza.items.each { |i| push i, false } if stanza
13
+ end
14
+
15
+ ##
16
+ # Process any incoming stanzas adn either add or remove the
17
+ # corresponding RosterItem
18
+ def process(stanza)
19
+ stanza.items.each do |i|
20
+ case i.subscription
21
+ when :remove then @items.delete(key(i.jid))
22
+ else @items[key(i.jid)] = RosterItem.new(i)
23
+ end
24
+ end
25
+ end
26
+
27
+ ##
28
+ # Pushes a JID into the roster
29
+ # then returns self to allow for chaining
30
+ def <<(elem)
31
+ push elem
32
+ self
33
+ end
34
+
35
+ ##
36
+ # Push a JID into the roster
37
+ # Will send the new item to the server
38
+ # unless overridden by calling #push(elem, false)
39
+ def push(elem, send = true)
40
+ jid = elem.respond_to?(:jid) ? elem.jid : JID.new(elem)
41
+ @items[key(jid)] = node = RosterItem.new(elem)
42
+
43
+ @stream.send_data(node.to_stanza(:set)) if send
44
+ end
45
+ alias_method :add, :push
46
+
47
+ ##
48
+ # Remove a JID from the roster
49
+ # Sends a remove query stanza to the server
50
+ def delete(jid)
51
+ @items.delete key(jid)
52
+ @stream.send_data Stanza::Iq::Roster.new(:set, Stanza::Iq::Roster::RosterItem.new(jid, nil, :remove))
53
+ end
54
+ alias_method :remove, :delete
55
+
56
+ ##
57
+ # Get a RosterItem by JID
58
+ def [](jid)
59
+ items[key(jid)]
60
+ end
61
+
62
+ ##
63
+ # Iterate over all RosterItems
64
+ def each(&block)
65
+ items.each &block
66
+ end
67
+
68
+ ##
69
+ # Returns a duplicate of all RosterItems
70
+ def items
71
+ @items.dup
72
+ end
73
+
74
+ private
75
+ def self.key(jid)
76
+ JID.new(jid).stripped.to_s
77
+ end
78
+
79
+ def key(jid)
80
+ self.class.key(jid)
81
+ end
82
+ end #Roster
83
+
84
+ end
@@ -0,0 +1,92 @@
1
+ module Blather
2
+
3
+ ##
4
+ # RosterItems hold internal representations of the user's roster
5
+ # including each JID's status.
6
+ class RosterItem
7
+ VALID_SUBSCRIPTION_TYPES = [:both, :from, :none, :remove, :to]
8
+
9
+ attr_reader :jid,
10
+ :ask,
11
+ :statuses
12
+
13
+ attr_accessor :name,
14
+ :groups
15
+
16
+ ##
17
+ # item:: can be a JID, String (a@b) or a Stanza
18
+ def initialize(item)
19
+ @statuses = []
20
+
21
+ case item
22
+ when JID
23
+ self.jid = item.stripped
24
+ when String
25
+ self.jid = JID.new(item).stripped
26
+ 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']
31
+ item.groups.each { |g| self.groups << g }
32
+ end
33
+ end
34
+
35
+ ##
36
+ # Set the jid
37
+ def jid=(jid)
38
+ @jid = JID.new(jid).stripped
39
+ end
40
+
41
+ ##
42
+ # Set the subscription
43
+ # Ensures it is one of VALID_SUBSCRIPTION_TYPES
44
+ def subscription=(sub)
45
+ raise ArgumentError, "Invalid Type (#{sub}), use: #{VALID_SUBSCRIPTION_TYPES*' '}" if
46
+ sub && !VALID_SUBSCRIPTION_TYPES.include?(sub = sub.to_sym)
47
+ @subscription = sub ? sub : :none
48
+ end
49
+
50
+ ##
51
+ # Get the current subscription
52
+ # returns:: :both, :from, :none, :remove, :to or :none
53
+ def subscription
54
+ @subscription || :none
55
+ end
56
+
57
+ ##
58
+ # Set the ask value
59
+ # ask:: must only be nil or :subscribe
60
+ def ask=(ask)
61
+ raise ArgumentError, "Invalid Type (#{ask}), can only be :subscribe" if ask && (ask = ask.to_sym) != :subscribe
62
+ @ask = ask ? ask : nil
63
+ end
64
+
65
+ ##
66
+ # Set the status then sorts them according to priority
67
+ # presence:: Status
68
+ def status=(presence)
69
+ @statuses.delete_if { |s| s.from == presence.from }
70
+ @statuses << presence
71
+ @statuses.sort!
72
+ end
73
+
74
+ ##
75
+ # Return the status with the highest priority
76
+ # if resource is set find the status of that specific resource
77
+ def status(resource = nil)
78
+ top = resource ? @statuses.detect { |s| s.from.resource == resource } : @statuses.first
79
+ end
80
+
81
+ ##
82
+ # Translate the RosterItem into a proper stanza that can be sent over the stream
83
+ def to_stanza(type = nil)
84
+ r = Stanza::Iq::Roster.new type
85
+ n = Stanza::Iq::Roster::RosterItem.new jid, name, subscription, ask
86
+ r.query << n
87
+ n.groups = groups
88
+ r
89
+ end
90
+ end #RosterItem
91
+
92
+ end
@@ -0,0 +1,116 @@
1
+ module Blather
2
+ ##
3
+ # Base XMPP Stanza
4
+ class Stanza < XMPPNode
5
+ @@registered_callbacks = []
6
+
7
+ def self.registered_callbacks
8
+ @@registered_callbacks
9
+ end
10
+
11
+ class_inheritable_array :callback_heirarchy
12
+
13
+ ##
14
+ # Registers a callback onto the callback heirarchy stack
15
+ #
16
+ # Thanks to help from ActiveSupport every class
17
+ # that inherits Stanza can register a callback for itself
18
+ # which is added to a list and iterated over when looking for
19
+ # a callback to use
20
+ def self.register(callback_type, name = nil, xmlns = nil)
21
+ @@registered_callbacks << callback_type
22
+
23
+ self.callback_heirarchy ||= []
24
+ self.callback_heirarchy.unshift callback_type
25
+
26
+ name = name || self.name || callback_type
27
+ super name, xmlns
28
+ end
29
+
30
+ ##
31
+ # Helper method that creates a unique ID for stanzas
32
+ def self.next_id
33
+ @@last_id ||= 0
34
+ @@last_id += 1
35
+ 'blather%04x' % @@last_id
36
+ end
37
+
38
+ ##
39
+ # Creates a new stanza with the same name as the node
40
+ # then inherits all the node's attributes and properties
41
+ def self.import(node)
42
+ self.new(node.element_name).inherit(node)
43
+ end
44
+
45
+ ##
46
+ # Creates a new Stanza with the name given
47
+ # then attaches an ID and document (to enable searching)
48
+ def self.new(elem_name = nil)
49
+ elem = super
50
+ elem.id = next_id
51
+ XML::Document.new.root = elem
52
+ elem
53
+ end
54
+
55
+ def error?
56
+ self.type == :error
57
+ end
58
+
59
+ ##
60
+ # Copies itself then swaps from and to
61
+ # then returns the new stanza
62
+ def reply
63
+ self.copy(true).reply!
64
+ end
65
+
66
+ ##
67
+ # Swaps from and to
68
+ def reply!
69
+ self.to, self.from = self.from, self.to
70
+ self
71
+ end
72
+
73
+ def id=(id)
74
+ attributes.remove :id
75
+ self['id'] = id if id
76
+ end
77
+
78
+ def id
79
+ self['id']
80
+ end
81
+
82
+ def to=(to)
83
+ attributes.remove :to
84
+ self['to'] = to.to_s if to
85
+ end
86
+
87
+ ##
88
+ # returns:: JID created from the "to" value of the stanza
89
+ def to
90
+ JID.new(self['to']) if self['to']
91
+ end
92
+
93
+ def from=(from)
94
+ attributes.remove :from
95
+ self['from'] = from.to_s if from
96
+ end
97
+
98
+ ##
99
+ # returns:: JID created from the "from" value of the stanza
100
+ def from
101
+ JID.new(self['from']) if self['from']
102
+ end
103
+
104
+ def type=(type)
105
+ attributes.remove :type
106
+ self['type'] = type.to_s
107
+ end
108
+
109
+ ##
110
+ # returns:: a symbol of the type
111
+ def type
112
+ self['type'].to_sym if self['type']
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,27 @@
1
+ module Blather
2
+ class Stanza
3
+
4
+ ##
5
+ # Base Iq stanza
6
+ class Iq < Stanza
7
+ register :iq
8
+
9
+ def self.import(node)
10
+ raise "Import missmatch #{[node.element_name, self.name].inspect}" if node.element_name != self.name.to_s
11
+ klass = nil
12
+ node.each { |e| break if klass = class_from_registration(e.element_name, e.xmlns) }
13
+ (klass || self).new(node['type']).inherit(node)
14
+ end
15
+
16
+ def self.new(type, to = nil, id = nil)
17
+ elem = super :iq
18
+ elem.xmlns = nil
19
+ elem.type = type
20
+ elem.to = to
21
+ elem.id = id if id
22
+ elem
23
+ end
24
+ end
25
+
26
+ end #Stanza
27
+ end