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