butler 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. data/CHANGELOG +4 -0
  2. data/GPL.txt +340 -0
  3. data/LICENSE.txt +52 -0
  4. data/README +37 -0
  5. data/Rakefile +334 -0
  6. data/bin/botcontrol +230 -0
  7. data/data/butler/config_template.yaml +4 -0
  8. data/data/butler/dialogs/backup.rb +19 -0
  9. data/data/butler/dialogs/botcontrol.rb +4 -0
  10. data/data/butler/dialogs/config.rb +1 -0
  11. data/data/butler/dialogs/create.rb +53 -0
  12. data/data/butler/dialogs/delete.rb +3 -0
  13. data/data/butler/dialogs/en/backup.yaml +6 -0
  14. data/data/butler/dialogs/en/botcontrol.yaml +5 -0
  15. data/data/butler/dialogs/en/create.yaml +11 -0
  16. data/data/butler/dialogs/en/delete.yaml +2 -0
  17. data/data/butler/dialogs/en/help.yaml +17 -0
  18. data/data/butler/dialogs/en/info.yaml +13 -0
  19. data/data/butler/dialogs/en/list.yaml +4 -0
  20. data/data/butler/dialogs/en/notyetimplemented.yaml +2 -0
  21. data/data/butler/dialogs/en/rename.yaml +3 -0
  22. data/data/butler/dialogs/en/start.yaml +3 -0
  23. data/data/butler/dialogs/en/sync_plugins.yaml +3 -0
  24. data/data/butler/dialogs/en/uninstall.yaml +5 -0
  25. data/data/butler/dialogs/en/unknown_command.yaml +2 -0
  26. data/data/butler/dialogs/help.rb +11 -0
  27. data/data/butler/dialogs/info.rb +27 -0
  28. data/data/butler/dialogs/interactive.rb +1 -0
  29. data/data/butler/dialogs/list.rb +10 -0
  30. data/data/butler/dialogs/notyetimplemented.rb +1 -0
  31. data/data/butler/dialogs/rename.rb +4 -0
  32. data/data/butler/dialogs/selectbot.rb +2 -0
  33. data/data/butler/dialogs/start.rb +5 -0
  34. data/data/butler/dialogs/sync_plugins.rb +30 -0
  35. data/data/butler/dialogs/uninstall.rb +17 -0
  36. data/data/butler/dialogs/unknown_command.rb +1 -0
  37. data/data/butler/plugins/core/logout.rb +41 -0
  38. data/data/butler/plugins/core/plugins.rb +134 -0
  39. data/data/butler/plugins/core/privilege.rb +103 -0
  40. data/data/butler/plugins/core/user.rb +166 -0
  41. data/data/butler/plugins/dev/eval.rb +64 -0
  42. data/data/butler/plugins/dev/nometa.rb +14 -0
  43. data/data/butler/plugins/dev/onhandlers.rb +93 -0
  44. data/data/butler/plugins/dev/raw.rb +36 -0
  45. data/data/butler/plugins/dev/rawlog.rb +77 -0
  46. data/data/butler/plugins/games/eightball.rb +54 -0
  47. data/data/butler/plugins/games/mastermind.rb +174 -0
  48. data/data/butler/plugins/irc/action.rb +36 -0
  49. data/data/butler/plugins/irc/join.rb +38 -0
  50. data/data/butler/plugins/irc/notice.rb +36 -0
  51. data/data/butler/plugins/irc/part.rb +38 -0
  52. data/data/butler/plugins/irc/privmsg.rb +36 -0
  53. data/data/butler/plugins/irc/quit.rb +36 -0
  54. data/data/butler/plugins/operator/deop.rb +41 -0
  55. data/data/butler/plugins/operator/devoice.rb +41 -0
  56. data/data/butler/plugins/operator/limit.rb +47 -0
  57. data/data/butler/plugins/operator/op.rb +41 -0
  58. data/data/butler/plugins/operator/voice.rb +41 -0
  59. data/data/butler/plugins/public/help.rb +69 -0
  60. data/data/butler/plugins/public/login.rb +72 -0
  61. data/data/butler/plugins/public/usage.rb +49 -0
  62. data/data/butler/plugins/service/clones.rb +56 -0
  63. data/data/butler/plugins/service/define.rb +47 -0
  64. data/data/butler/plugins/service/log.rb +183 -0
  65. data/data/butler/plugins/service/svn.rb +91 -0
  66. data/data/butler/plugins/util/cycle.rb +98 -0
  67. data/data/butler/plugins/util/load.rb +41 -0
  68. data/data/butler/plugins/util/pong.rb +29 -0
  69. data/data/butler/strings/random/acknowledge.en.yaml +5 -0
  70. data/data/butler/strings/random/gratitude.en.yaml +3 -0
  71. data/data/butler/strings/random/hello.en.yaml +4 -0
  72. data/data/butler/strings/random/ignorance.en.yaml +7 -0
  73. data/data/butler/strings/random/ignorance_about.en.yaml +3 -0
  74. data/data/butler/strings/random/insult.en.yaml +3 -0
  75. data/data/butler/strings/random/rejection.en.yaml +12 -0
  76. data/data/man/botcontrol.1 +17 -0
  77. data/lib/access.rb +187 -0
  78. data/lib/access/admin.rb +16 -0
  79. data/lib/access/privilege.rb +122 -0
  80. data/lib/access/role.rb +102 -0
  81. data/lib/access/savable.rb +18 -0
  82. data/lib/access/user.rb +180 -0
  83. data/lib/access/yamlbase.rb +126 -0
  84. data/lib/butler.rb +188 -0
  85. data/lib/butler/bot.rb +247 -0
  86. data/lib/butler/control.rb +93 -0
  87. data/lib/butler/dialog.rb +64 -0
  88. data/lib/butler/initialvalues.rb +40 -0
  89. data/lib/butler/irc/channel.rb +135 -0
  90. data/lib/butler/irc/channels.rb +96 -0
  91. data/lib/butler/irc/client.rb +351 -0
  92. data/lib/butler/irc/hostmask.rb +53 -0
  93. data/lib/butler/irc/message.rb +184 -0
  94. data/lib/butler/irc/parser.rb +125 -0
  95. data/lib/butler/irc/parser/commands.rb +83 -0
  96. data/lib/butler/irc/parser/generic.rb +343 -0
  97. data/lib/butler/irc/socket.rb +378 -0
  98. data/lib/butler/irc/string.rb +186 -0
  99. data/lib/butler/irc/topic.rb +15 -0
  100. data/lib/butler/irc/user.rb +265 -0
  101. data/lib/butler/irc/users.rb +112 -0
  102. data/lib/butler/plugin.rb +249 -0
  103. data/lib/butler/plugin/configproxy.rb +35 -0
  104. data/lib/butler/plugin/mapper.rb +85 -0
  105. data/lib/butler/plugin/matcher.rb +55 -0
  106. data/lib/butler/plugin/onhandlers.rb +70 -0
  107. data/lib/butler/plugin/trigger.rb +58 -0
  108. data/lib/butler/plugins.rb +147 -0
  109. data/lib/butler/version.rb +17 -0
  110. data/lib/cloptions.rb +217 -0
  111. data/lib/cloptions/adapters.rb +24 -0
  112. data/lib/cloptions/switch.rb +132 -0
  113. data/lib/configuration.rb +223 -0
  114. data/lib/dialogline.rb +296 -0
  115. data/lib/dialogline/localizations.rb +24 -0
  116. data/lib/durations.rb +57 -0
  117. data/lib/event.rb +295 -0
  118. data/lib/event/at.rb +64 -0
  119. data/lib/event/every.rb +56 -0
  120. data/lib/event/timed.rb +112 -0
  121. data/lib/installer.rb +75 -0
  122. data/lib/iterator.rb +34 -0
  123. data/lib/log.rb +68 -0
  124. data/lib/log/comfort.rb +85 -0
  125. data/lib/log/converter.rb +23 -0
  126. data/lib/log/entry.rb +152 -0
  127. data/lib/log/fakeio.rb +55 -0
  128. data/lib/log/file.rb +54 -0
  129. data/lib/log/filereader.rb +81 -0
  130. data/lib/log/forward.rb +49 -0
  131. data/lib/log/methods.rb +39 -0
  132. data/lib/log/nolog.rb +18 -0
  133. data/lib/log/splitter.rb +26 -0
  134. data/lib/ostructfixed.rb +26 -0
  135. data/lib/ruby/array/columnize.rb +38 -0
  136. data/lib/ruby/dir/mktree.rb +28 -0
  137. data/lib/ruby/enumerable/join.rb +13 -0
  138. data/lib/ruby/exception/detailed.rb +24 -0
  139. data/lib/ruby/file/append.rb +11 -0
  140. data/lib/ruby/file/write.rb +11 -0
  141. data/lib/ruby/hash/zip.rb +15 -0
  142. data/lib/ruby/kernel/bench.rb +15 -0
  143. data/lib/ruby/kernel/daemonize.rb +42 -0
  144. data/lib/ruby/kernel/non_verbose.rb +17 -0
  145. data/lib/ruby/kernel/safe_fork.rb +18 -0
  146. data/lib/ruby/range/stepped.rb +11 -0
  147. data/lib/ruby/string/arguments.rb +72 -0
  148. data/lib/ruby/string/chunks.rb +15 -0
  149. data/lib/ruby/string/post_arguments.rb +44 -0
  150. data/lib/ruby/string/unescaped.rb +17 -0
  151. data/lib/scheduler.rb +164 -0
  152. data/lib/scriptfile.rb +101 -0
  153. data/lib/templater.rb +86 -0
  154. data/test/cloptions.rb +134 -0
  155. data/test/cv.rb +28 -0
  156. data/test/irc/client.rb +85 -0
  157. data/test/irc/client_login.txt +53 -0
  158. data/test/irc/client_subscribe.txt +8 -0
  159. data/test/irc/message.rb +30 -0
  160. data/test/irc/messages.txt +64 -0
  161. data/test/irc/parser.rb +13 -0
  162. data/test/irc/profile_parser.rb +12 -0
  163. data/test/irc/users.rb +28 -0
  164. metadata +256 -0
