butler 1.8.1 → 1.8.2

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