require 'singleton'
module Irc
- BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
+class Bot
+ Config.register Config::ArrayValue.new('plugins.blacklist',
:default => [], :wizard => false, :requires_rescan => true,
:desc => "Plugins that should not be loaded")
module Plugins
functionality. Rather than subclassing BotModule, however, one should
subclass either CoreBotModule (reserved for system modules) or Plugin
(for user plugins).
-
+
A BotModule interacts with Irc events by defining one or more of the following
methods, which get called as appropriate when the corresponding Irc event
happens.
map is the new, cleaner way to respond to specific message formats without
littering your plugin code with regexps, and should be used instead of
#register() and #privmsg() (see below) when possible.
-
+
The difference between map and map! is that map! will not register the new
command as an alternative name for the plugin.
use message.ctcp_reply, which sends a private NOTICE
to the sender.
+ message(PrivMessage)::
+ Called for all PRIVMSG. Hook on this method if you
+ need to handle PRIVMSGs regardless of whether they are
+ addressed to the bot or not, and regardless of
+
privmsg(PrivMessage)::
Called for a PRIVMSG if the first word matches one
the plugin #register()ed for. Use m.plugin to get
unreplied(PrivMessage)::
Called for a PRIVMSG which has not been replied to.
+ notice(NoticeMessage)::
+ Called for all Notices. Please notice that in general
+ should not be replied to.
+
kick(KickMessage)::
Called when a user (or the bot) is kicked from a
channel the bot is in.
+ invite(InviteMessage)::
+ Called when the bot is invited to a channel.
+
join(JoinMessage)::
Called when a user (or the bot) joins a channel
nick(NickMessage)::
Called when a user (or the bot) changes Nick
+ modechange(ModeChangeMessage)::
+ Called when a User or Channel mode is changed
topic(TopicMessage)::
Called when a user (or the bot) changes a channel
topic
+ welcome(WelcomeMessage)::
+ Called when the welcome message is received on
+ joining a server succesfully.
+
+ motd(MotdMessage)::
+ Called when the Message Of The Day is fully
+ recevied from the server.
+
connect:: Called when a server is joined successfully, but
before autojoin channels are joined (no params)
=end
class BotModule
- attr_reader :bot # the associated bot
+ # the associated bot
+ attr_reader :bot
+
+ # the plugin registry
+ attr_reader :registry
+
+ # the message map handler
+ attr_reader :handler
# Initialise your bot module. Always call super if you override this method,
# as important variables are set up for you:
# the rbot instance
# @registry::
# the botmodule's registry, which can be used to store permanent data
- # (see BotRegistryAccessor for additional documentation)
+ # (see Registry::Accessor for additional documentation)
#
# Other instance variables which are defined and should not be overwritten
# byt the user, but aren't usually accessed directly, are:
@botmodule_triggers = Array.new
@handler = MessageMapper.new(self)
- @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
+ @registry = Registry::Accessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
@manager.add_botmodule(self)
if self.respond_to?('set_language')
end
end
+ # Changing the value of @priority directly will cause problems,
+ # Please use priority=.
+ def priority
+ @priority ||= 1
+ end
+
# Returns the symbol :BotModule
def botmodule_class
:BotModule
# Signal to other BotModules that an even happened.
#
def call_event(ev, *args)
- @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *args)
+ @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new))
end
# call-seq: map(template, options)
# responds to appropriately-formed messages on Irc.
#
def map(*args)
- @handler.map(self, *args)
- # register this map
- name = @handler.last.items[0]
- self.register name, :auth => nil
- unless self.respond_to?('privmsg')
- def self.privmsg(m) #:nodoc:
- handle(m)
- end
- end
+ do_map(false, *args)
end
# call-seq: map!(template, options)
# as an alternative name for the plugin.
#
def map!(*args)
+ do_map(true, *args)
+ end
+
+ # Auxiliary method called by #map and #map!
+ def do_map(silent, *args)
@handler.map(self, *args)
# register this map
- name = @handler.last.items[0]
- self.register name, :auth => nil, :hidden => true
+ map = @handler.last
+ name = map.items[0]
+ self.register name, :auth => nil, :hidden => silent
+ @manager.register_map(self, map)
unless self.respond_to?('privmsg')
def self.privmsg(m) #:nodoc:
handle(m)
m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
end
+ # Define the priority of the module. During event delegation, lower
+ # priority modules will be called first. Default priority is 1
+ def priority=(prio)
+ if @priority != prio
+ @priority = prio
+ @bot.plugins.mark_priorities_dirty
+ end
+ end
end
# A CoreBotModule is a BotModule that provides core functionality.
include Singleton
attr_reader :bot
attr_reader :botmodules
+ attr_reader :maps
# This is the list of patterns commonly delegated to plugins.
# A fast delegation lookup is enabled for them.
@names_hash = Hash.new
@commandmappers = Hash.new
+ @maps = Hash.new
+
+ # modules will be sorted on first delegate call
+ @sorted_modules = nil
+
@delegate_list = Hash.new { |h, k|
h[k] = Array.new
}
bot_associate(nil)
end
+ def inspect
+ ret = self.to_s[0..-2]
+ ret << ' corebotmodules='
+ ret << @botmodules[:CoreBotModule].map { |m|
+ m.name
+ }.inspect
+ ret << ' plugins='
+ ret << @botmodules[:Plugin].map { |m|
+ m.name
+ }.inspect
+ ret << ">"
+ end
+
# Reset lists of botmodules
def reset_botmodule_lists
@botmodules[:CoreBotModule].clear
@botmodules[:Plugin].clear
@names_hash.clear
@commandmappers.clear
+ @maps.clear
@failures_shown = false
+ mark_priorities_dirty
end
# Associate with bot _bot_
@commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
end
+ # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
+ # which has three keys:
+ #
+ # botmodule:: the associated botmodule
+ # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
+ # map:: the actual MessageTemplate object
+ #
+ #
+ def register_map(botmodule, map)
+ raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
+ @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
+ end
+
def add_botmodule(botmodule)
raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
kl = botmodule.botmodule_class
end
@botmodules[kl] << botmodule
@names_hash[botmodule.to_sym] = botmodule
+ mark_priorities_dirty
end
# Returns an array of the loaded plugins
@commandmappers
end
+ # Tells the PluginManager that the next time it delegates an event, it
+ # should sort the modules by priority
+ def mark_priorities_dirty
+ @sorted_modules = nil
+ end
+
# Makes a string of error _err_ by adding text _str_
def report_error(str, err)
([str, err.inspect] + err.backtrace).join("\n")
@delegate_list[m.intern] << p
}
}
+ mark_priorities_dirty
end
# call the save method for each active plugin
key = $1
params = $2
- # Let's see if we can match a plugin by the given name
+ # Let's see if we can match a plugin by the given name
(core_modules + plugins).each { |p|
- next unless p.name == key
+ next unless p.name == key
begin
return p.help(key, params)
rescue Exception => err
end
}
- # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
+ # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
k = key.to_sym
if commands.has_key?(k)
p = commands[k][:botmodule]
return false
end
- # see if each plugin handles +method+, and if so, call it, passing
- # +message+ as a parameter
+ def sort_modules
+ @sorted_modules = (core_modules + plugins).sort do |a, b|
+ a.priority <=> b.priority
+ end || []
+
+ @delegate_list.each_value do |list|
+ list.sort! {|a,b| a.priority <=> b.priority}
+ end
+ end
+
+ # call-seq: delegate</span><span class="method-args">(method, m, opts={})</span>
+ # <span class="method-name">delegate</span><span class="method-args">(method, opts={})
+ #
+ # see if each plugin handles _method_, and if so, call it, passing
+ # _m_ as a parameter (if present). BotModules are called in order of
+ # priority from lowest to highest.
+ #
+ # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
+ # will only be delegated to plugins with negative priority. Conversely, if
+ # it's a fake message (see BotModule#fake_message), it will only be
+ # delegated to plugins with positive priority.
+ #
+ # Note that _m_ can also be an exploded Array, but in this case the last
+ # element of it cannot be a Hash, or it will be interpreted as the options
+ # Hash for delegate itself. The last element can be a subclass of a Hash, though.
+ # To be on the safe side, you can add an empty Hash as last parameter for delegate
+ # when calling it with an exploded Array:
+ # @bot.plugins.delegate(method, *(args.push Hash.new))
+ #
+ # Currently supported options are the following:
+ # :above ::
+ # if specified, the delegation will only consider plugins with a priority
+ # higher than the specified value
+ # :below ::
+ # if specified, the delegation will only consider plugins with a priority
+ # lower than the specified value
+ #
def delegate(method, *args)
+ # if the priorities order of the delegate list is dirty,
+ # meaning some modules have been added or priorities have been
+ # changed, then the delegate list will need to be sorted before
+ # delegation. This should always be true for the first delegation.
+ sort_modules unless @sorted_modules
+
+ opts = {}
+ opts.merge(args.pop) if args.last.class == Hash
+
+ m = args.first
+ if BasicUserMessage === m
+ # ignored messages should not be delegated
+ # to plugins with positive priority
+ opts[:below] ||= 0 if m.ignored?
+ # fake messages should not be delegated
+ # to plugins with negative priority
+ opts[:above] ||= 0 if m.recurse_depth > 0
+ end
+
+ above = opts[:above]
+ below = opts[:below]
+
# debug "Delegating #{method.inspect}"
ret = Array.new
if method.match(DEFAULT_DELEGATE_PATTERNS)
return [] unless @delegate_list.has_key?(m)
@delegate_list[m].each { |p|
begin
- ret.push p.send(method, *args)
+ prio = p.priority
+ unless (above and above >= prio) or (below and below <= prio)
+ ret.push p.send(method, *args)
+ end
rescue Exception => err
raise if err.kind_of?(SystemExit)
error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
}
else
debug "slow-delegating #{method}"
- (core_modules + plugins).each { |p|
+ @sorted_modules.each { |p|
if(p.respond_to? method)
begin
# debug "#{p.botmodule_class} #{p.name} responds"
- ret.push p.send(method, *args)
+ prio = p.priority
+ unless (above and above >= prio) or (below and below <= prio)
+ ret.push p.send(method, *args)
+ end
rescue Exception => err
raise if err.kind_of?(SystemExit)
error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
# see if we have a plugin that wants to handle this message, if so, pass
# it to the plugin and return true, otherwise false
def privmsg(m)
- # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
+ debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
return unless m.plugin
k = m.plugin.to_sym
if commands.has_key?(k)
a = commands[k][:auth]
# We check here for things that don't check themselves
# (e.g. mapped things)
- # debug "Checking auth ..."
+ debug "Checking auth ..."
if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
- # debug "Checking response ..."
+ debug "Checking response ..."
if p.respond_to?("privmsg")
begin
- # debug "#{p.botmodule_class} #{p.name} responds"
+ debug "#{p.botmodule_class} #{p.name} responds"
p.privmsg(m)
rescue Exception => err
raise if err.kind_of?(SystemExit)
error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
raise if err.kind_of?(BDB::Fatal)
end
- # debug "Successfully delegated #{m.message}"
+ debug "Successfully delegated #{m.inspect}"
return true
else
- # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
+ debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
end
else
- # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
+ debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
end
+ else
+ debug "Command #{k} isn't handled"
end
- # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
return false
- # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
+ end
+
+ # delegate IRC messages, by delegating 'listen' first, and the actual method
+ # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
+ # as appropriate.
+ def irc_delegate(method, m)
+ delegate('listen', m)
+ if method.to_sym == :privmsg
+ delegate('ctcp_listen', m) if m.ctcp
+ delegate('message', m)
+ privmsg(m) if m.address?
+ delegate('unreplied', m) unless m.replied
+ else
+ delegate(method, m)
+ end
end
end
end
end
+end