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,42 @@
|
|
1
|
+
module Blather
|
2
|
+
class Stanza
|
3
|
+
class Iq
|
4
|
+
|
5
|
+
class Query < Iq
|
6
|
+
register :query, :query
|
7
|
+
|
8
|
+
def self.new(type)
|
9
|
+
elem = super
|
10
|
+
elem.query.xmlns = self.xmlns
|
11
|
+
elem
|
12
|
+
end
|
13
|
+
|
14
|
+
def inherit(node)
|
15
|
+
query.remove!
|
16
|
+
@query = nil
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def query
|
21
|
+
@query ||= if q = find_first('query')
|
22
|
+
q
|
23
|
+
else
|
24
|
+
self << q = XMPPNode.new('query')
|
25
|
+
q
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def reply
|
30
|
+
elem = super
|
31
|
+
elem.type = :result
|
32
|
+
end
|
33
|
+
|
34
|
+
def reply!
|
35
|
+
self.type = :result
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end #Query
|
39
|
+
|
40
|
+
end #Iq
|
41
|
+
end #Stanza
|
42
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Blather
|
2
|
+
class Stanza
|
3
|
+
class Iq
|
4
|
+
|
5
|
+
class Roster < Query
|
6
|
+
register :roster, nil, 'jabber:iq:roster'
|
7
|
+
|
8
|
+
def self.new(type, item = nil)
|
9
|
+
elem = super(type)
|
10
|
+
elem.query << item
|
11
|
+
elem
|
12
|
+
end
|
13
|
+
|
14
|
+
def inherit(node)
|
15
|
+
items.each { |i| i.remove! }
|
16
|
+
@items = nil
|
17
|
+
super
|
18
|
+
items.each { |i| query << RosterItem.new(i); i.remove! }
|
19
|
+
@items = nil
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def items
|
24
|
+
@items ||= query.find('item')#.map { |g| RosterItem.new g }
|
25
|
+
end
|
26
|
+
|
27
|
+
class RosterItem < XMPPNode
|
28
|
+
def self.new(jid = nil, name = nil, subscription = nil, ask = nil)
|
29
|
+
elem = super('item')
|
30
|
+
if jid.is_a?(XML::Node)
|
31
|
+
elem.inherit jid
|
32
|
+
else
|
33
|
+
elem.jid = jid
|
34
|
+
elem.name = name
|
35
|
+
elem.subscription = subscription
|
36
|
+
elem.ask = ask
|
37
|
+
end
|
38
|
+
elem
|
39
|
+
end
|
40
|
+
|
41
|
+
def jid
|
42
|
+
(j = self['jid']) ? JID.new(j) : nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def jid=(jid)
|
46
|
+
attributes.remove :jid
|
47
|
+
self['jid'] = jid.to_s if jid
|
48
|
+
end
|
49
|
+
|
50
|
+
def name
|
51
|
+
self['name']
|
52
|
+
end
|
53
|
+
|
54
|
+
def name=(name)
|
55
|
+
attributes.remove :name
|
56
|
+
self['name'] = name if name
|
57
|
+
end
|
58
|
+
|
59
|
+
def subscription
|
60
|
+
self['subscription'].to_sym if self['subscription']
|
61
|
+
end
|
62
|
+
|
63
|
+
def subscription=(subscription)
|
64
|
+
attributes.remove :subscription
|
65
|
+
self['subscription'] = subscription.to_s if subscription
|
66
|
+
end
|
67
|
+
|
68
|
+
def ask
|
69
|
+
self['ask'].to_sym if self['ask']
|
70
|
+
end
|
71
|
+
|
72
|
+
def ask=(ask)
|
73
|
+
attributes.remove :ask
|
74
|
+
self['ask'] = ask if ask
|
75
|
+
end
|
76
|
+
|
77
|
+
def groups
|
78
|
+
@groups ||= find('group').map { |g| g.content }
|
79
|
+
end
|
80
|
+
|
81
|
+
def groups=(grps)
|
82
|
+
find('group').each { |g| g.remove! }
|
83
|
+
@groups = nil
|
84
|
+
|
85
|
+
grps.uniq.each { |g| add_node XML::Node.new('group', g.to_s) } if grps
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_stanza
|
89
|
+
Roster.new(:set, self)
|
90
|
+
end
|
91
|
+
end #RosterItem
|
92
|
+
end #Roster
|
93
|
+
|
94
|
+
end #Iq
|
95
|
+
end #Stanza
|
96
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Blather
|
2
|
+
class Stanza
|
3
|
+
|
4
|
+
##
|
5
|
+
# Base Message stanza
|
6
|
+
class Message < Stanza
|
7
|
+
VALID_TYPES = [:chat, :error, :groupchat, :headline, :normal]
|
8
|
+
|
9
|
+
register :message
|
10
|
+
|
11
|
+
def self.new(to = nil, type = nil, body = nil)
|
12
|
+
elem = super()
|
13
|
+
elem.to = to
|
14
|
+
elem.type = type
|
15
|
+
elem.body = body
|
16
|
+
elem
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Ensures type is :chat, :error, :groupchat, :headline or :normal
|
21
|
+
def type=(type)
|
22
|
+
raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}" if type && !VALID_TYPES.include?(type.to_sym)
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def body=(body)
|
27
|
+
remove_child :body
|
28
|
+
self << XMPPNode.new('body', body) if body
|
29
|
+
end
|
30
|
+
|
31
|
+
def body
|
32
|
+
content_from :body
|
33
|
+
end
|
34
|
+
|
35
|
+
def subject=(subject)
|
36
|
+
remove_child :subject
|
37
|
+
self << XMPPNode.new('subject', subject) if subject
|
38
|
+
end
|
39
|
+
|
40
|
+
def subject
|
41
|
+
content_from :subject
|
42
|
+
end
|
43
|
+
|
44
|
+
def thread=(thread)
|
45
|
+
remove_child :thread
|
46
|
+
self << XMPPNode.new('body', body) if body
|
47
|
+
end
|
48
|
+
|
49
|
+
def thread
|
50
|
+
content_from :thread
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end #Stanza
|
55
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Blather
|
2
|
+
class Stanza
|
3
|
+
|
4
|
+
##
|
5
|
+
# Base Presence stanza
|
6
|
+
class Presence < Stanza
|
7
|
+
VALID_TYPES = [:unavailable, :subscribe, :subscribed, :unsubscribe, :unsubscribed, :probe, :error]
|
8
|
+
|
9
|
+
register :presence
|
10
|
+
|
11
|
+
##
|
12
|
+
# Creates a class based on the presence type
|
13
|
+
# either a Status or Subscription object is created based
|
14
|
+
# on the type attribute.
|
15
|
+
# If neither is found it instantiates a Presence object
|
16
|
+
def self.import(node)
|
17
|
+
klass = case node['type']
|
18
|
+
when nil, 'unavailable' then Status
|
19
|
+
when /subscribe/ then Subscription
|
20
|
+
else self
|
21
|
+
end
|
22
|
+
klass.new.inherit(node)
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Ensures type is one of :unavailable, :subscribe, :subscribed, :unsubscribe, :unsubscribed, :probe or :error
|
27
|
+
def type=(type)
|
28
|
+
raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}" if type && !VALID_TYPES.include?(type.to_sym)
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end #Stanza
|
35
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Blather
|
2
|
+
class Stanza
|
3
|
+
class Presence
|
4
|
+
|
5
|
+
class Status < Presence
|
6
|
+
VALID_STATES = [:away, :chat, :dnd, :xa]
|
7
|
+
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
register :status
|
11
|
+
|
12
|
+
def self.new(state = nil, message = nil)
|
13
|
+
elem = super()
|
14
|
+
elem.state = state
|
15
|
+
elem.message = message
|
16
|
+
elem
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Ensures type is nil or :unavailable
|
21
|
+
def type=(type)
|
22
|
+
raise ArgumentError, "Invalid type (#{type}). Must be nil or unavailable" if type && type.to_sym != :unavailable
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Ensure state is one of :away, :chat, :dnd, :xa or nil
|
28
|
+
def state=(state)
|
29
|
+
state = state.to_sym if state
|
30
|
+
state = nil if state == :available
|
31
|
+
raise ArgumentError, "Invalid Status (#{state}), use: #{VALID_STATES*' '}" if state && !VALID_STATES.include?(state)
|
32
|
+
|
33
|
+
remove_child :show
|
34
|
+
self << XMPPNode.new('show', state) if state
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# return:: :available if state is nil
|
39
|
+
def state
|
40
|
+
(type || content_from(:show) || :available).to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Ensure priority is between -128 and 127
|
45
|
+
def priority=(priority)
|
46
|
+
raise ArgumentError, 'Priority must be between -128 and +127' if priority && !(-128..127).include?(priority.to_i)
|
47
|
+
|
48
|
+
remove_child :priority
|
49
|
+
self << XMPPNode.new('priority', priority) if priority
|
50
|
+
end
|
51
|
+
|
52
|
+
def priority
|
53
|
+
@priority ||= content_from(:priority).to_i
|
54
|
+
end
|
55
|
+
|
56
|
+
def message=(msg)
|
57
|
+
remove_child :status
|
58
|
+
self << XMPPNode.new('status', msg) if msg
|
59
|
+
end
|
60
|
+
|
61
|
+
def message
|
62
|
+
content_from :status
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Compare status based on priority
|
67
|
+
# raises an error if the JIDs aren't the same
|
68
|
+
def <=>(o)
|
69
|
+
raise "Cannot compare status from different JIDs: #{[self.from, o.from].inspect}" unless self.from.stripped == o.from.stripped
|
70
|
+
self.priority <=> o.priority
|
71
|
+
end
|
72
|
+
|
73
|
+
end #Status
|
74
|
+
|
75
|
+
end #Presence
|
76
|
+
end #Stanza
|
77
|
+
end #Blather
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Blather
|
2
|
+
class Stanza
|
3
|
+
class Presence
|
4
|
+
|
5
|
+
class Subscription < Presence
|
6
|
+
register :subscription
|
7
|
+
|
8
|
+
def self.new(to = nil, type = nil)
|
9
|
+
elem = super()
|
10
|
+
elem.to = to
|
11
|
+
elem.type = type
|
12
|
+
elem
|
13
|
+
end
|
14
|
+
|
15
|
+
def inherit(node)
|
16
|
+
inherit_attrs node.attributes
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def to=(to)
|
21
|
+
super JID.new(to).stripped
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Create an approve stanza
|
26
|
+
def approve!
|
27
|
+
self.type = :subscribed
|
28
|
+
morph_to_reply
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Create a refuse stanza
|
33
|
+
def refuse!
|
34
|
+
self.type = :unsubscribed
|
35
|
+
morph_to_reply
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Create an unsubscribe stanza
|
40
|
+
def unsubscribe!
|
41
|
+
self.type = :unsubscribe
|
42
|
+
morph_to_reply
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Create a cancel stanza
|
47
|
+
def cancel!
|
48
|
+
self.type = :unsubscribed
|
49
|
+
morph_to_reply
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Create a request stanza
|
54
|
+
def request!
|
55
|
+
self.type = :subscribe
|
56
|
+
morph_to_reply
|
57
|
+
end
|
58
|
+
|
59
|
+
def request?
|
60
|
+
self.type == :subscribe
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def morph_to_reply
|
65
|
+
self.to = self.from if self.from
|
66
|
+
self.from = nil
|
67
|
+
self
|
68
|
+
end
|
69
|
+
end #Subscription
|
70
|
+
|
71
|
+
end #Presence
|
72
|
+
end #Stanza
|
73
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
module Blather
|
2
|
+
|
3
|
+
module Stream
|
4
|
+
|
5
|
+
# Connect to the server
|
6
|
+
def self.start(client, jid, pass, host = nil, port = 5222)
|
7
|
+
jid = JID.new jid
|
8
|
+
host ||= jid.domain
|
9
|
+
|
10
|
+
EM.connect host, port, self, client, jid, pass
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(client, jid, pass) # :nodoc:
|
14
|
+
super()
|
15
|
+
|
16
|
+
@client = client
|
17
|
+
|
18
|
+
self.jid = jid
|
19
|
+
@pass = pass
|
20
|
+
|
21
|
+
@to = @jid.domain
|
22
|
+
@id = nil
|
23
|
+
@lang = 'en'
|
24
|
+
@version = '1.0'
|
25
|
+
@namespace = 'jabber:client'
|
26
|
+
|
27
|
+
@parser = Parser.new self
|
28
|
+
end
|
29
|
+
|
30
|
+
def connection_completed # :nodoc:
|
31
|
+
# @keepalive = EM::Timer.new(60) { send_data ' ' }
|
32
|
+
@state = :stopped
|
33
|
+
dispatch
|
34
|
+
end
|
35
|
+
|
36
|
+
def receive_data(data) # :nodoc:
|
37
|
+
@parser.parse data
|
38
|
+
|
39
|
+
rescue => e
|
40
|
+
@client.respond_to?(:rescue) ? @client.rescue(e) : raise(e)
|
41
|
+
end
|
42
|
+
|
43
|
+
def unbind # :nodoc:
|
44
|
+
# @keepalive.cancel
|
45
|
+
@state == :stopped
|
46
|
+
end
|
47
|
+
|
48
|
+
def receive(node) # :nodoc:
|
49
|
+
LOG.debug "\n"+('-'*30)+"\n"
|
50
|
+
LOG.debug "RECEIVING (#{node.element_name}) #{node}"
|
51
|
+
@node = node
|
52
|
+
|
53
|
+
case @node.element_name
|
54
|
+
when 'stream:stream'
|
55
|
+
@state = :ready if @state == :stopped
|
56
|
+
|
57
|
+
when 'stream:end'
|
58
|
+
@state = :stopped
|
59
|
+
|
60
|
+
when 'stream:features'
|
61
|
+
@features = @node.children
|
62
|
+
@state = :features
|
63
|
+
dispatch
|
64
|
+
|
65
|
+
when 'stream:error'
|
66
|
+
raise StreamError.new(@node)
|
67
|
+
|
68
|
+
else
|
69
|
+
dispatch
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Send data over the wire
|
76
|
+
def send(stanza)
|
77
|
+
#TODO Queue if not ready
|
78
|
+
LOG.debug "SENDING: (#{caller[1]}) #{stanza}"
|
79
|
+
send_data stanza.to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
def stopped?
|
83
|
+
@state == :stopped
|
84
|
+
end
|
85
|
+
|
86
|
+
def ready?
|
87
|
+
@state == :ready
|
88
|
+
end
|
89
|
+
|
90
|
+
def jid=(new_jid) # :nodoc:
|
91
|
+
LOG.debug "NEW JID: #{new_jid}"
|
92
|
+
new_jid = JID.new new_jid
|
93
|
+
@client.jid = new_jid
|
94
|
+
@jid = new_jid
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def dispatch
|
99
|
+
__send__ @state
|
100
|
+
end
|
101
|
+
|
102
|
+
def start
|
103
|
+
send <<-STREAM
|
104
|
+
<stream:stream
|
105
|
+
to='#{@to}'
|
106
|
+
xmlns='#{@namespace}'
|
107
|
+
xmlns:stream='http://etherx.jabber.org/streams'
|
108
|
+
version='#{@version}'
|
109
|
+
xml:lang='#{@lang}'
|
110
|
+
>
|
111
|
+
STREAM
|
112
|
+
end
|
113
|
+
|
114
|
+
def stop
|
115
|
+
send '</stream:stream>'
|
116
|
+
end
|
117
|
+
|
118
|
+
def stopped
|
119
|
+
start
|
120
|
+
end
|
121
|
+
|
122
|
+
def ready
|
123
|
+
@client.call @node.to_stanza
|
124
|
+
end
|
125
|
+
|
126
|
+
def features
|
127
|
+
feature = @features.first
|
128
|
+
LOG.debug "FEATURE: #{feature}"
|
129
|
+
@state = case feature ? feature['xmlns'] : nil
|
130
|
+
when 'urn:ietf:params:xml:ns:xmpp-tls' then :establish_tls
|
131
|
+
when 'urn:ietf:params:xml:ns:xmpp-sasl' then :authenticate_sasl
|
132
|
+
when 'urn:ietf:params:xml:ns:xmpp-bind' then :bind_resource
|
133
|
+
when 'urn:ietf:params:xml:ns:xmpp-session' then :establish_session
|
134
|
+
else :ready
|
135
|
+
end
|
136
|
+
|
137
|
+
dispatch unless ready?
|
138
|
+
end
|
139
|
+
|
140
|
+
def establish_tls
|
141
|
+
unless @tls
|
142
|
+
@tls = TLS.new self
|
143
|
+
@tls.success { LOG.debug "TLS: SUCCESS"; @tls = nil; start }
|
144
|
+
@tls.failure { LOG.debug "TLS: FAILURE"; stop }
|
145
|
+
@node = @features.shift
|
146
|
+
end
|
147
|
+
@tls.receive @node
|
148
|
+
end
|
149
|
+
|
150
|
+
def authenticate_sasl
|
151
|
+
unless @sasl
|
152
|
+
@sasl = SASL.new(self, @jid, @pass)
|
153
|
+
@sasl.success { LOG.debug "SASL SUCCESS"; @sasl = nil; start }
|
154
|
+
@sasl.failure { LOG.debug "SASL FAIL"; stop }
|
155
|
+
@node = @features.shift
|
156
|
+
end
|
157
|
+
@sasl.receive @node
|
158
|
+
end
|
159
|
+
|
160
|
+
def bind_resource
|
161
|
+
unless @resource
|
162
|
+
@resource = Resource.new self, @jid
|
163
|
+
@resource.success { |jid| LOG.debug "RESOURCE: SUCCESS"; @resource = nil; self.jid = jid; @state = :features; dispatch }
|
164
|
+
@resource.failure { LOG.debug "RESOURCE: FAILURE"; stop }
|
165
|
+
@node = @features.shift
|
166
|
+
end
|
167
|
+
@resource.receive @node
|
168
|
+
end
|
169
|
+
|
170
|
+
def establish_session
|
171
|
+
unless @session
|
172
|
+
@session = Session.new self, @to
|
173
|
+
@session.success { LOG.debug "SESSION: SUCCESS"; @session = nil; @client.stream_started(self); @state = :features; dispatch }
|
174
|
+
@session.failure { LOG.debug "SESSION: FAILURE"; stop }
|
175
|
+
@node = @features.shift
|
176
|
+
end
|
177
|
+
@session.receive @node
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|