butler 1.8.1 → 1.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/CHANGELOG.txt +212 -0
  2. data/{README → README.txt} +0 -0
  3. data/Rakefile +16 -11
  4. data/bin/botcontrol +35 -14
  5. data/data/butler/dialogs/create.rb +29 -40
  6. data/data/butler/dialogs/create_config.rb +1 -1
  7. data/data/butler/dialogs/dir.rb +13 -0
  8. data/data/butler/dialogs/en/create.yaml +24 -10
  9. data/data/butler/dialogs/en/dir.yaml +5 -0
  10. data/data/butler/dialogs/en/help.yaml +28 -11
  11. data/data/butler/dialogs/en/quickcreate.yaml +14 -0
  12. data/data/butler/dialogs/help.rb +16 -4
  13. data/data/butler/dialogs/quickcreate.rb +49 -0
  14. data/data/butler/plugins/core/access.rb +211 -0
  15. data/data/butler/plugins/core/logout.rb +11 -11
  16. data/data/butler/plugins/core/plugins.rb +23 -41
  17. data/data/butler/plugins/dev/bleakhouse.rb +46 -0
  18. data/data/butler/plugins/games/roll.rb +1 -1
  19. data/data/butler/plugins/operator/deop.rb +15 -20
  20. data/data/butler/plugins/operator/devoice.rb +14 -20
  21. data/data/butler/plugins/operator/limit.rb +56 -21
  22. data/data/butler/plugins/operator/op.rb +15 -20
  23. data/data/butler/plugins/operator/voice.rb +15 -20
  24. data/data/butler/plugins/service/define.rb +3 -3
  25. data/data/butler/plugins/service/more.rb +40 -0
  26. data/data/butler/plugins/util/cycle.rb +1 -1
  27. data/data/butler/plugins/util/load.rb +5 -5
  28. data/data/butler/plugins/util/pong.rb +3 -2
  29. data/lib/access/privilege.rb +17 -0
  30. data/lib/access/role.rb +33 -2
  31. data/lib/access/savable.rb +6 -0
  32. data/lib/access/yamlbase.rb +1 -2
  33. data/lib/butler/bot.rb +40 -7
  34. data/lib/butler/debuglog.rb +17 -0
  35. data/lib/butler/dialog.rb +1 -1
  36. data/lib/butler/irc/{channels.rb → channellist.rb} +2 -2
  37. data/lib/butler/irc/client.rb +60 -79
  38. data/lib/butler/irc/client/filter.rb +12 -0
  39. data/lib/butler/irc/client/listener.rb +55 -0
  40. data/lib/butler/irc/client/listenerlist.rb +69 -0
  41. data/lib/butler/irc/hostmask.rb +31 -16
  42. data/lib/butler/irc/message.rb +3 -3
  43. data/lib/butler/irc/parser.rb +2 -2
  44. data/lib/butler/irc/parser/rfc2812.rb +2 -6
  45. data/lib/butler/irc/socket.rb +12 -6
  46. data/lib/butler/irc/string.rb +4 -0
  47. data/lib/butler/irc/user.rb +0 -6
  48. data/lib/butler/irc/{users.rb → userlist.rb} +2 -2
  49. data/lib/butler/irc/whois.rb +6 -0
  50. data/lib/butler/plugin.rb +48 -14
  51. data/lib/butler/plugin/configproxy.rb +20 -0
  52. data/lib/butler/plugin/mapper.rb +308 -24
  53. data/lib/butler/plugin/matcher.rb +3 -1
  54. data/lib/butler/plugin/more.rb +65 -0
  55. data/lib/butler/plugin/onhandlers.rb +4 -4
  56. data/lib/butler/plugin/trigger.rb +4 -2
  57. data/lib/butler/plugins.rb +1 -1
  58. data/lib/butler/session.rb +11 -0
  59. data/lib/butler/version.rb +1 -1
  60. data/lib/cloptions.rb +1 -1
  61. data/lib/diagnostics.rb +20 -0
  62. data/lib/dialogline.rb +1 -1
  63. data/lib/durations.rb +19 -6
  64. data/lib/event.rb +8 -5
  65. data/lib/installer.rb +10 -3
  66. data/lib/ostructfixed.rb +11 -0
  67. data/lib/ruby/kernel/daemonize.rb +1 -2
  68. data/test/butler/plugin/mapper.rb +46 -0
  69. metadata +28 -11
  70. data/CHANGELOG +0 -44
  71. data/data/butler/plugins/core/privilege.rb +0 -103
