blather 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -101,6 +101,21 @@ The different types of guards are:
101
101
  # Equivalent to !stanza.find('/iq/ns:pubsub', :ns => 'pubsub:namespace').empty?
102
102
  iq '/iq/ns:pubsub', :ns => 'pubsub:namespace'
103
103
 
104
+ === Filters
105
+
106
+ Blather provides before and after filters that work much the way regular handlers work. Filters come in a before and after
107
+ flavor. They're called in order of definition and can be guarded like handlers.
108
+
109
+ before { |s| "I'm run before any handler" }
110
+ before { |s| "I'm run next" }
111
+
112
+ before(:message) { |s| "I'm only run in front of message stanzas" }
113
+ before(nil, :id => 1) { |s| "I'll only be run when the stanza's ID == 1" }
114
+
115
+ # ... handlers
116
+
117
+ after { |s| "I'm run after everything" }
118
+
104
119
  == On the Command Line:
105
120
 
106
121
  Default usage is:
@@ -126,7 +141,7 @@ Command line options:
126
141
 
127
142
  Jeff Smick <sprsquish@gmail.com>
128
143
 
129
- = Contributors
144
+ === Contributors
130
145
 
131
146
  Nolan Darilek <nolan@thewordnerd.info>
132
147
 
data/examples/echo.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'blather/client'
4
- Blather.logger.level = Logger::DEBUG
4
+
5
5
  when_ready { puts "Connected ! send messages to #{jid.stripped}." }
6
6
 
7
7
  subscription :request? do |s|
data/examples/ping.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'blather/client'
2
+
3
+ setup 'echo@jabber.local/ping', 'echo'
4
+
5
+ status :from => Blather::JID.new('echo@jabber.local/pong') do |s|
6
+ say s.from, 'ping'
7
+ end
8
+
9
+ message :chat?, :body => 'pong' do |m|
10
+ say m.from, 'ping'
11
+ end
data/examples/pong.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'blather/client'
2
+
3
+ setup 'echo@jabber.local/pong', 'echo'
4
+ message :chat?, :body => 'ping' do |m|
5
+ say m.from, 'pong'
6
+ end
@@ -7,8 +7,8 @@ options = {}
7
7
  optparse = OptionParser.new do |opts|
8
8
  opts.banner = "Run with #{$0} [options] user@server/resource password [host] [port]"
9
9
 
10
- opts.on('-D', '--debug', 'Run in debug mode (you will see all XMPP communication)') do |debug|
11
- options[:debug] = debug
10
+ opts.on('-D', '--debug', 'Run in debug mode (you will see all XMPP communication)') do
11
+ options[:debug] = true
12
12
  end
13
13
 
14
14
  opts.on('-d', '--daemonize', 'Daemonize the process') do |daemonize|
@@ -60,10 +60,12 @@ at_exit do
60
60
  if options[:log]
61
61
  log = File.new(options[:log], 'a')
62
62
  log.sync = options[:debug]
63
- Blather.logger.level = Logger::DEBUG if options[:debug]
64
63
  $stdout.reopen log
65
64
  $stderr.reopen $stdout
66
65
  end
66
+
67
+ Blather.logger.level = Logger::DEBUG if options[:debug]
68
+
67
69
  trap(:INT) { EM.stop }
68
70
  trap(:TERM) { EM.stop }
69
71
  EM.run { client.run }
@@ -2,29 +2,67 @@ require File.join(File.dirname(__FILE__), *%w[.. .. blather])
2
2
 
3
3
  module Blather #:nodoc:
4
4
 
5
- class Client #:nodoc:
5
+ # = Blather Client
6
+ #
7
+ # Blather's Client class provides a set of helpers for working with common XMPP tasks such as setting up and starting
8
+ # the connection, settings status, registering and dispatching filters and handlers and roster management.
9
+ #
10
+ # Client can be used separately from the DSL if you'd like to implement your own DSL
11
+ # Here's the echo example using the client without the DSL:
12
+ #
13
+ # require 'blather/client/client'
14
+ # client = Client.setup 'echo@jabber.local', 'echo'
15
+ #
16
+ # client.register_handler(:ready) { puts "Connected ! send messages to #{client.jid.stripped}." }
17
+ #
18
+ # client.register_handler :subscription, :request? do |s|
19
+ # client.write s.approve!
20
+ # end
21
+ #
22
+ # client.register_handler :message, :chat?, :body => 'exit' do |m|
23
+ # client.write Blather::Stanza::Message.new(m.from, 'Exiting...')
24
+ # client.close
25
+ # end
26
+ #
27
+ # client.register_handler :message, :chat?, :body do |m|
28
+ # client.write Blather::Stanza::Message.new(m.from, "You sent: #{m.body}")
29
+ # end
30
+ #
31
+ class Client
6
32
  attr_reader :jid,
