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.
- data/CHANGELOG +1 -0
- data/LICENSE +20 -0
- data/Manifest +43 -0
- data/README.rdoc +78 -0
- data/Rakefile +16 -0
- data/blather.gemspec +41 -0
- data/examples/echo.rb +22 -0
- data/examples/shell_client.rb +28 -0
- data/lib/autotest/discover.rb +1 -0
- data/lib/autotest/spec.rb +60 -0
- data/lib/blather.rb +46 -0
- data/lib/blather/callback.rb +24 -0
- data/lib/blather/client.rb +81 -0
- data/lib/blather/core/errors.rb +24 -0
- data/lib/blather/core/jid.rb +101 -0
- data/lib/blather/core/roster.rb +84 -0
- data/lib/blather/core/roster_item.rb +92 -0
- data/lib/blather/core/stanza.rb +116 -0
- data/lib/blather/core/stanza/iq.rb +27 -0
- data/lib/blather/core/stanza/iq/query.rb +42 -0
- data/lib/blather/core/stanza/iq/roster.rb +96 -0
- data/lib/blather/core/stanza/message.rb +55 -0
- data/lib/blather/core/stanza/presence.rb +35 -0
- data/lib/blather/core/stanza/presence/status.rb +77 -0
- data/lib/blather/core/stanza/presence/subscription.rb +73 -0
- data/lib/blather/core/stream.rb +181 -0
- data/lib/blather/core/stream/parser.rb +74 -0
- data/lib/blather/core/stream/resource.rb +51 -0
- data/lib/blather/core/stream/sasl.rb +135 -0
- data/lib/blather/core/stream/session.rb +43 -0
- data/lib/blather/core/stream/tls.rb +29 -0
- data/lib/blather/core/sugar.rb +150 -0
- data/lib/blather/core/xmpp_node.rb +132 -0
- data/lib/blather/extensions.rb +4 -0
- data/lib/blather/extensions/last_activity.rb +55 -0
- data/lib/blather/extensions/version.rb +85 -0
- data/spec/blather/core/jid_spec.rb +78 -0
- data/spec/blather/core/roster_item_spec.rb +80 -0
- data/spec/blather/core/roster_spec.rb +79 -0
- data/spec/blather/core/stanza_spec.rb +95 -0
- data/spec/blather/core/stream_spec.rb +263 -0
- data/spec/blather/core/xmpp_node_spec.rb +130 -0
- data/spec/build_safe.rb +20 -0
- data/spec/spec_helper.rb +49 -0
- 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
|