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