@@ -17,10 +17,10 @@ trigger "define"
17
17
  def on_trigger
18
18
  word = @message.post_arguments[1]
19
19
  doc = Hpricot(open(Query%[@message.language, CGI.escape(word)]))
20
- see_also = (doc/"//div[@id='res']/font[1]/p/a")[5..-1].map { |e| e.inner_text }
21
- found = (doc/"//div[@id='res']/ul/font/li").find { |e| (e/"//a").inner_text.include?("wikipedia.org") }
20
+ see_also = (doc/"//font[1]/p/a")[5..-1].map { |e| e.inner_text }
21
+ found = (doc/"//ul/font/li").find { |e| (e/"//a").inner_text.include?("wikipedia.org") }
22
22
  if found then
23
- answer(found.inner_text.sub(%r{\w+\.wikipedia\.org/wiki/.*?$}, ''))
23
+ more('![b]word![o]', found.inner_text.sub(%r{\w+\.wikipedia\.org/wiki/.*?$}, ''))
24
24
  else
25
25
  answer(:not_found, :search_string => word)
26
26
  end
@@ -0,0 +1,40 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ def on_trigger(*arguments)
10
+ more = @message.from.session["more"]
11
+ if more && more.succ? then
12
+ more.succ
13
+ answer(more.show)
14
+ else
15
+ answer(:no_more)
16
+ end
17
+ @message.from.session.delete("more") unless more and more.succ?
18
+ end
19
+
20
+ __END__
21
+ ---
22
+ :about:
23
+ :mail: "apeiros@gmx.net"
24
+ :version: "1.0.0"
25
+ :author: "Stefan Rusterholz"
26
+ :help:
27
+ en:
28
+ "": "Continue output."
29
+ :revision:
30
+ :plugin: 1
31
+ :strings:
32
+ :no_more:
33
+ en: Nothing more to display.
34
+ :summary:
35
+ en: "Continue output."
36
+ :trigger:
37
+ en: more
38
+ de: mehr
39
+ :usage:
40
+ en: "![b]more![o]"
@@ -14,7 +14,7 @@ configuration(
14
14
 
15
15
  trigger "cycle"
16
16
 
17
- def on_part(listener, user, channel)
17
+ def on_part(listener, user, channel, reason)
18
18
  if (
19
19
  user != butler.myself && # don't trigger if butler itself parts
20
20
  channel.length == 1 && # trigger only if butler is the last remaining
@@ -6,11 +6,9 @@
6
6
 
7
7
 
8
8
 
9
- trigger "load"
10
-
11
9
  def on_trigger
12
- ps_out = `ps -o vsz,rss,%cpu,%mem -p #{Process.pid}`
13
- vsz, rss, cpu, pmem = ps_out.scan(/\d+(?:\.\d+)?/).map { |e| e.to_f }
10
+ ps_out = `ps -o vsz,rss,%cpu,%mem -p #{Process.pid.to_i}`
11
+ vsz, rss, cpu, pmem = ps_out.scan(/\d+(?:[.,]\d+)?/).map { |e| e.gsub(/,/,'.').to_f } # ps on 10.5.1 outputs ',' instead of '.' for MEM%
14
12
  virtual, real = (vsz-rss).div(1024), rss.div(1024)
15
13
  answer(:memusage, :real => real, :virtual => virtual, :cpu => cpu, :pmem => pmem)
16
14
  rescue Exception => e
@@ -29,10 +27,12 @@ __END__
29
27
  :author: "Stefan Rusterholz"
30
28
  :usage:
31
29
  en: "![b]load![o]"
30
+ :trigger:
31
+ en: load
32
32
  :strings:
33
33
  :memusage:
34
34
  en: |
35
- Usage: <%= real %>MB, <%= virtual %>MB (real/virtual), <%= cpu %>% CPU, <%= pmem %>% Memory.
35
+ Memory: <%= real %>MB real, <%= virtual %>MB virtual (<%= pmem %>%), CPU: <%= cpu %>%.
36
36
  :failure:
37
37
  en: |
38
38
  Failed with exception <%= exception %>.
@@ -6,8 +6,6 @@
6
6
 
7
7
 
8
8
 
9
- trigger "ping"
10
-
11
9
  def on_trigger
12
10
  message.answer("pong")
13
11
  end
@@ -22,6 +20,9 @@ __END__
22
20
  :mail: "apeiros@gmx.net"
23
21
  :version: "1.0.0"
24
22
  :author: "Stefan Rusterholz"
23
+ :trigger:
24
+ en:
25
+ ping
25
26
  :usage:
26
27
  en: "![b]ping![o]"
27
28
  :help:
@@ -58,6 +58,15 @@ class Access
58
58
  def hash
59
59
  @id.hash
60
60
  end
61
+
62
+ def inspect # :nodoc:
63
+ "#<%s:0x%08x id=%s description=%s>" % [
64
+ self.class,
65
+ object_id << 1,
66
+ @id,
67
+ @description
68
+ ]
69
+ end
61
70
  end
62
71
 
63
72
  class Privileges
@@ -118,5 +127,13 @@ class Access
118
127
  return true if yield("")
119
128
  false
120
129
  end
130
+
131
+ def inspect # :nodoc:
132
+ "#<%s:0x%08x %s>" % [
133
+ self.class,
134
+ object_id << 1,
135
+ @privileges.keys.join(', ')
136
+ ]
137
+ end
121
138
  end
122
139
  end
@@ -12,6 +12,7 @@ class Access
12
12
  # an additional restriction (which is applied globally).
13
13
  #
14
14
  class Role
15
+ include Savable
15
16
 
16
17
  # The module to extend the Database manager
17
18
  module Base
@@ -36,7 +37,7 @@ class Access
36
37
  data[:roles] = data[:roles].map { |role| roles[role] }
37
38
  array = data.values_at(:id, :description)
38
39
  array << data
39
- role = new(*array)
40
+ role = Role.new(*array)
40
41
  role.access = access
41
42
  role.base = self
42
43
  role
@@ -48,6 +49,12 @@ class Access
48
49
 
49
50
  # The description of the role
50
51
  attr_reader :description
52
+
53
+ # The roles this role belongs to
54
+ attr_reader :roles
55
+
56
+ # Privileges this role has granted
57
+ attr_reader :privileges
51
58
 
52
59
  # Create a new Role
53
60
  # role is a role-id, should be \w+
@@ -57,7 +64,13 @@ class Access
57
64
  @id = role
58
65
  @privileges = Privileges.new(self, other[:privileges])
59
66
  @roles = Roles.new(self, other[:roles])
60
- @description = description || "No description"
67
+ @description = (description || "No description").freeze
68
+ end
69
+
70
+ # Change the description of this role
71
+ def description=(value)
72
+ @description = value.freeze
73
+ save
61
74
  end
62
75
 
63
76
  # :nodoc:
@@ -87,6 +100,16 @@ class Access
87
100
  def hash
88
101
  @id.hash
89
102
  end
103
+
104
+ def inspect # :nodoc:
105
+ "#<%s:0x%08x description=%s privileges=%s roles=%s>" % [
106
+ self.class,
107
+ object_id << 1,
108
+ @description,
109
+ @privileges.inspect,
110
+ @roles.inspect,
111
+ ]
112
+ end
90
113
  end
91
114
 
92
115
  # A list of roles
@@ -133,5 +156,13 @@ class Access
133
156
  def storable
134
157
  @roles.map { |role| role.id }
135
158
  end
159
+
160
+ def inspect # :nodoc:
161
+ "#<%s:0x%08x roles=%s>" % [
162
+ self.class,
163
+ object_id << 1,
164
+ @roles.map { |r| r.id }.join(', '),
165
+ ]
166
+ end
136
167
  end
137
168
  end
@@ -16,8 +16,14 @@ class Access
16
16
  # The "Base" instance that manages this record
17
17
  attr_accessor :base
18
18
 
19
+ # save changes to this record
19
20
  def save
20
21
  base.save(id())
21
22
  end
23
+
24
+ # delete this record from the database
25
+ def delete
26
+ base.delete(id())
27
+ end
22
28
  end
23
29
  end
@@ -81,8 +81,7 @@ class Access
81
81
  alias add <<
82
82
 
83
83
  # Delete a record from the storage
84
- def delete(record)
85
- id = record.kind_of?(@type) ? record.id : record
84
+ def delete(id)
86
85
  @data.delete(id)
87
86
  File.delete(filename(id))
88
87
  end
@@ -8,18 +8,20 @@
8
8
 
9
9
  require 'butler'
10
10
  require 'butler/irc/client'
11
+ require 'butler/debuglog'
11
12
  require 'configuration'
12
13
  require 'log'
13
14
  require 'butler/plugins'
15
+ require 'butler/session'
14
16
  require 'ruby/string/arguments'
15
17
  require 'ruby/string/post_arguments'
16
18
  require 'scheduler'
17
19
  require 'set'
18
20
  require 'thread'
21
+ require 'timeout'
19
22
 
20
23
 
21
24
 
22
- # FIXME: credits @ daemonize lib
23
25
  class Butler
24
26
  class Bot < IRC::Client
25
27
  include Log::Comfort
@@ -42,10 +44,13 @@ class Butler
42
44
  :access => @base+'/access',
43
45
  :base => @base,
44
46
  :config => @base+'/config',
47
+ :lib => @base+'/lib',
45
48
  :log => @base+'/log',
46
49
  :plugins => @base+'/plugins',
47
50
  :strings => @base+'/strings'
48
51
  )
52
+ $LOAD_PATH.unshift(@path.lib)
53
+ require 'butlerinit' if File.exist?(@path.lib+"/butlerinit.rb")
49
54
  @logger = $stderr
50
55
  @config = Configuration.new(@base+'/config')
51
56
  @scheduler = Scheduler.new
@@ -71,6 +76,12 @@ class Butler
71
76
  @commands = {}
72
77
  @plugins = Plugins.new(self, @base+'/plugins')
73
78
 
79
+ if $DEBUG then
80
+ @irc.extend DebugLog
81
+ @irc.raw_log = File.open(@path.log+'/debug.log', 'wb')
82
+ @irc.raw_log.sync = true
83
+ end
84
+
74
85
  subscribe(:PRIVMSG, -10, &method(:invoke_commands))
75
86
  subscribe(:NOTICE, -10, &method(:invoke_commands))
76
87
 
@@ -111,7 +122,7 @@ class Butler
111
122
  end
112
123
  }