@@ -0,0 +1,35 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class Butler
10
+ class Plugin
11
+ class ConfigProxy
12
+ def initialize(config, base="")
13
+ @config = config
14
+ @base = base
15
+ end
16
+
17
+ def []=(key, value)
18
+ @config[key.empty? ? @base : "#{@base}.#{key}"] = value
19
+ end
20
+
21
+ def [](key, *args)
22
+ @config[(key.empty? ? @base : "#{@base}.#{key}"), *args]
23
+ end
24
+
25
+ def exist?(key)
26
+ @config.exist?(key.empty? ? @base : "#{@base}.#{key}")
27
+ end
28
+ alias has_key? exist?
29
+
30
+ def delete(key)
31
+ @config.delete(key.empty? ? @base : "#{@base}.#{key}")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,85 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ require 'butler/plugin'
10
+ require 'ostruct'
11
+
12
+
13
+
14
+ class Butler
15
+ class Plugin
16
+ class Mapper
17
+ attr_reader :authorization
18
+ attr_reader :hash
19
+ attr_reader :language
20
+ attr_reader :name
21
+ attr_reader :plugin
22
+ attr_reader :trigger
23
+ attr_reader :hash
24
+
25
+ def initialize(plugin, meth, language, expression)
26
+ @plugin = plugin
27
+ @method = meth
28
+ @language = language
29
+ @expression = expression
30
+ @trigger = expression[/^\w+/].downcase
31
+ @captures = []
32
+ @authorization = "plugin/#{@plugin.base}".freeze
33
+ @name = "mapper:#{@language}:#{@expression}".freeze
34
+ @hash = @name.hash
35
+
36
+ tiles = expression.strip.scan(/(?:^|\s)[:*]\w+(?:\[[^\]]+\])?|\S+/).map { |word|
37
+ word.strip!
38
+ case word
39
+ when /:(\w+)(.*)/
40
+ @captures << [$1.to_sym, $2]
41
+ '(\S+)'
42
+ when /\*(\w+)(.*)/
43
+ @captures << [$1.to_sym, $2]
44
+ '(\S+(?:\s+\S+)*?)'
45
+ else
46
+ Regexp.escape(word)
47
+ end
48
+ }
49
+ @regexp = Regexp.new('^'+tiles.join('\s+')+'$')
50
+ end
51
+
52
+ def invoked_by?(message)
53
+ if match = @regexp.match(message.post_arguments[0]) then
54
+ params = {}
55
+ @captures.zip(match.captures) { |(name, valid), value|
56
+ params[name] = value
57
+ }
58
+ [OpenStruct.new(params)]
59
+ else
60
+ nil
61
+ end
62
+ end
63
+
64
+ def call(message, params)
65
+ @plugin.new(message).send(@method, params)
66
+ end
67
+
68
+ def priority
69
+ -10
70
+ end
71
+
72
+ def <=>(other)
73
+ -10 <=> other.priority
74
+ end
75
+
76
+ def abort_invocations?
77
+ true
78
+ end
79
+
80
+ def eql?(other)
81
+ other.kind_of?(Mapper) && @name.eql?(other.name)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,55 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class Butler
10
+ class Plugin
11
+ class Matcher
12
+ attr_reader :priority
13
+ attr_reader :hash
14
+ attr_reader :en_match
15
+ attr_reader :plugin
16
+
17
+ def initialize(matcher, priority=0, &callback)
18
+ @matcher = case matcher
19
+ when Hash: matcher
20
+ when Regexp: { "en" => matcher }
21
+ else raise ArgumentError, "Matcher must be either Regexp or Hash, but is #{matcher.class}"
22
+ end
23
+ raise ArgumentError, "Matcher must supply an english variant" unless @matcher["en"]
24
+ @priority = priority
25
+ @callback = callback
26
+ @en_matcher = @matcher["en"]
27
+ @hash = @en_matcher.hash
28
+ end
29
+
30
+ def name
31
+ "matcher:#{@en_matcher}"
32
+ end
33
+
34
+ def <=>(other)
35
+ @priority <=> other.priority
36
+ end
37
+
38
+ def eql?(other)
39
+ other.kind_of?(Matcher) && @en_matcher.eql?(other.en_matcher)
40
+ end
41
+
42
+ def ===(message)
43
+ if @matcher.has_key?(message.language) then
44
+ @matcher[message.language] =~ message.text
45
+ else
46
+ @matcher["en"] =~ message.text
47
+ end
48
+ end
49
+
50
+ def call(*args)
51
+ @callback.call(self, *args)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class Butler
10
+ class Plugin
11
+ module OnHandlers
12
+ def method_added(name, *a, &b)
13
+ subscribe_on_handler(name)
14
+ end
15
+
16
+ def subscribe_on_handler(handler, priority=0)
17
+ case handler
18
+ when :on_join: subscribe(:JOIN, priority) { |listener, message| new(message).on_join(listener, message.from, message.channel) }
19
+ when :on_privmsg: subscribe(:PRIVMSG, priority) { |listener, message| new(message).on_privmsg(listener, message.from, message.text) }
20
+ when :on_notice: subscribe(:NOTICE, priority) { |listener, message| new(message).on_notice(listener, message.from, message.text) }
21
+ when :on_nick: subscribe(:NICK, priority) { |listener, message| new(message).on_nick(listener, message.from, message.old_nick) }
22
+ when :on_topic: subscribe(:TOPIC, priority) { |listener, message| new(message).on_topic(listener, message.from, message.channel, message.text) }
23
+ when :on_part: subscribe(:PART, priority) { |listener, message| new(message).on_part(listener, message.from, message.channel) }
24
+ when :on_quit: subscribe(:QUIT, priority) { |listener, message| new(message).on_quit(listener, message.from, message.text) }
25
+ when :on_kick: subscribe(:KICK, priority) { |listener, message| new(message).on_kick(listener) }
26
+ when :on_kill: subscribe(:KILL, priority) { |listener, message| new(message).on_kill(listener) }
27
+ when :on_kline: subscribe(:KLINE, priority) { |listener, message| new(message).on_kline(listener) }
28
+ when :on_invocation
29
+ subscribe(:PRIVMSG, priority) { |listener, message|
30
+ new(message).on_invocation(listener) if message.invocation
31
+ }
32
+ subscribe(:NOTICE, priority) { |listener, message|
33
+ new(message).on_invocation(listener) if message.invocation
34
+ }
35
+ when :on_ban
36
+ subscribe(:MODE, priority) { |listener, message|
37
+ bans = message.modes.select { |dir, type, target| dir == '+' && type == 'b' }
38
+ new(message).on_ban(listener, *bans.map { |a,b,c| c }) unless bans.empty?
39
+ }
40
+ when :on_unban
41
+ subscribe(:MODE, priority) { |listener, message|
42
+ unbans = message.modes.select { |dir, type, target| dir == '-' && type == 'b' }
43
+ new(message).on_unban(listener, *unbans.map { |a,b,c| c }) unless unbans.empty?
44
+ }
45
+ when :on_op
46
+ subscribe(:MODE, priority) { |listener, message|
47
+ ops = message.modes.select { |dir, type, target| dir == '+' && type == 'o' }
48
+ new(message).on_op(listener, *ops.map { |a,b,c| c }) unless ops.empty?
49
+ }
50
+ when :on_deop
51
+ subscribe(:MODE, priority) { |listener, message|
52
+ deops = message.modes.select { |dir, type, target| dir == '-' && type == 'o' }
53
+ new(message).on_deop(listener, *deops.map { |a,b,c| c }) unless deops.empty?
54
+ }
55
+ when :on_voice
56
+ subscribe(:MODE, priority) { |listener, message|
57
+ voices = message.modes.select { |dir, type, target| dir == '+' && type == 'v' }
58
+ new(message).on_voice(listener, *voices.map { |a,b,c| c }) unless voices.empty?
59
+ }
60
+ when :on_devoice
61
+ subscribe(:MODE, priority) { |listener, message|
62
+ devoices = message.modes.select { |dir, type, target| dir == '-' && type == 'v' }
63
+ new(message).on_devoice(listener, *devoices.map { |a,b,c| c }) unless devoices.empty?
64
+ }
65
+ # :on_action => :PRIVMSG, :NOTICE
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,58 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ require 'butler/plugin'
10
+
11
+
12
+
13
+ class Butler
14
+ class Plugin
15
+ class Trigger
16
+ attr_reader :authorization
17
+ attr_reader :hash
18
+ attr_reader :language
19
+ attr_reader :name
20
+ attr_reader :plugin
21
+ attr_reader :trigger
22
+ attr_reader :hash
23
+
24
+ def initialize(plugin, language, trigger)
25
+ @plugin = plugin
26
+ @language = language
27
+ @trigger = trigger.downcase
28
+ @authorization = "plugin/#{@plugin.base}".freeze
29
+ @name = "trigger:#{@language}:#{@trigger}".freeze
30
+ @hash = @name.hash
31
+ end
32
+
33
+ def invoked_by?(message)
34
+ []
35
+ end
36
+
37
+ def call(message)
38
+ @plugin.new(message).on_trigger
39
+ end
40
+
41
+ def priority
42
+ 0
43
+ end
44
+
45
+ def <=>(other)
46
+ 0 <=> other.priority
47
+ end
48
+
49
+ def abort_invocations?
50
+ true
51
+ end
52
+
53
+ def eql?(other)
54
+ other.kind_of?(Trigger) && @name.eql?(other.name)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,147 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ require 'scriptfile'
10
+ require 'butler/plugin'
11
+ require 'butler/dialog'
12
+ require 'iterator'
13
+ require 'log/comfort'
14
+ require 'ruby/exception/detailed'
15
+
16
+
17
+
18
+ class String
19
+ # CamelCase a string, e.g. "foo_bar" becomes "FooBar"
20
+ def camelcase
21
+ scan(/[^_]+/).map { |s| s.capitalize }.join("")
22
+ end
23
+ end
24
+
25
+ class Butler
26
+ # Plugins manages the plugins in Butler
27
+ # It uses
28
+ class Plugins
29
+ include Enumerable
30
+ include Log::Comfort
31
+
32
+ Suffix = ".rb"
33
+
34
+ attr_reader :dir
35
+ def initialize(butler, plugin_dir)
36
+ @dir = File.expand_path(plugin_dir).freeze
37
+ raise ArgumentError, "#{@dir} is not a directory" unless File.directory?(@dir)
38
+ @plugins = {}
39
+ @constants = {}
40
+ @butler = butler
41
+ end
42
+
43
+ # returns a list with first-level groups
44
+ def groups
45
+ slice = @dir.length+1..-1
46
+ Dir[@dir+"/*"].reject { |file|
47
+ File.extname(file).downcase == Suffix
48
+ }.map { |file|
49
+ file[slice]
50
+ }
51
+ end
52
+
53
+ def instances
54
+ @plugins.values
55
+ end
56
+
57
+ # returns a list with the bases of all plugins
58
+ def all
59
+ slice = @dir.length+1..-(Suffix.length+1)
60
+ Dir["#{@dir}/**/*#{Suffix}"].select { |file| File.file?(file) }.map { |file|
61
+ file[slice]
62
+ }
63
+ end
64
+
65
+ # returns a list with the names of active plugins
66
+ def active
67
+ @plugins.keys
68
+ end
69
+
70
+ def inactive
71
+ all-active
72
+ end
73
+
74
+ def loaded?(base)
75
+ @plugins.has_key?(base)
76
+ end
77
+
78
+ def [](base)
79
+ @plugins[base]
80
+ end
81
+
82
+ def identify(name)
83
+ plugin = Dir["#{@dir}/**/*#{name}#{Suffix}"].first
84
+ plugin && plugin[@dir.length+1..-(Suffix.length+1)]
85
+ end
86
+
87
+ def load_all
88
+ all.each { |base|
89
+ load(base)
90
+ }
91
+ end
92
+
93
+ def load_group(group)
94
+ slice = @dir.length+1..-(Suffix.length+1)
95
+ Dir["#{@dir}/#{group}/**/*#{Suffix}"].select { |file| File.file?(file) }.each { |file|
96
+ load(file[slice])
97
+ }
98
+ end
99
+
100
+ def load(base)
101
+ path = "#{@dir}/#{base.downcase}#{Suffix}"
102
+ name = File.basename(base).camelcase
103
+ codefile = path
104
+
105
+ unless File.file?(codefile) then
106
+ raise ArgumentError, "Invalid plugin '#{base}' (codefile: #{codefile})."
107
+ end
108
+ begin
109
+ unload(base) if loaded?(base) # a plugin may not be loaded twice
110
+ rescue Exception => e
111
+ exception(e)
112
+ end
113
+ begin
114
+ constant = "%s_%08X" % [name, rand(0xffffffff)]
115
+ end while Butler::Plugins.const_defined?(constant)
116
+ plugin = Butler::Plugins.const_set(constant, Class.new(Plugin))
117
+ plugin.log_device = @butler.log_device
118
+ begin
119
+ plugin.load_plugin(@butler, base, path)
120
+ plugin.class_eval(File.read(codefile), codefile)
121
+ plugin.on_load
122
+ rescue Exception => e
123
+ e.extend Exception::Detailed
124
+ e.prepend "Loading plugin #{base} failed."
125
+ exception(e)
126
+ end
127
+ @constants[base] = constant.freeze
128
+ @plugins[base] = plugin
129
+ end
130
+ alias reload load
131
+
132
+ def unload(base)
133
+ begin
134
+ @plugins[base].on_unload
135
+ @plugins[base].unload_plugin
136
+ rescue Exception => e
137
+ e.extend Exception::Detailed
138
+ e.prepend "Exception raised while unloading plugin #{base}."
139
+ exception(e)
140
+ end
141
+ Butler::Plugins.send(:remove_const, @constants[base])
142
+ @plugins.delete(base)
143
+ @constants.delete(base)
144
+ true
145
+ end
146
+ end
147
+ end