4 BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
5 :default => [], :wizard => false, :requires_restart => true,
6 :desc => "Plugins that should not be loaded")
8 require 'rbot/messagemapper'
11 base class for all rbot plugins
12 certain methods will be called if they are provided, if you define one of
13 the following methods, it will be called as appropriate:
15 map(template, options)::
16 map!(template, options)::
17 map is the new, cleaner way to respond to specific message formats
18 without littering your plugin code with regexps. The difference
19 between map and map! is that map! will not register the new command
20 as an alternative name for the plugin.
24 plugin.map 'karmastats', :action => 'karma_stats'
26 # while in the plugin...
27 def karma_stats(m, params)
31 # the default action is the first component
34 # attributes can be pulled out of the match string
35 plugin.map 'karma for :key'
36 plugin.map 'karma :key'
38 # while in the plugin...
41 m.reply 'karma for #{item}'
44 # you can setup defaults, to make parameters optional
45 plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
47 # the default auth check is also against the first component
48 # but that can be changed
49 plugin.map 'karmastats', :auth => 'karma'
51 # maps can be restricted to public or private message:
52 plugin.map 'karmastats', :private false,
53 plugin.map 'karmastats', :public false,
57 Called for all messages of any type. To
58 differentiate them, use message.kind_of? It'll be
59 either a PrivMessage, NoticeMessage, KickMessage,
60 QuitMessage, PartMessage, JoinMessage, NickMessage,
63 privmsg(PrivMessage)::
64 called for a PRIVMSG if the first word matches one
65 the plugin register()d for. Use m.plugin to get
66 that word and m.params for the rest of the message,
70 Called when a user (or the bot) is kicked from a
71 channel the bot is in.
74 Called when a user (or the bot) joins a channel
77 Called when a user (or the bot) parts a channel
80 Called when a user (or the bot) quits IRC
83 Called when a user (or the bot) changes Nick
85 Called when a user (or the bot) changes a channel
88 connect():: Called when a server is joined successfully, but
89 before autojoin channels are joined (no params)
91 save:: Called when you are required to save your plugin's
92 state, if you maintain data between sessions
94 cleanup:: called before your plugin is "unloaded", prior to a
95 plugin reload or bot quit - close any open
96 files/connections or flush caches here
100 attr_reader :bot # the associated bot
101 # initialise your bot module. Always call super if you override this method,
102 # as important variables are set up for you
104 @bot = Plugins.pluginmanager.bot
105 @botmodule_triggers = Array.new
106 @handler = MessageMapper.new(self)
107 @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
111 # debug "Flushing #{@registry}"
116 # debug "Closing #{@registry}"
127 name = @handler.last.items[0]
129 unless self.respond_to?('privmsg')
139 name = @handler.last.items[0]
140 self.register name, {:hidden => true}
141 unless self.respond_to?('privmsg')
148 # return an identifier for this plugin, defaults to a list of the message
149 # prefixes handled (used for error messages etc)
151 self.class.downcase.sub(/(plugin)?$/,"")
154 # return a help string for your module. for complex modules, you may wish
155 # to break your help into topics, and return a list of available topics if
156 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
157 # this message - if your plugin handles multiple prefixes, make sure you
158 # return the correct help for the prefix requested
159 def help(plugin, topic)
163 # register the plugin as a handler for messages prefixed +name+
164 # this can be called multiple times for a plugin to handle multiple
166 def register(name, kl, opts={})
167 raise ArgumentError, "Third argument must be a hash!" unless opts.kind_of?(Hash)
168 return if Plugins.pluginmanager.botmodules[kl].has_key?(name)
169 Plugins.pluginmanager.botmodules[kl][name] = self
170 @botmodule_triggers << name unless opts.fetch(:hidden, false)
173 # default usage method provided as a utility for simple plugins. The
174 # MessageMapper uses 'usage' as its default fallback method.
175 def usage(m, params = {})
176 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
181 class CoreBotModule < BotModule
182 def register(name, opts={})
183 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
184 super(name, :core, opts)
188 class Plugin < BotModule
189 def register(name, opts={})
190 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
191 super(name, :plugin, opts)
195 # class to manage multiple plugins and delegate messages to them for
197 class PluginManagerClass
200 attr_reader :botmodules
206 # Associate with bot _bot_
207 def bot_associate(bot)
213 # associated IrcBot class
217 # Returns a hash of the registered message prefixes and associated
223 # Returns a hash of the registered message prefixes and associated
229 # Makes a string of error _err_ by adding text _str_
230 def report_error(str, err)
231 ([str, err.inspect] + err.backtrace).join("\n")
234 # This method is the one that actually loads a module from the
237 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
239 # It returns the Symbol :loaded on success, and an Exception
242 def load_botmodule_file(fname, desc=nil)
243 # create a new, anonymous module to "house" the plugin
244 # the idea here is to prevent namespace pollution. perhaps there
246 plugin_module = Module.new
248 desc = desc.to_s + " " if desc
250 plugin_string = IO.readlines(fname).join("")
251 debug "loading #{desc}#{fname}"
252 plugin_module.module_eval(plugin_string, fname)
254 rescue Exception => err
255 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
256 warning report_error("#{desc}#{fname} load failed", err)
257 bt = err.backtrace.select { |line|
258 line.match(/^(\(eval\)|#{fname}):\d+/)
261 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
265 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
268 newerr = err.class.new(msg)
269 newerr.set_backtrace(bt)
273 private :load_botmodule_file
275 # Load core botmodules
277 # TODO FIXME should this be hardcoded?
278 if(FileTest.directory?(dir))
281 next unless(file =~ /[^.]\.rb$/)
283 did_it = load_botmodule_file("#{dir}/#{file}", "core module")
286 # debug "loaded core botmodule #{dir}/#{file}"
288 raise "failed to load core botmodule #{dir}/#{file}!"
294 # dirlist:: array of directories to scan (in order) for plugins
296 # create a new plugin handler, scanning for plugins in +dirlist+
297 def load_plugins(dirlist)
302 # load plugins from pre-assigned list of directories
308 @bot.config['plugins.blacklist'].each { |p|
310 processed[pn.intern] = :blacklisted
314 # TODO FIXME should this be hardcoded?
315 dirs << Config::datadir + "/plugins"
317 dirs.reverse.each {|dir|
318 if(FileTest.directory?(dir))
322 next if(file =~ /^\./)
324 if processed.has_key?(file.intern)
325 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
329 if(file =~ /^(.+\.rb)\.disabled$/)
330 # GB: Do we want to do this? This means that a disabled plugin in a directory
331 # will disable in all subsequent directories. This was probably meant
332 # to be used before plugins.blacklist was implemented, so I think
333 # we don't need this anymore
334 processed[$1.intern] = :disabled
335 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
339 next unless(file =~ /\.rb$/)
341 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
344 processed[file.intern] = did_it
346 @failed << { :name => file, :dir => dir, :reason => did_it }
354 # call the save method for each active plugin
356 delegate 'flush_registry'
360 # call the cleanup method for each active plugin
365 # drop all plugins and rescan plugins on disk
366 # calls save and cleanup for each plugin before dropping them
374 def status(short=false)
375 # Active plugins first
377 list = "#{self.length} plugin#{'s' if length > 1}"
381 list << ": " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ")
384 list = "no plugins active"
386 # Ignored plugins next
387 unless @ignored.empty?
388 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
389 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
391 # Failed plugins next
392 unless @failed.empty?
393 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
394 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
399 # return list of help topics (plugin names)
401 return " [#{status}]"
405 plugins.values.uniq.length
408 # return help for +topic+ (call associated plugin's help method)
411 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
412 # debug "Failures: #{@failed.inspect}"
413 return "no plugins failed to load" if @failed.empty?
414 return (@failed.inject(Array.new) { |list, p|
415 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
416 list << "with error #{p[:reason].class}: #{p[:reason]}"
417 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
420 when /ignored?\s*plugins?/
421 return "no plugins were ignored" if @ignored.empty?
422 return (@ignored.inject(Array.new) { |list, p|
425 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
427 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
431 when /^(\S+)\s*(.*)$/
434 if(@@plugins.has_key?(key))
436 return @@plugins[key].help(key, params)
437 rescue Exception => err
438 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
439 error report_error("plugin #{@@plugins[key].name} help() failed:", err)
447 # see if each plugin handles +method+, and if so, call it, passing
448 # +message+ as a parameter
449 def delegate(method, *args)
450 [core_modules, plugins].each { |pl|
451 pl.values.uniq.each {|p|
452 if(p.respond_to? method)
455 rescue Exception => err
456 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
457 error report_error("plugin #{p.name} #{method}() failed:", err)
464 # see if we have a plugin that wants to handle this message, if so, pass
465 # it to the plugin and return true, otherwise false
467 [core_modules, plugins].each { |pl|
468 return unless(m.plugin)
469 if (pl.has_key?(m.plugin) &&
470 pl[m.plugin].respond_to?("privmsg") &&
471 @bot.auth.allow?(m.plugin, m.source, m.replyto))
473 pl[m.plugin].privmsg(m)
474 rescue BDB::Fatal => err
475 error error_report("plugin #{pl[m.plugin].name} privmsg() failed:", err)
477 rescue Exception => err
478 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
479 error "plugin #{pl[m.plugin].name} privmsg() failed: #{err.class}: #{err}\n#{error err.backtrace.join("\n")}"
488 # Returns the only PluginManagerClass instance
489 def Plugins.pluginmanager
490 return PluginManagerClass.instance