7
33
  :roster
8
34
 
9
- def initialize
35
+ ##
36
+ # Initialize and setup the client
37
+ # * +jid+ - the JID to login with
38
+ # * +password+ - password associated with the JID
39
+ # * +host+ - hostname or IP to connect to. If nil the stream will look one up based on the domain in the JID
40
+ # * +port+ - port to connect to
41
+ def self.setup(jid, password, host = nil, port = nil)
42
+ self.new.setup(jid, password, host, port)
43
+ end
44
+
45
+ def initialize # :nodoc:
10
46
  @state = :initializing
11
47
 
12
48
  @status = Stanza::Presence::Status.new
13
49
  @handlers = {}
14
50
  @tmp_handlers = {}
51
+ @filters = {:before => [], :after => []}
15
52
  @roster = Roster.new self
16
53
 
17
54
  setup_initial_handlers
18
55
  end
19
56
 
20
- def jid=(new_jid)
21
- @jid = JID.new new_jid
22
- end
23
-
57
+ ##
58
+ # Get the current status. Taken from the +state+ attribute of Status
24
59
  def status
25
60
  @status.state
26
61
  end
27
62
 
63
+ ##
64
+ # Set the status. Status can be set with either a single value or an array containing
65
+ # [state, message, to].
28
66
  def status=(state)
29
67
  state, msg, to = state
30
68
 
@@ -35,77 +73,121 @@ module Blather #:nodoc:
35
73
  write status
36
74
  end
37
75
 
38
- def setup?
39
- @setup.is_a? Array
40
- end
41
-
42
- def setup(jid, password, host = nil, port = nil)
43
- @jid = JID.new(jid)
44
- @setup = [@jid, password]
45
- @setup << host if host
46
- @setup << port if port
47
- self
48
- end
49
-
76
+ ##
77
+ # Start the connection.
50
78
  def run
51
79
  raise 'not setup!' unless setup?
52
80
  klass = @setup[0].node ? Blather::Stream::Client : Blather::Stream::Component
53
81
  @stream = klass.start self, *@setup
54
82
  end
55
83
 
84
+ ##
85
+ # Register a filter to be run before or after the handler chain is run.
86
+ # * +type+ - the type of filter. Must be +:before+ or +:after+
87
+ # * +guards+ - guards that should be checked before the filter is called
88
+ def register_filter(type, handler = nil, *guards, &filter)
89
+ raise "Invalid filter: #{type}. Must be :before or :after" unless [:before, :after].include?(type)
90
+ @filters[type] << [guards, handler, filter]
91
+ end
92
+
93
+ ##
94
+ # Register a temporary handler. Temporary handlers are based on the ID of the JID and live
95
+ # only until a stanza with said ID is received.
96
+ # * +id+ - the ID of the stanza that should be handled
56
97
  def register_tmp_handler(id, &handler)
57
98
  @tmp_handlers[id] = handler
58
99
  end
59
100
 
101
+ ##
102
+ # Register a handler
103
+ # * +type+ - the handler type. Should be registered in Stanza.handler_list. Blather will log a warning if it's not.
104
+ # * +guards+ - the list of guards that must be verified before the handler will be called
60
105
  def register_handler(type, *guards, &handler)
61
- check_guards guards
106
+ check_handler type, guards
62
107
  @handlers[type] ||= []
63
108
  @handlers[type] << [guards, handler]
64
109
  end
65
110
 
111
+ ##
112
+ # Write data to the stream
66
113
  def write(stanza)
67
114
  @stream.send(stanza) if @stream
68
115
  end
69
116
 
