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,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