113
124
  end
114
-
125
+
115
126
  def add_command(command)
116
127
  @commands[command.language] ||= {}
117
128
  @commands[command.language][command.trigger] ||= SortedSet.new
@@ -148,26 +159,46 @@ class Butler
148
159
  @config["connections/main/real"]
149
160
  )
150
161
  info("Logged in")
151
- # FIXME, use #same_nick?
152
- if pass && @myself.nick.downcase != nick.downcase then
162
+ if pass && @myself.nick.same_nick?(nick) then
153
163
  @irc.ghost(nick, pass)
154
164
  sleep(1) # FIXME, do a wait_for or similar
155
165
  @irc.nick(nick)
156
166
  end
157
167
  @irc.identify(pass) if pass
158
168
  join(*@config["connections/main/channels"])
169
+ plugins_dispatch(:on_login, "main")
159
170
  end
160
171
 
161
172
  def on_disconnect(reason)
162
173
  return if reason == :quit
174
+ plugins_dispatch(:on_disconnect, reason)
163
175
  unless @reconnect[1].zero? then
164
176
  @reconnect[1] -= 1
165
177
  sleep(@reconnect[0])
166
178
  login
167
179
  end
168
180
  end
169
-
170
181
 
182
+ def quit(reason=nil, *args)
183
+ timeout(3) {
184
+ plugins_dispatch(:on_quit, reason, *args).join
185
+ }
186
+ ensure
187
+ super(reason)
188
+ end
189
+
190
+ def plugins_dispatch(event, *args)
191
+ Thread.new { # avoid total lockup due to an improperly written on_disconnect
192
+ begin
193
+ @plugins.instances.each { |plugin|
194
+ plugin.send(event, *args)
195
+ }
196
+ rescue Exception => e
197
+ exception(e)
198
+ end
199
+ }
200
+ end
201
+
171
202
  def output_to_logfiles