117
+ ##
118
+ # Helper that will create a temporary handler for the stanza being sent before writing it to the stream.
119
+ #
120
+ # client.write_with_handler(stanza) { |s| "handle stanza here" }
121
+ #
122
+ # is equivalent to:
123
+ #
124
+ # client.register_tmp_handler(stanza.id) { |s| "handle stanza here" }
125
+ # client.write stanza
70
126
  def write_with_handler(stanza, &handler)
71
127
  register_tmp_handler stanza.id, &handler
72
128
  write stanza
73
129
  end
74
130
 
75
- def post_init
76
- self.jid.node ? client_post_init : ready!
77
- end
78
-
131
+ ##
132
+ # Close the connection
79
133
  def close
80
134
  @stream.close_connection_after_writing
81
135
  end
82
136
 
83
- def unbind
137
+ def post_init # :nodoc:
138
+ self.jid.node ? client_post_init : ready!
139
+ end
140
+
141
+ def unbind # :nodoc:
84
142
  EM.stop if EM.reactor_running?
85
143
  end
86
144
 
87
- def receive_data(stanza)
88
- if handler = @tmp_handlers.delete(stanza.id)
89
- handler.call stanza
90
- else
91
- stanza.handler_heirarchy.each do |type|
92
- break if call_handler_for(type, stanza)# && (stanza.is_a?(BlatherError) || stanza.type == :iq)
93
- end
145
+ def receive_data(stanza) # :nodoc:
146
+ catch(:halt) do
147
+ run_filters :before, stanza
148
+ handle_stanza stanza
149
+ run_filters :after, stanza
94
150
  end
95
151
  end
96
152
 
153
+ def jid=(new_jid) # :nodoc :
154
+ @jid = JID.new new_jid
155
+ end
156
+
157
+ def setup? # :nodoc:
158
+ @setup.is_a? Array
159
+ end
160
+
161
+ def setup(jid, password, host = nil, port = nil) # :nodoc:
162
+ @jid = JID.new(jid)
163
+ @setup = [@jid, password]
164
+ @setup << host if host
165
+ @setup << port if port
166
+ self
167
+ end
168
+
97
169
  protected
98
- def setup_initial_handlers
170
+ def check_handler(type, guards)
171
+ Blather.logger.warn "Handler for type \"#{type}\" will never be called as it's not a registered type" unless current_handlers.include?(type)
172
+ check_guards guards
173
+ end
174
+
175
+ def current_handlers
176
+ [:ready] + Stanza.handler_list + BlatherError.handler_list
177
+ end
178
+
179
+ def setup_initial_handlers # :nodoc:
99
180
  register_handler :error do |err|
100
181
  raise err
101
182
  end
102
183
 
103
184
  register_handler :iq, :type => [:get, :set] do |iq|
104
- write(StanzaError.new(iq, 'service-unavailable', :cancel).to_node)
185
+ write StanzaError.new(iq, 'service-unavailable', :cancel).to_node
105
186
  end
106
187
 
107
188
  register_handler :status do |status|
108
189
  roster[status.from].status = status if roster[status.from]
190
+ nil
109
191
  end
110
192
 
111
193
  register_handler :roster do |node|
@@ -113,12 +195,12 @@ module Blather #:nodoc:
113
195
  end
114
196
  end
115
197
 
116
- def ready!
198
+ def ready! # :nodoc:
117
199
  @state = :ready
118
200
  call_handler_for :ready, nil
119
201
  end
120
202
 
121
- def client_post_init
203
+ def client_post_init # :nodoc:
122
204
  write_with_handler Stanza::Iq::Roster.new do |node|
123
205
  roster.process node
124
206
  write @status
@@ -126,25 +208,43 @@ module Blather #:nodoc:
126
208
  end
127
209
  end
128
210
 
129
- def call_handler_for(type, stanza)
130
- if @handlers[type]
131
- @handlers[type].find do |guards, handler|
132
- if guards.first.is_a?(String)
133
- unless (result = stanza.find(*guards)).empty?
134
- handler.call(stanza, result)
135
- end
136
- elsif !guarded?(guards, stanza)
137
- handler.call(stanza)
138
- end
211
+ def run_filters(type, stanza) # :nodoc:
212
+ @filters[type].each do |guards, handler, filter|
213
+ next if handler && !stanza.handler_heirarchy.include?(handler)
214
+ catch(:pass) { call_handler filter, guards, stanza }
215
+ end
216
+ end
217
+
218
+ def handle_stanza(stanza) # :nodoc:
219
+ if handler = @tmp_handlers.delete(stanza.id)
220
+ handler.call stanza
221
+ else
222
+ stanza.handler_heirarchy.each do |type|
223
+ break if call_handler_for(type, stanza)
139
224
  end
