butler 1.8.3 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/CHANGELOG.txt +293 -37
  2. data/README.txt +10 -0
  3. data/Rakefile +24 -13
  4. data/bin/botcontrol +6 -5
  5. data/data/butler/dialogs/create.rb +21 -6
  6. data/data/butler/dialogs/create_config.rb +5 -2
  7. data/data/butler/dialogs/en/create.yaml +6 -3
  8. data/data/butler/dialogs/en/create_config.yaml +1 -0
  9. data/data/butler/dialogs/en/quickcreate.yaml +1 -1
  10. data/data/butler/dialogs/en/sync.yaml +7 -0
  11. data/data/butler/dialogs/en/username.yaml +2 -0
  12. data/data/butler/dialogs/quickcreate.rb +6 -2
  13. data/data/butler/dialogs/sync.rb +83 -0
  14. data/data/butler/dialogs/username.rb +1 -0
  15. data/data/butler/plugins/core/ping.rb +22 -0
  16. data/data/butler/plugins/core/remote.rb +38 -0
  17. data/data/butler/plugins/dev/eval.rb +6 -4
  18. data/data/butler/plugins/dev/onhandlers.rb +93 -0
  19. data/data/butler/plugins/dev/rawlog.rb +109 -45
  20. data/data/butler/plugins/games/countdown.rb +23 -14
  21. data/data/butler/plugins/games/eightball.rb +21 -13
  22. data/data/butler/plugins/games/roll.rb +12 -12
  23. data/data/butler/plugins/irc/join.rb +2 -2
  24. data/data/butler/plugins/irc/notice.rb +10 -10
  25. data/data/butler/plugins/irc/part.rb +12 -12
  26. data/data/butler/plugins/irc/privmsg.rb +10 -10
  27. data/data/butler/plugins/irc/quit.rb +12 -12
  28. data/data/butler/plugins/operator/devoice.rb +1 -1
  29. data/data/butler/plugins/public/help.rb +10 -4
  30. data/data/butler/plugins/{util → service}/calculator.rb +0 -0
  31. data/data/butler/plugins/service/define.rb +16 -13
  32. data/data/butler/plugins/service/log.rb +85 -0
  33. data/data/butler/plugins/service/seen.rb +64 -0
  34. data/data/butler/plugins/service/svn.rb +6 -5
  35. data/data/butler/plugins/util/load.rb +3 -1
  36. data/data/butler/services/org.rubyforge.butler/calculator/1/service.rb +96 -0
  37. data/data/butler/services/org.rubyforge.butler/log/1/service.rb +148 -68
  38. data/lib/access/admin.rb +5 -0
  39. data/lib/blank.rb +32 -0
  40. data/lib/butler.rb +4 -4
  41. data/lib/butler/bot.rb +118 -33
  42. data/lib/butler/control.rb +5 -4
  43. data/lib/butler/debuglog.rb +12 -4
  44. data/lib/butler/dialog.rb +1 -1
  45. data/lib/butler/initialvalues.rb +1 -1
  46. data/lib/butler/irc/client.rb +31 -12
  47. data/lib/butler/irc/message.rb +32 -13
  48. data/lib/butler/irc/parser.rb +67 -30
  49. data/lib/butler/irc/parser/{commands.rb → command.rb} +0 -38
  50. data/lib/butler/irc/parser/generic.rb +9 -12
  51. data/lib/butler/irc/parser/rfc2812.rb +40 -2
  52. data/lib/butler/irc/socket.rb +66 -41
  53. data/lib/butler/irc/string.rb +1 -5
  54. data/lib/butler/plugin.rb +56 -23
  55. data/lib/butler/plugin/configproxy.rb +1 -0
  56. data/lib/butler/plugin/more.rb +2 -2
  57. data/lib/butler/plugins.rb +7 -1
  58. data/lib/butler/remote/connection.rb +113 -0
  59. data/lib/butler/remote/message.rb +157 -0
  60. data/lib/butler/remote/server.rb +85 -0
  61. data/lib/butler/remote/user.rb +46 -0
  62. data/lib/butler/service.rb +2 -1
  63. data/lib/butler/services.rb +2 -2
  64. data/lib/butler/version.rb +2 -2
  65. data/lib/configuration.rb +13 -16
  66. data/lib/ostructfixed.rb +0 -6
  67. data/lib/ruby/array/random.rb +2 -1
  68. data/lib/scriptfile.rb +63 -14
  69. data/lib/timingoutresource.rb +54 -0
  70. data/test/test_scriptfile.rb +51 -0
  71. metadata +63 -61
  72. data/data/butler/dialogs/en/sync_plugins.yaml +0 -3
  73. data/data/butler/dialogs/sync_plugins.rb +0 -30
  74. data/data/butler/services/org.rubyforge.butler/calculator/1/calculator.rb +0 -68
  75. data/lib/log/splitter.rb +0 -30
  76. data/test/test_access/privilege/banners.statistics.yaml +0 -3
  77. data/test/test_access/privilege/banners.yaml +0 -3
  78. data/test/test_access/privilege/news.create.yaml +0 -3
  79. data/test/test_access/privilege/news.delete.yaml +0 -3
  80. data/test/test_access/privilege/news.edit.yaml +0 -3
  81. data/test/test_access/privilege/news.read.yaml +0 -3
  82. data/test/test_access/privilege/news.yaml +0 -3
  83. data/test/test_access/privilege/paid_content.yaml +0 -3
  84. data/test/test_access/privilege/statistics.ftp.yaml +0 -3
  85. data/test/test_access/privilege/statistics.web.yaml +0 -3
  86. data/test/test_access/privilege/statistics.yaml +0 -3
  87. data/test/test_access/role/chiefeditor.yaml +0 -7
  88. data/test/test_access/role/editor.yaml +0 -9
  89. data/test/test_access/user/test.yaml +0 -12