172
203
  # Errors still go to $stderr, $stdin handles puts as "info" level $stderr prints
173
204
  @logger = Log.file(@path.log+'/error.log')
@@ -186,12 +217,13 @@ class Butler
186
217
  end
187
218
  end # Bot
188
219
 
189
- class IRC::Users
220
+ class IRC::UserList
190
221
  attr_reader :client
191
222
  end
192
223
 
193
224
  class IRC::User
194
225
  attr_accessor :access
226
+ attr_reader :session
195
227
 
196
228
  def authorized?(*args)
197
229
  @access.authorized?(*args)
@@ -200,7 +232,8 @@ class Butler
200
232
  alias butler_initialize initialize unless method_defined? :butler_initialize
201
233
  def initialize(users, *args, &block)
202
234
  butler_initialize(users, *args, &block)
203
- @access = users.client.access.default_user
235
+ @access = users.client.access.default_user
236
+ @session = Session.new
204
237
  end
205
238
  end # IRC::User
206
239
 
@@ -0,0 +1,17 @@
1
+ class Butler
2
+ module DebugLog
3
+ attr_accessor :raw_log
4
+
5
+ def read
6
+ data = super
7
+ @raw_log.printf("%s < %p\n", Time.now, data)
8
+ data
9
+ end
10
+
11
+ def write(data)
12
+ rv = super
13
+ @raw_log.printf("%s > %p\n", Time.now, data)
14
+ rv
15
+ end
16
+ end
17
+ end
@@ -29,7 +29,7 @@ class Butler
29
29
 