140
225
  end
141
226
  end
142
227
 
228
+ def call_handler_for(type, stanza) # :nodoc:
229
+ return unless handler = @handlers[type]
230
+ handler.find do |guards, handler|
231
+ catch(:pass) { call_handler handler, guards, stanza }
232
+ end
233
+ end
234
+
235
+ def call_handler(handler, guards, stanza) # :nodoc:
236
+ if guards.first.respond_to?(:to_str) && !(result = stanza.find(*guards)).empty?
237
+ handler.call(stanza, result)
238
+ elsif !guarded?(guards, stanza)
239
+ handler.call(stanza)
240
+ end
241
+ end
242
+
143
243
  ##
144
244
  # If any of the guards returns FALSE this returns true
145
245
  # the logic is reversed to allow short circuiting
146
246
  # (why would anyone want to loop over more values than necessary?)
147
- def guarded?(guards, stanza)
247
+ def guarded?(guards, stanza) # :nodoc:
148
248
  guards.find do |guard|
149
249
  case guard
150
250
  when Symbol
@@ -156,10 +256,9 @@ module Blather #:nodoc:
156
256
  # return FALSE unless any inequality is found
157
257
  guard.find do |method, test|
158
258
  value = stanza.__send__(method)
159
- case test
160
- when Regexp
161
- !value.to_s.match(test)
162
- when Array
259
+ if test.class.respond_to?(:last_match)
260
+ !(test =~ value)
261
+ elsif test.is_a?(Array)
163
262
  !test.include? value
164
263
  else
165
264
  test != value
@@ -171,7 +270,7 @@ module Blather #:nodoc:
171
270
  end
172
271
  end
173
272
 
174
- def check_guards(guards)
273
+ def check_guards(guards) # :nodoc:
175
274
  guards.each do |guard|
176
275
  case guard
177
276
  when Array
@@ -38,6 +38,18 @@ module Blather
38
38
  client.close
39
39
  end
40
40
 
41
+ ##
42
+ # Setup a before filter
43
+ def before(handler = nil, *guards, &block)
44
+ client.register_filter :before, handler, *guards, &block
45
+ end
46
+
47
+ ##
48
+ # Setup an after filter
49
+ def after(handler = nil, *guards, &block)
50
+ client.register_filter :after, handler, *guards, &block
51
+ end
52
+
41
53
  ##
42
54
  # Set handler for a stanza type
43
55
  def handle(stanza_type, *guards, &block)
@@ -82,6 +94,18 @@ module Blather
82
94
  client.jid
83
95
  end
84
96
 
97
+ ##
98
+ # Halt the handler chain
99
+ def halt
100
+ throw :halt
101
+ end
102
+
103
+ ##
104
+ # Pass responsibility to the next handler
105
+ def pass
106
+ throw :pass
107
+ end
108
+
85
109
  ##
86
110
  # Request items or info from an entity
87
111
  # discover (items|info), [jid], [node] do |response|
@@ -27,7 +27,7 @@ module DSL
27
27
  ##
28
28
  # Discover Nodes
29
29
  # Yields a list of DiscoItem::Item objects
30
- # +path+ is the node's path. Default is '/'
30
+ # * +path+ is the node's path. Default is '/'
31
31
  def nodes(path = nil, host = nil, &callback)
32
32
  path ||= '/'
33
33
  stanza = Stanza::DiscoItems.new(:get, path)
@@ -38,7 +38,7 @@ module DSL
38
38
  ##
39
39
  # Discover node information
40
40
  # Yields a DiscoInfo node
41
- # +path+ is the node's path
41
+ # * +path+ is the node's path
42
42
  def node(path, host = nil, &callback)
43
43
  stanza = Stanza::DiscoInfo.new(:get, path)
44
44
  stanza.to = send_to(host)
@@ -47,9 +47,9 @@ module DSL
47
47
 
48
48
  ##
49
49
  # Retrieve items for a node