@@ -7,9 +7,14 @@
7
7
 
8
8
 
9
9
  class Access
10
+
10
11
  # Extend Admin users with this plugin, speeds up privileged?
11
12
  # by statically returning true and not performing any lookup.
12
13
  module Admin
14
+ # Admin is admin, of course
15
+ def admin?
16
+ true
17
+ end
13
18
 
14
19
  # admins have all privileges
15
20
  def privileged?(*args)
@@ -0,0 +1,32 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ module Kernel
10
+ # returns a blank class with only __* methods and those mentioned in
11
+ # leave_methods argument
12
+ # be aware that e.g. all Kernel methods are gone too, such as raise
13
+ # you can still use it via e.g. Kernel.raise or use leave_methods
14
+ # v0.1.1
15
+ def Blank(*leave_methods)
16
+ @_blank_classes ||= {}
17
+ leave_methods.push("__send__", "__id__", "initialize")
18
+ leave_methods.flatten!
19
+ leave_methods.uniq!
20
+ leave_methods.sort!
21
+ unless @_blank_classes[leave_methods] then
22
+ slate = Class.new
23
+ (slate.instance_methods+slate.private_instance_methods+slate.protected_instance_methods).each { |m|
24
+ slate.send(:undef_method, m) unless leave_methods.include?(m)
25
+ }
26
+ @_blank_classes[leave_methods] = slate
27
+ end
28
+ @_blank_classes[leave_methods]
29
+ end
30
+ end
31
+
32
+ Blank = Blank() unless defined? Blank
@@ -35,7 +35,7 @@ class Butler
35
35
  # -:daemonize => whether this script should become a deamon or not [true]
36
36
  # -...
37
37
  def start(path, botname, opts={}, &block)
38
- Thread.abort_on_exception = true # if $DEBUG # FIXME
38
+ Thread.abort_on_exception = true if $DEBUG
39
39
  path ||= @path
40
40
  butler = nil
41
41
 
@@ -55,8 +55,8 @@ class Butler
55
55
  File.write(pidfile, $$)
56
56
  butler = Bot.new(path, botname, opts)
57
57
  path = butler.path
58
- $stderr = File.open(path.log+"/error.log", "w")
59
- $stdout = File.open(path.log+"/out.log", "w")
58
+ $stderr = File.open(path.log+"/error.log", "a")
59
+ $stdout = File.open(path.log+"/out.log", "a")
60
60
  trap("SIGHUP") { butler.quit }
61
61
  butler.output_to_logfiles
62
62
  butler.plugins.load_all
@@ -93,7 +93,7 @@ class Butler
93
93
  exception(e)
94
94
  ensure
95
95
  File.delete(pidfile) if pidfile and File.exist?(pidfile)
96
- butler.quit if butler #and butler.logged_in? # FIXME, make the commented code happen
96
+ butler.quit if butler and butler.connected?
97
97
  info("Terminated")