30
30
  def initialize(plugin)
31
31
  @plugin = plugin
32
- @plugin.butler.subscribe(:PRIVMSG, 0, true) { |listener, message|
32
+ @plugin.butler.subscribe(:PRIVMSG, 20) { |listener, message|
33
33
  if message.private? then
34
34
  postpone
35
35
  dialog(listener, message)
@@ -8,7 +8,7 @@
8
8
 
9
9
  require 'butler/irc/string'
10
10
  require 'butler/irc/channel'
11
- require 'butler/irc/users'
11
+ require 'butler/irc/userlist'
12
12
  require 'thread'
13
13
 
14
14
 
@@ -31,7 +31,7 @@ class Butler
31
31
  ]
32
32
 
33
33
  # Butler::IRC::Channel represents a channel in IRC.
34
- class Channels
34
+ class ChannelList
35
35
  include Enumerable
36
36
 
37
37
  def initialize(client)
@@ -1,15 +1,27 @@
1
- require 'butler/irc/channels'
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ require 'butler/irc/channellist'
10
+ require 'butler/irc/client/filter'
11
+ require 'butler/irc/client/listener'
12
+ require 'butler/irc/client/listenerlist'
2
13
  require 'butler/irc/parser'
3
14
  require 'butler/irc/socket'
4
- require 'butler/irc/users'
15
+ require 'butler/irc/userlist'
16
+ require 'butler/irc/whois'
5
17
  require 'log/comfort'
6
18
  require 'timeout'
7
19
  require 'thread'
8
20
 
21
+
22
+
9
23
  class Butler
10
24
  module IRC
11
- # Reply for Butler::IRC::Client#whois
12
- Whois = Struct.new(:exists, :nick, :user, :host, :real, :registered, :channels, :server, :idle, :signon)
13
25
 
14
26
  # == Description
15
27
  # Wraps Butler::IRC::Socket, providing methods to be aware of the environment.
@@ -28,7 +40,7 @@ class Butler
28
40
  # when /#{irc_client.myself.nick}[,:]/
29
41
  # message.answer("#{message.from.nick}, you spoke to me?")
30
42
  # when :JOIN
31
- # message.from.notice("Welcome to #{message.channel}!")
43
+ # irc_client.irc.notice("Welcome to #{message.channel}!", message.from)
32
44
  # end
33
45
  # puts "received: #{message}"
34
46
  # }
@@ -38,36 +50,7 @@ class Butler
38
50
  include Log::Comfort
39
51
 
40
52
  class Terminate < RuntimeError; end
41
- # Created by Butler::IRC::Client#subscribe() and similar methods
42
- Listener = Struct.new(:priority, :callback, :args, :unsubscriber) unless defined? Listener
43
- class Listener
44
- alias set_priority priority=
45
- private :set_priority
46
- private :unsubscriber
47
-
48
- def initialize(priority, callback, args=[], &unsubscriber)
49
- super(priority, callback, args, unsubscriber)
50
- end
51
-
52
- # will remove this listener from the clients dispatcher forever
53
- def unsubscribe
54
- unsubscriber.call(self)
55
- end
56
53
 
57
- # set the priority of this listener
58
- # see Butler::IRC::Client#subscribe() for infos about priority
59
- def priority=(value)
60
- set_priority(value)
61
- container.sort_by { |l| -l.priority }
62
- end
63
- end
64
- module Filter # :nodoc:
65
- attr_accessor :listener
66
- def unsubscribe
67
- listener.unsubscribe
68
- end
69
- end
70
-
71
54
  # The timeout defaults
72
55
  DefaultTimeout = {
73
56
  :login => 150,
@@ -92,7 +75,10 @@ class Butler
92
75
  # server_charset
93
76
  attr_reader :channel_charset
94
77
 
78
+ # The channels the client participates
95
79
  attr_reader :channels
80
+
81
+ # The users visible to the client
96
82
  attr_reader :users
97
83
 
98
84
  # The Butler::IRC::Socket, most send commands have to be used on this
@@ -101,30 +87,37 @@ class Butler
101
87
  # The user representing the bot
102
88
  attr_reader :myself
103
89
 
90
+ # A callback invoked on disconnects, accepts one argument: reason, which can be
91
+ # * :quit: you told the client to quit
92
+ # * :disconnect: the server disconnected your client
93
+ # * :error: an error occurred in the read-thread (shouldn't happen)
94
+ attr_accessor :disconnect_callback
95
+
96
+ # Arguments:
97
+ # * server: the server to connect to
104
98
  def initialize(server, options={}, &on_disconnect)
105
- options = DefaultOptions.merge(options)
106
- @logger = nil
107
- @users = Users.new(self)
108
- @channels = Channels.new(self)
109
- @users.channels = @channels
110
- @channels.users = @users
111
- @parser = Parser.new(self, @users, @channels, "rfc2812", "generic")
99
+ options = DefaultOptions.merge(options)
100
+ @disconnect_callback =
101
+ @logger = nil
102
+ @users = UserList.new(self)
103
+ @channels = ChannelList.new(self)
104
+ @users.channels = @channels
105
+ @channels.users = @users
106
+ @parser = Parser.new(self, @users, @channels, "rfc2812", "generic")
112
107
 
113
- @client_charset = options.delete(:client_charset)
114
- @server_charset = options.delete(:server_charset)
108
+ @client_charset = options.delete(:client_charset)
109
+ @server_charset = options.delete(:server_charset)
115
110
  # proc needed because @server_charset might change
116
- @channel_charset = Hash.new { |h,k| @server_charset }.merge(options.delete(:channel_charset))
111
+ @channel_charset = Hash.new { |h,k| @server_charset }.merge(options.delete(:channel_charset))
117
112
 
118
- @timeout = DefaultTimeout.merge(options.delete(:timeout))
113
+ @timeout = DefaultTimeout.merge(options.delete(:timeout))
119
114
 
120
- @irc = Socket.new(options.delete(:server) || server, options) # the socket, all methods to the socket are wrapped
115
+ @irc = Socket.new(options.delete(:server) || server, options) # the socket, all methods to the socket are wrapped
121
116
 
122
- @listen = Hash.new { |h,k| h[k] = [] }
123
- @listener = {}
124
- @event_loop = Queue.new
125
- @dispatch_lock = Mutex.new
126
- @thread_read = nil
127
- @myself = @users.myself
117
+ @listener = ListenerList.new
118
+ @event_loop = Queue.new
119
+ @thread_read = nil
120
+ @myself = @users.myself
128
121
 
129
122
  subscribe(:PING, 100) { |listener, message| @irc.pong(message.pong) }
130
123
  end
@@ -139,31 +132,20 @@ class Butler
139
132
  # priority may be any numeric, higher priority is dispatched to first,
140
133
  # lower priority later
141
134
  # returns an Butler::IRC::Client::Listener
142
- def subscribe(symbol=nil, priority=0, id=nil, *args, &callback)
143
- id ||= callback
144
- raise "#{id} already subscribed" if @listener.has_key?(id)
145
- listener = Listener.new(priority, callback, args) { |item|
146
- @dispatch_lock.synchronize {
147
- @listener.delete(id)
148
- @listen[symbol].delete(item)
149
- @listen.delete(symbol) if @listen[symbol].empty?
150
- }
151
- }
152
- @dispatch_lock.synchronize {
153
- @listener[id] = listener
154
- @listen[symbol] << listener
155
- @listen[symbol].sort_by { |l| -l.priority }
156
- }
135
+ def subscribe(symbols=nil, priority=0, *args, &callback)
136
+ listener = Listener.new(symbols, priority, *args, &callback)
137
+ @listener.subscribe(listener)
157
138
  listener
158
139
  end
140
+
141
+ # subscribe a Listener instance (or anything that emulates its interface)
142
+ def subscribe_listener(listener)
143
+ @listener.subscribe(listener)
144
+ end
159
145
 
160
- # unsubscribe a listener by id
161
- def unsubscribe(id)
162
- @dispatch_lock.synchronize {
163
- @listener.delete(id)
164
- @listen[symbol].delete(item)
165
- @listen.delete(symbol) if @listen[symbol].empty?
166
- }
146
+ # unsubscribe a listener
147
+ def unsubscribe(listener)
148
+ @listener.unsubscribe(listener)
167
149
  end
168
150
 
169
151
  # blocks current thread until a Message with symbol
@@ -313,7 +295,9 @@ class Butler
313
295
  @irc.close
314
296
  end
315
297
 
298
+ # Called on disconnects, see disconnect_callback attribute documentation
316
299
  def on_disconnect(reason)
300
+ @disconnect_callback.call(reasons) if @disconnect_callback
317
301
  end
318
302
 
319
303
  def event_loop(priority=-1)
@@ -335,11 +319,8 @@ class Butler
335
319
  def process(message)
336
320
  message = @parser.server_message(message)
337
321
  message.transcode!(@channel_charset[message.channel], @client_charset)
338
- @dispatch_lock.synchronize {
339
- @listen[nil].each { |listener| listener.callback.call(listener, message) }
340
- if @listen.has_key?(message.symbol)
341
- @listen[message.symbol].each { |listener| listener.callback.call(listener, message, *listener.args) }
342
- end
322
+ @listener.synchronized_each_for(message.symbol) { |listener|
323
+ listener.call(message)
343
324
  }
344
325
  rescue Terminate, Errno::EPIPE => error
345
326
  raise error # on these errors we got to get out of the loop