50
- # +path+ is the node's path
51
- # +list+ can be an array of items to retrieve
52
- # +max+ can be the maximum number of items to return
50
+ # * +path+ is the node's path
51
+ # * +list+ can be an array of items to retrieve
52
+ # * +max+ can be the maximum number of items to return
53
53
  def items(path, list = [], max = nil, host = nil, &callback)
54
54
  request Stanza::PubSub::Items.request(send_to(host), path, list, max), :items, callback
55
55
  end
@@ -57,8 +57,8 @@ module DSL
57
57
  ##
58
58
  # Subscribe to a node
59
59
  # Yields the resulting Subscription object
60
- # +node+ is the node to subscribe to
61
- # +jid+ is the jid that should be used. Defaults to the stripped current JID
60
+ # * +node+ is the node to subscribe to
61
+ # * +jid+ is the jid that should be used. Defaults to the stripped current JID
62
62
  def subscribe(node, jid = nil, host = nil)
63
63
  jid ||= DSL.client.jid.stripped
64
64
  request(Stanza::PubSub::Subscribe.new(:set, send_to(host), node, jid)) { |n| yield n if block_given? }
@@ -67,8 +67,8 @@ module DSL
67
67
  ##
68
68
  # Unsubscribe from a node
69
69
  # Yields the resulting Unsubscribe object
70
- # +node+ is the node to subscribe to
71
- # +jid+ is the jid that should be used. Defaults to the stripped current JID
70
+ # * +node+ is the node to subscribe to
71
+ # * +jid+ is the jid that should be used. Defaults to the stripped current JID
72
72
  def unsubscribe(node, jid = nil, host = nil)
73
73
  jid ||= DSL.client.jid.stripped
74
74
  request(Stanza::PubSub::Unsubscribe.new(:set, send_to(host), node, jid)) { |n| yield n if block_given? }
@@ -77,8 +77,8 @@ module DSL
77
77
  ##
78
78
  # Publish an item to a node
79
79
  # Yields the resulting Publish node
80
- # +node+ is the node to publish to
81
- # +payload+ is the payload to send (see Blather::Stanza::PubSub::Publish for details)
80
+ # * +node+ is the node to publish to
81
+ # * +payload+ is the payload to send (see Blather::Stanza::PubSub::Publish for details)
82
82
  def publish(node, payload, host = nil)
83
83
  request(Stanza::PubSub::Publish.new(send_to(host), node, :set, payload)) { |n| yield n if block_given? }
84
84
  end
@@ -86,8 +86,8 @@ module DSL
86
86
  ##
87
87
  # Delete items from a node
88
88
  # Yields the resulting node
89
- # +node+ is the node to retract items from
90
- # +ids+ is a list of ids to retract. This can also be a single id
89
+ # * +node+ is the node to retract items from
90
+ # * +ids+ is a list of ids to retract. This can also be a single id
91
91
  def retract(node, ids = [], host = nil)
92
92
  request(Stanza::PubSub::Retract.new(send_to(host), node, :set, ids)) { |n| yield n if block_given? }
93
93
  end
@@ -96,7 +96,7 @@ module DSL
96
96
  # Create a node
97
97
  # Yields the resulting node
98
98
  # This does not (yet) handle configuration
99
- # +node+ is the node to create
99
+ # * +node+ is the node to create
100
100
  def create(node, host = nil)
101
101
  request(Stanza::PubSub::Create.new(:set, send_to(host), node)) { |n| yield n if block_given? }
102
102
  end
@@ -104,7 +104,7 @@ module DSL
104
104
  ##
105
105
  # Purge all node items
106
106
  # Yields the resulting node
107
- # +node+ is the node to purge
107
+ # * +node+ is the node to purge
108
108
  def purge(node, host = nil)
109
109
  request(Stanza::PubSubOwner::Purge.new(:set, send_to(host), node)) { |n| yield n if block_given? }
110
110
  end
@@ -112,7 +112,7 @@ module DSL
112
112
  ##
113
113
  # Delete a node
114
114
  # Yields the resulting node
115
- # +node+ is the node to delete
115
+ # * +node+ is the node to delete
116
116
  def delete(node, host = nil)
117
117
  request(Stanza::PubSubOwner::Delete.new(:set, send_to(host), node)) { |n| yield n if block_given? }
118
118
  end