98
98
  end
99
99
 
@@ -12,6 +12,10 @@ require 'butler/debuglog'
12
12
  require 'configuration'
13
13
  require 'log'
14
14
  require 'butler/plugins'
15
+ require 'butler/remote/connection'
16
+ require 'butler/remote/message'
17
+ require 'butler/remote/server'
18
+ require 'butler/remote/user'
15
19
  require 'butler/services'
16
20
  require 'butler/session'
17
21
  require 'ruby/string/arguments'
@@ -27,16 +31,32 @@ class Butler
27
31
  class Bot < IRC::Client
28
32
  include Log::Comfort
29
33
 
34
+ # The access framework for this bot. An instance of Access.
30
35
  attr_reader :access
31
- attr_reader :base
36
+
37
+ # This bot instances configuration. An instance of Configuration.
32
38
  attr_reader :config
39
+
40
+ # The logging device, per default nil
33
41
  attr_reader :logger
42
+
43
+ # An OpenStruct with all important paths of this bot instance.
34
44
  attr_reader :path
45
+
46
+ # Services butler offers, instance of Butler::Plugins
35
47
  attr_reader :plugins
48
+
49
+ # Remote connections server, instance of Butler::Remote::Server
50
+ attr_reader :remote
51
+
52
+ # Scheduler to hook up timed events, instance of Scheduler
53
+ # For plugins and services, please look up the class methods for
54
+ # Butler::Plugin and Butler::Service, they are to be prefered.
36
55
  attr_reader :scheduler
56
+
57
+ # Services butler offers, instance of Butler::Services
37
58
  attr_reader :services
38
59
 
39
- # FIXME, raise if selftest fails
40
60
  def initialize(path, name, opts={})
41
61
  path ||= Butler.path
42
62
  @irc = nil # early inspects
@@ -55,6 +75,7 @@ class Butler
55
75
  $LOAD_PATH.unshift(@path.lib)
56
76
  require 'butlerinit' if File.exist?(@path.lib+"/butlerinit.rb")
57
77
  @logger = $stderr
78
+ @remote = Remote::Server.new(self)
58
79
  @config = Configuration.new(@base+'/config')
59
80
  @scheduler = Scheduler.new
60
81
  @access = Access.new(
@@ -89,8 +110,10 @@ class Butler
89
110
 
90
111
  subscribe(:PRIVMSG, -10, &method(:invoke_commands))
91
112
  subscribe(:NOTICE, -10, &method(:invoke_commands))
92
-
93
- selftest
113
+ end
114
+
115
+ def connected?
116
+ @irc.connected?
94
117
  end
95
118
 
96
119
  def invoke_commands(listener, message)
@@ -144,38 +167,51 @@ class Butler
144
167
 
145
168
  # returns the rest of the sequence if it was an invocation, nil else
146
169
  def invocation(sequence)
147
- @myself && sequence[/^(?:!?#{@myself.nick}[:;,])\s+/i]
148
- end
149
-
150
- # runs a diagnose, will collect all wrongs and raise if it finds any
151
- def selftest
152
-
170
+ (@myself && sequence[/^(?:!?#{@myself.nick}[:;,])\s+/i]) || sequence[/^#{@config['invocation']}/i]
153
171
  end
154
172
 
155
173
  def login
156
174
  @access.default_user = @access["default_user"]
157
175
  @access.default_user.login
158
- p @access.default_user
159
176
 
160
177
  nick = @config["connections/main/nick"]
161
178
  pass = @config["connections/main/password"]
162
- super(
163
- nick,
164
- @config["connections/main/user"],
165
- @config["connections/main/real"]
166
- )
179
+ begin
180
+ super(
181
+ nick,
182
+ @config["connections/main/user"],
183
+ @config["connections/main/real"],
184
+ @config["connections/main/serverpass"]
185
+ )
186
+ rescue Errno::ECONNREFUSED
187
+ if @reconnect[1].zero? then
188
+ warn("Connection was refused")
189
+ return false
190
+ else
191
+ warn("Connection was refused, trying again in #{@reconnect.first} seconds")
192
+ @reconnect[1] -= 1
193
+ sleep(@reconnect[0])
194
+ retry
195
+ end
196
+ end
167
197
  info("Logged in")
168
- if pass && @myself.nick.same_nick?(nick) then
169
- @irc.ghost(nick, pass)
170
- sleep(1) # FIXME, do a wait_for or similar
171
- @irc.nick(nick)
198
+ if pass && !@parser.same_nick?(@myself.nick, nick) then
199
+ if wait_for(:NOTICE, 60, :prepare => proc { @irc.ghost(nick, pass) } ) { |m|
200
+ m.from && m.from.nick =~ /nickserv/i && m.text =~ /has been killed/
201
+ } then
202
+ @irc.nick(nick)
203
+ else
204
+ warn("Could not ghost the user already occupying my nick '#{nick}'")
205
+ end
172
206
  end
173
207
  @irc.identify(pass) if pass
174
208
  join(*@config["connections/main/channels"])
175
209
  plugins_dispatch(:on_login, "main")
210
+ true
176
211
  end
177
212
 
178
213
  def on_disconnect(reason)
214
+ info("Disconnected due to #{reason}")
179
215
  return if reason == :quit
180
216
  plugins_dispatch(:on_disconnect, reason)
181
217
  unless @reconnect[1].zero? then
@@ -254,9 +290,10 @@ class Butler
254
290
  alias butler_initialize initialize unless method_defined? :butler_initialize
255
291
  def initialize(*args, &block)
256
292
  butler_initialize(*args, &block)
257
- @language = nil
258
- @invocation = nil
259
- @arguments = nil
293
+ @language = nil
294
+ @invocation = nil
295
+ @arguments = nil
296
+ @formatted_arguments = nil
260
297
  @post_arguments = nil
261
298
  end
262
299
 
@@ -264,23 +301,71 @@ class Butler
264
301
  # e.g. 'Hallo "this is token2" "and this \"token\" is token3"'
265
302
  # would be parsed into: ["hallo", "this is token2", "and this \"token\" is token3"]
266
303
  def arguments
267
- return [] unless text # only messages with text can be tokenized to parameters
268
- @arguments ||= begin # don't do double-work
269
- text[@invocation.length..-1].arguments
270
- rescue String::SingleQuoteException => ex
271
- ex.pre+[ex.post.join(" ")]
272
- end
304
+ # only messages with text can be tokenized to parameters
305
+ raise NoMethodError, "Message has no text, can't generate arguments" unless text
306
+ # cached
307
+ return @arguments if @arguments
308
+ # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
309
+ args = text[@invocation.length..-1].mirc_stripped.scan(/"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
310
+ # sanitize the last argument in case it's not correctly quoted
311
+ args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
312
+ # unescape data and remove quotes
313
+ @arguments = args.map { |arg|
314
+ case arg[0,1]
315
+ when "'": arg[1..-2].gsub(/\\(['\\])/, '\1')
316
+ when '"': arg[1..-2].gsub(/\\(["\\])/, '\1')
317
+ else arg.gsub(/\\(["'\\ ])/, '\1')
318
+ end
319
+ }
273
320
  end
274
-
275
- # FIXME due to it's current working, post_arguments will strip formatting
321
+
322
+ # like #arguments, but doesn't strip color information
323
+ def formatted_arguments
324
+ # only messages with text can be tokenized to parameters
325
+ raise NoMethodError, "Message has no text, can't generate arguments" unless text
326
+ # cached
327
+ return @arguments if @arguments
328
+ # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
329
+ args = text[@invocation.length..-1].scan(/"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
330
+ # sanitize the last argument in case it's not correctly quoted
331
+ args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
332
+ # unescape data and remove quotes
333
+ @formatted_arguments = args.map { |arg|
334
+ case arg[0,1]
335
+ when "'": arg[1..-2].gsub(/\\(['\\])/, '\1')
336
+ when '"': arg[1..-2].gsub(/\\(["\\])/, '\1')
337
+ else arg.gsub(/\\(["'\\ ])/, '\1')
338
+ end
339
+ }
340
+ end
341
+
276
342
  # Returns the rest of message text after argument
277
343
  # == Synopsis
278
344
  # "foo bar baz".post_arguments[0] # => "foo bar baz"
279
345
  # "foo bar baz".post_arguments[1] # => "bar baz"
280
346
  # "foo bar baz".post_arguments[2] # => "baz"
281
347
  def post_arguments
282
- return [] unless text # only messages with text can be tokenized to parameters
283
- @post_arguments ||= text[@invocation.length..-1].post_arguments # don't do double-work
348
+ # only messages with text can be tokenized to parameters
349
+ raise NoMethodError, "Message has no text, can't generate arguments" unless text
350
+ # cached
351
+ return @post_arguments if @post_arguments
352
+ # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
353
+ args = text[@invocation.length..-1].scan(/\s+|"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
354
+ # sanitize arguments
355
+ args.shift if args.first =~ /^\s+$/
356
+ args.pop if args.last =~ /^\s+$/
357
+ args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
358
+ # unescape data and remove quotes
359
+ args.map! { |arg|
360
+ case arg[0,1]
361
+ when "'": arg.gsub(/\\(['\\])/, '\1')
362
+ when '"': arg.gsub(/\\(["\\])/, '\1')
363
+ else arg.gsub(/\\(["'\\ ])/, '\1')
364
+ end
365
+ }
366
+ @post_arguments = []
367
+ (0...args.length).step(2) { |i| @post_arguments << args[i..-1].join('') }
368
+ @post_arguments
284
369
  end
285
370
  end # IRC::Message
286
371
  end # Butler
@@ -13,8 +13,8 @@ require 'ruby/file/write'
13
13
  class Butler
14
14
  IsGem = true # REPLACE: IsGem = false
15
15
 
16
- # this module provides services to botcontrol
17
- # for gems, this module uses the Gem module to get information on the where-
16
+ # This Class provides services to botcontrol
17
+ # For gems, this class uses the Gem module to get information on the where-
18
18
  # abouts of data. If installed using rake from tarball, it is hard-coded.
19
19
  class Control
20
20
  attr_reader :dialog
@@ -38,7 +38,7 @@ class Butler
38
38
 
39
39
  def user(name=nil)
40
40
  # use Etc.getlogin?
41
- name || ENV['SUDO_USER'] || ENV['USER'] || @dialog.discuss("username", false)[:name]
41
+ name || ENV['SUDO_USER'] || ENV['USER'] || @dialog.discuss("username", false)[:username]
42
42
  end
43
43
 
44
44
  def configured?(user=nil)
@@ -72,7 +72,8 @@ class Butler
72
72
  def user_config(user=nil)
73
73
  path = @config.users[user(user)]
74
74
  user_config = OpenStruct.new(YAML.load_file(path))
75
- user_config.plugin_repository = @path.plugins
75
+ user_config.plugin_repository = @path.plugins
76
+ user_config.service_repository = @path.services
76
77
  user_config
77
78
  end
78
79
 
@@ -1,16 +1,24 @@
1
1
  class Butler
2
+
3
+ # Extend an object with debug-log and it will print all read/write actions to @raw_log (defaulting
4
+ # to $stderr)
2
5
  module DebugLog
6
+ def self.extended(obj) # :nodoc:
7
+ obj.raw_log = $stderr
8
+ end
9
+
10
+ # The object to call printf on to log read/writes.
3
11
  attr_accessor :raw_log
4
12
 
5
- def read
13
+ def read # :nodoc:
6
14
  data = super
7
- @raw_log.printf("%s < %p\n", Time.now, data)
15
+ @raw_log.printf("<- %s %p\n", Time.now, data)
8
16
  data
9
17
  end
10
18
 
11
- def write(data)
19
+ def write(data) # :nodoc:
12
20
  rv = super
13
- @raw_log.printf("%s > %p\n", Time.now, data)
21
+ @raw_log.printf("-> %s %p\n", Time.now, data)
14
22
  rv
15
23
  end
16
24
  end
@@ -29,7 +29,7 @@ class Butler
29
29
 
30
30
  def initialize(plugin)
31
31
  @plugin = plugin
32
- @plugin.butler.subscribe(:PRIVMSG, 20) { |listener, message|
32
+ @plugin.butler.subscribe(:PRIVMSG, 0, true) { |listener, message|
33
33
  if message.private? then
34
34
  postpone
35
35
  dialog(listener, message)
@@ -22,7 +22,7 @@ class Butler
22
22
  BOTPATH/config
23
23
  connections
24
24
  channels
25
- plugin
25
+ plugins
26
26
  ]
27
27
  EmptyConfig = {
28
28
  'remote' => {
@@ -121,7 +121,18 @@ class Butler
121
121
 
122
122
  subscribe(:PING, 100) { |listener, message| @irc.pong(message.pong) }
123
123
  subscribe(:ISUPPORT) { |listener, message|
124
- # @parser. ...
124
+ isupport = {}
125
+ @parser.isupport.to_hash.each { |key, value|
126
+ isupport[key.to_s] = value
127
+ }
128
+ message.support.each { |key,value|
129
+ isupport[key.downcase] = value
130
+ }
131
+ if message.support.has_key?("PREFIX") then
132
+ isupport["prefixes"] = message.support["PREFIX"].values.join('')
133
+ end
134
+ @parser.reset(isupport)
135
+ @irc.write_with_eol("CAPAB IDENTIFY-MSG") if @parser.isupport.capab and !@parser.msg_identify
125
136
  }
126
137
  subscribe(:RPL_IDENTIFY_MSG) { |listener, message|
127
138
  @parser.msg_identify = true
@@ -158,10 +169,12 @@ class Butler
158
169
  # (optionally passing a test given as block) is received,
159
170
  # returns the message received that matches.
160
171
  # returns nil if it times out before a match
161
- def wait_for(symbol, timeout=nil, &test)
172
+ def wait_for(symbol, timeout=nil, opt={}, &test)
173
+ listener = nil
162
174
  timeout(timeout) {
163
175
  queue = Queue.new
164
176
  listener = subscribe(symbol) { |l, m| queue.push(m) }
177
+ opt[:prepare].call if opt.has_key?(:prepare)
165
178
  begin
166
179
  message = queue.shift
167
180
  end until block_given? ? yield(message) : true
@@ -190,7 +203,7 @@ class Butler
190
203
  end
191
204
 
192
205
  # login under nick, user, real
193
- def login(nick, user, real)
206
+ def login(nick, user, real, serverpass=nil)
194
207
  queue, nick_change = nil, nil
195
208
  number = 0
196
209
  timeout(@timeout[:login]) {
@@ -205,7 +218,7 @@ class Butler
205
218
  @myself.nick = change
206
219
  }
207
220
  @thread_read = Thread.new(&method(:thread_read))
208
- @irc.login(nick, user, real)
221
+ @irc.login(nick, user, real, serverpass)
209
222
  queue.shift
210
223
  }
211
224
  true
@@ -287,18 +300,20 @@ class Butler
287
300
  # normally this thread is alive as long as the client is connected to the server
288
301
  def thread_read
289
302
  while message = @irc.read; process(message); end
303
+ @irc.close
290
304
  on_disconnect(:disconnect)
291
305
  rescue Terminate => e # got the termination signal
292
306
  info("Terminating read thread")
307
+ @irc.close
293
308
  on_disconnect(:quit)
294
309
  rescue Errno::EPIPE => error # irc server closed connection
295
310
  exception(error)
311
+ @irc.close
296
312
  on_disconnect(:disconnect)
297
313
  rescue Exception => error
298
314
  exception(error)
299
- on_disconnect(:error)
300
- ensure
301
315
  @irc.close
316
+ on_disconnect(:error)
302
317
  end
303
318
 
304
319
  # Called on disconnects, see disconnect_callback attribute documentation
@@ -323,17 +338,21 @@ class Butler
323
338
 
324
339
  # process a Butler::IRC::Message, normally fed from thread_read.
325
340
  def process(message)
326
- message = @parser.server_message(message)
327
- message.transcode!(@channel_charset[message.channel], @client_charset)
328
- @listener.synchronized_each_for(message.symbol) { |listener|
329
- listener.call(message)
330
- }
341
+ dispatch(@parser.server_message(message))
331
342
  rescue Terminate, Errno::EPIPE => error
332
343
  raise error # on these errors we got to get out of the loop
333
344
  rescue Exception => error
334
345
  exception(error) # those errors are logged, reading goes on
335
346
  end
336
347
 
348
+ # dispatch a message
349
+ def dispatch(message)
350
+ message.transcode!(@channel_charset[message.channel], @client_charset)
351
+ @listener.synchronized_each_for(message.symbol) { |listener|
352
+ listener.call(message)
353
+ }
354
+ end
355
+
337
356
  def inspect # :nodoc:
338
357
  "#<%s:0x%08x irc=%s>" % [
339
358
  self.class,
@@ -343,4 +362,4 @@ class Butler
343
362
  end
344
363
  end # Client
345
364
  end # IRC
346
- end # Butler
365
+ end # Butler