summaryrefslogtreecommitdiff
path: root/rbot
diff options
context:
space:
mode:
Diffstat (limited to 'rbot')
-rw-r--r--rbot/auth.rb2
-rw-r--r--rbot/config.rb205
-rw-r--r--rbot/ircbot.rb64
-rw-r--r--rbot/ircsocket.rb56
-rw-r--r--rbot/keywords.rb4
-rw-r--r--rbot/message.rb9
-rw-r--r--rbot/messagemapper.rb158
-rw-r--r--rbot/plugins.rb66
-rw-r--r--rbot/plugins/autoop.rb4
-rw-r--r--rbot/plugins/cal.rb9
-rw-r--r--rbot/plugins/eightball.rb5
-rw-r--r--rbot/plugins/karma.rb93
-rw-r--r--rbot/plugins/lart.rb8
-rw-r--r--rbot/plugins/nickserv.rb10
-rw-r--r--rbot/plugins/opmeh.rb1
-rw-r--r--rbot/plugins/quotes.rb4
-rw-r--r--rbot/plugins/remind.rb2
-rw-r--r--rbot/plugins/roulette.rb2
18 files changed, 547 insertions, 155 deletions
diff --git a/rbot/auth.rb b/rbot/auth.rb
index 017745ab..7811d9e4 100644
--- a/rbot/auth.rb
+++ b/rbot/auth.rb
@@ -182,7 +182,7 @@ module Irc
m.reply "user #$1 is gone"
end
when (/^auth\s+(\S+)/)
- if($1 == @bot.config["PASSWD"])
+ if($1 == @bot.config["auth.password"])
@bot.auth.useradd(Regexp.escape(m.source), 1000)
m.reply "Identified, security level maxed out"
else
diff --git a/rbot/config.rb b/rbot/config.rb
index 52899205..971a413c 100644
--- a/rbot/config.rb
+++ b/rbot/config.rb
@@ -1,40 +1,205 @@
module Irc
+ require 'yaml'
+
# container for bot configuration
- # just treat it like a hash
- class BotConfig < Hash
+ class BotConfig
+
+ # currently we store values in a hash but this could be changed in the
+ # future. We use hash semantics, however.
+ def method_missing(method, *args, &block)
+ return @config.send(method, *args, &block)
+ end
# bot:: parent bot class
# create a new config hash from #{botclass}/conf.rbot
def initialize(bot)
- super(false)
@bot = bot
# some defaults
- self["SERVER"] = "localhost"
- self["PORT"] = "6667"
- self["NICK"] = "rbot"
- self["USER"] = "gilbertt"
- self["LANGUAGE"] = "english"
- self["SAVE_EVERY"] = "60"
- self["KEYWORD_LISTEN"] = false
- if(File.exist?("#{@bot.botclass}/conf.rbot"))
- IO.foreach("#{@bot.botclass}/conf.rbot") do |line|
- next if(line =~ /^\s*#/)
- if(line =~ /(\S+)\s+=\s+(.*)$/)
- self[$1] = $2 if($2)
- end
- end
+ @config = Hash.new(false)
+
+ @config['server.name'] = "localhost"
+ @config['server.port'] = 6667
+ @config['server.password'] = false
+ @config['server.bindhost'] = false
+ @config['irc.nick'] = "rbot"
+ @config['irc.user'] = "rbot"
+ @config['irc.join_channels'] = ""
+ @config['core.language'] = "english"
+ @config['core.save_every'] = 60
+ @config['keyword.listen'] = false
+ @config['auth.password'] = ""
+ @config['server.sendq_delay'] = 2.0
+ @config['server.sendq_burst'] = 4
+ @config['keyword.address'] = true
+ @config['keyword.listen'] = false
+
+ # TODO
+ # have this class persist key/values in hash using yaml as it kinda
+ # already does.
+ # have other users of the class describe config to it on init, like:
+ # @config.add(:key => 'server.name', :type => 'string',
+ # :default => 'localhost', :restart => true,
+ # :help => 'irc server to connect to')
+ # that way the config module doesn't have to know about all the other
+ # classes but can still provide help and defaults.
+ # Classes don't have to add keys, they can just use config as a
+ # persistent hash, but then they won't be presented by the config
+ # module for runtime display/changes.
+ # (:restart, if true, makes the bot reply to changes with "this change
+ # will take effect after the next restart)
+ # :proc => Proc.new {|newvalue| ...}
+ # (:proc, proc to run on change of setting)
+ # or maybe, @config.add_key(...) do |newvalue| .... end
+ # :validate => /regex/
+ # (operates on received string before conversion)
+ # Special handling for arrays so the config module can be used to
+ # add/remove elements as well as changing the whole thing
+ # Allow config options to list possible valid values (if type is enum,
+ # for example). Then things like the language module can list the
+ # available languages for choosing.
+
+ if(File.exist?("#{@bot.botclass}/conf.yaml"))
+ newconfig = YAML::load_file("#{@bot.botclass}/conf.yaml")
+ @config.update(newconfig)
+ else
+ # first-run wizard!
+ wiz = BotConfigWizard.new(@bot)
+ newconfig = wiz.run(@config)
+ @config.update(newconfig)
end
end
# write current configuration to #{botclass}/conf.rbot
def save
Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}"))
- File.open("#{@bot.botclass}/conf.rbot", "w") do |file|
- self.each do |key, value|
- file.puts "#{key} = #{value}"
+ File.open("#{@bot.botclass}/conf.yaml", "w") do |file|
+ file.puts @config.to_yaml
+ end
+ end
+ end
+
+ # I don't see a nice way to avoid the first start wizard knowing way too
+ # much about other modules etc, because it runs early and stuff it
+ # configures is used to initialise the other modules...
+ # To minimise this we'll do as little as possible and leave the rest to
+ # online modification
+ class BotConfigWizard
+
+ # TODO things to configure..
+ # config directory (botclass) - people don't realise they should set
+ # this. The default... isn't good.
+ # users? - default *!*@* to 10
+ # levels? - need a way to specify a default level, methinks, for
+ # unconfigured items.
+ #
+ def initialize(bot)
+ @bot = bot
+ @questions = [
+ {
+ :question => "What server should the bot connect to?",
+ :prompt => "Hostname",
+ :key => "server.name",
+ :type => :string,
+ },
+ {
+ :question => "What port should the bot connect to?",
+ :prompt => "Port",
+ :key => "server.port",
+ :type => :number,
+ },
+ {
+ :question => "Does this IRC server require a password for access? Leave blank if not.",
+ :prompt => "Password",
+ :key => "server.password",
+ :type => :password,
+ },
+ {
+ :question => "Would you like rbot to bind to a specific local host or IP? Leave blank if not.",
+ :prompt => "Local bind",
+ :key => "server.bindhost",
+ :type => :string,
+ },
+ {
+ :question => "What IRC nickname should the bot attempt to use?",
+ :prompt => "Nick",
+ :key => "irc.nick",
+ :type => :string,
+ },
+ {
+ :question => "What local user should the bot appear to be?",
+ :prompt => "User",
+ :key => "irc.user",
+ :type => :string,
+ },
+ {
+ :question => "What channels should the bot always join at startup? List multiple channels using commas to separate. If a channel requires a password, use a space after the channel name. e.g: '#chan1, #chan2, #secretchan secritpass, #chan3'",
+ :prompt => "Channels",
+ :key => "irc.join_channels",
+ :type => :string,
+ },
+ {
+ :question => "Which language file should the bot use?",
+ :prompt => "Language",
+ :key => "core.language",
+ :type => :enum,
+ :items => Dir.new(File.dirname(__FILE__) + "/languages/").collect {|f|
+ f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil
+ }.compact
+ },
+ {
+ :question => "Enter your password for maxing your auth with the bot (used to associate new hostmasks with your owner-status etc)",
+ :prompt => "Password",
+ :key => "auth.password",
+ :type => :password,
+ },
+ ]
+ end
+
+ def run(defaults)
+ config = defaults.clone
+ puts "First time rbot configuration wizard"
+ puts "===================================="
+ puts "This is the first time you have run rbot with a config directory of:"
+ puts @bot.botclass
+ puts "This wizard will ask you a few questions to get you started."
+ puts "The rest of rbot's configuration can be manipulated via IRC once"
+ puts "rbot is connected and you are auth'd."
+ puts "-----------------------------------"
+
+ @questions.each do |q|
+ puts q[:question]
+ begin
+ key = q[:key]
+ if q[:type] == :enum
+ puts "valid values are: " + q[:items].join(", ")
+ end
+ if (defaults.has_key?(key))
+ print q[:prompt] + " [#{defaults[key]}]: "
+ else
+ print q[:prompt] + " []: "
+ end
+ response = STDIN.gets
+ response.chop!
+ response = defaults[key] if response == "" && defaults.has_key?(key)
+ case q[:type]
+ when :string
+ when :number
+ raise "value '#{response}' is not a number" unless (response.class == Fixnum || response =~ /^\d+$/)
+ response = response.to_i
+ when :password
+ when :enum
+ raise "selected value '#{response}' is not one of the valid values" unless q[:items].include?(response)
+ end
+ config[key] = response
+ puts "configured #{key} => #{config[key]}"
+ puts "-----------------------------------"
+ rescue RuntimeError => e
+ puts e.message
+ retry
end
end
+ return config
end
end
end
diff --git a/rbot/ircbot.rb b/rbot/ircbot.rb
index 7129c105..844231dd 100644
--- a/rbot/ircbot.rb
+++ b/rbot/ircbot.rb
@@ -86,20 +86,20 @@ class IrcBot
@config = Irc::BotConfig.new(self)
@timer = Timer::Timer.new
@registry = BotRegistry.new self
- @timer.add(@config["SAVE_EVERY"].to_i) { save }
+ @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
@channels = Hash.new
@logs = Hash.new
@httputil = Irc::HttpUtil.new(self)
- @lang = Irc::Language.new(@config["LANGUAGE"])
+ @lang = Irc::Language.new(@config['core.language'])
@keywords = Irc::Keywords.new(self)
@auth = Irc::IrcAuth.new(self)
@plugins = Irc::Plugins.new(self, ["#{botclass}/plugins"])
- @socket = Irc::IrcSocket.new(@config["SERVER"], @config["PORT"], @config["HOST"], @config["SENDQ_DELAY"], @config["SENDQ_BURST"])
- @nick = @config["NICK"]
- @server_password = @config["SERVER_PASSWORD"]
- if @config["ADDRESS_PREFIX"]
- @addressing_prefixes = @config["ADDRESS_PREFIX"].split(" ")
+
+ @socket = Irc::IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
+ @nick = @config['irc.nick']
+ if @config['core.address_prefix']
+ @addressing_prefixes = @config['core.address_prefix'].split(" ")
else
@addressing_prefixes = Array.new
end
@@ -179,13 +179,13 @@ class IrcBot
if data['NICK'] && data['NICK'].length > 0
@nick = data['NICK']
end
- if(@config["QUSER"])
- puts "authing with Q using #{@config["QUSER"]} #{@config["QAUTH"]}"
- @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config["QUSER"]} #{@config["QAUTH"]}"
+ if(@config['irc.quser'])
+ puts "authing with Q using #{@config['quakenet.user']} #{@config['quakenet.auth']}"
+ @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config['quakenet.user']} #{@config['quakenet.auth']}"
end
- if(@config["JOIN_CHANNELS"])
- @config["JOIN_CHANNELS"].split(", ").each {|c|
+ if(@config['irc.join_channels'])
+ @config['irc.join_channels'].split(", ").each {|c|
puts "autojoining channel #{c}"
if(c =~ /^(\S+)\s+(\S+)$/i)
join $1, $2
@@ -225,8 +225,8 @@ class IrcBot
m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], timestamp, data["TOPIC"])
ontopic(m)
- @plugins.delegate("topic", m)
@plugins.delegate("listen", m)
+ @plugins.delegate("topic", m)
}
@client["TOPIC"] = @client["TOPICINFO"] = proc {|data|
channel = data["CHANNEL"]
@@ -258,10 +258,10 @@ class IrcBot
begin
@socket.connect
rescue => e
- raise "failed to connect to IRC server at #{@config['SERVER']} #{@config['PORT']}: " + e
+ raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
end
- @socket.puts "PASS " + @server_password if @server_password
- @socket.puts "NICK #{@nick}\nUSER #{@config['USER']} 4 #{@config['SERVER']} :Ruby bot. (c) Tom Gilbert"
+ @socket.puts "PASS " + @config['server.password'] if @config['server.password']
+ @socket.puts "NICK #{@nick}\nUSER #{@config['server.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
end
# begin event handling loop
@@ -563,8 +563,8 @@ class IrcBot
join 0 if(@auth.allow?("join", m.source, m.replyto))
when (/^save$/i)
if(@auth.allow?("config", m.source, m.replyto))
- okay m.replyto
save
+ m.okay
end
when (/^nick\s+(\S+)$/i)
nickchg($1) if(@auth.allow?("nick", m.source, m.replyto))
@@ -580,55 +580,55 @@ class IrcBot
say m.replyto, "pong"
when (/^rescan$/i)
if(@auth.allow?("config", m.source, m.replyto))
- okay m.replyto
+ m.okay
rescan
end
when (/^quiet$/i)
if(auth.allow?("talk", m.source, m.replyto))
- say m.replyto, @lang.get("okay")
+ m.okay
@channels.each_value {|c| c.quiet = true }
end
when (/^quiet in (\S+)$/i)
where = $1
if(auth.allow?("talk", m.source, m.replyto))
- say m.replyto, @lang.get("okay")
+ m.okay
where.gsub!(/^here$/, m.target) if m.public?
@channels[where].quiet = true if(@channels.has_key?(where))
end
when (/^talk$/i)
if(auth.allow?("talk", m.source, m.replyto))
@channels.each_value {|c| c.quiet = false }
- okay m.replyto
+ m.okay
end
when (/^talk in (\S+)$/i)
where = $1
if(auth.allow?("talk", m.source, m.replyto))
where.gsub!(/^here$/, m.target) if m.public?
@channels[where].quiet = false if(@channels.has_key?(where))
- okay m.replyto
+ m.okay
end
- # TODO break this out into an options module
+ # TODO break this out into a config module
when (/^options get sendq_delay$/i)
if auth.allow?("config", m.source, m.replyto)
- m.reply "options->sendq_delay = #{@socket.get_sendq}"
+ m.reply "options->sendq_delay = #{@socket.sendq_delay}"
end
when (/^options get sendq_burst$/i)
if auth.allow?("config", m.source, m.replyto)
- m.reply "options->sendq_burst = #{@socket.get_maxburst}"
+ m.reply "options->sendq_burst = #{@socket.sendq_burst}"
end
when (/^options set sendq_burst (.*)$/i)
num = $1.to_i
if auth.allow?("config", m.source, m.replyto)
- @socket.set_maxburst(num)
- @config["SENDQ_BURST"] = num
- okay m.replyto
+ @socket.sendq_burst = num
+ @config['irc.sendq_burst'] = num
+ m.okay
end
when (/^options set sendq_delay (.*)$/i)
freq = $1.to_f
if auth.allow?("config", m.source, m.replyto)
- @socket.set_sendq(freq)
- @config["SENDQ_DELAY"] = freq
- okay m.replyto
+ @socket.sendq_delay = freq
+ @config['irc.sendq_delay'] = freq
+ m.okay
end
when (/^status$/i)
m.reply status if auth.allow?("status", m.source, m.replyto)
@@ -735,7 +735,7 @@ class IrcBot
@channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
@channels[m.channel].topic.by = m.source if !m.source.nil?
- puts @channels[m.channel].topic
+ debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
end
# delegate a privmsg to auth, keyword or plugin handlers
diff --git a/rbot/ircsocket.rb b/rbot/ircsocket.rb
index 25895644..35857736 100644
--- a/rbot/ircsocket.rb
+++ b/rbot/ircsocket.rb
@@ -8,29 +8,37 @@ module Irc
class IrcSocket
# total number of lines sent to the irc server
attr_reader :lines_sent
+
# total number of lines received from the irc server
attr_reader :lines_received
+
+ # delay between lines sent
+ attr_reader :sendq_delay
+
+ # max lines to burst
+ attr_reader :sendq_burst
+
# server:: server to connect to
# port:: IRCd port
# host:: optional local host to bind to (ruby 1.7+ required)
# create a new IrcSocket
- def initialize(server, port, host, sendfreq=2, maxburst=4)
+ def initialize(server, port, host, sendq_delay=2, sendq_burst=4)
@server = server.dup
@port = port.to_i
@host = host
@lines_sent = 0
@lines_received = 0
- if sendfreq
- @sendfreq = sendfreq.to_f
+ if sendq_delay
+ @sendq_delay = sendq_delay.to_f
else
- @sendfreq = 2
+ @sendq_delay = 2
end
- @last_send = Time.new - @sendfreq
+ @last_send = Time.new - @sendq_delay
@burst = 0
- if maxburst
- @maxburst = maxburst.to_i
+ if sendq_burst
+ @sendq_burst = sendq_burst.to_i
else
- @maxburst = 4
+ @sendq_burst = 4
end
end
@@ -52,15 +60,15 @@ module Irc
@qthread = false
@qmutex = Mutex.new
@sendq = Array.new
- if (@sendfreq > 0)
+ if (@sendq_delay > 0)
@qthread = Thread.new { spooler }
end
end
- def set_sendq(newfreq)
+ def sendq_delay=(newfreq)
debug "changing sendq frequency to #{newfreq}"
@qmutex.synchronize do
- @sendfreq = newfreq
+ @sendq_delay = newfreq
if newfreq == 0 && @qthread
clearq
Thread.kill(@qthread)
@@ -71,20 +79,12 @@ module Irc
end
end
- def set_maxburst(newburst)
+ def sendq_burst=(newburst)
@qmutex.synchronize do
- @maxburst = newburst
+ @sendq_burst = newburst
end
end
- def get_maxburst
- return @maxburst
- end
-
- def get_sendq
- return @sendfreq
- end
-
# used to send lines to the remote IRCd
# message: IRC message to send
def puts(message)
@@ -106,7 +106,7 @@ module Irc
end
def queue(msg)
- if @sendfreq > 0
+ if @sendq_delay > 0
@qmutex.synchronize do
# debug "QUEUEING: #{msg}"
@sendq.push msg
@@ -120,7 +120,7 @@ module Irc
def spooler
while true
spool
- sleep 0.1
+ sleep 0.2
end
end
@@ -128,17 +128,17 @@ module Irc
def spool
unless @sendq.empty?
now = Time.new
- if (now >= (@last_send + @sendfreq))
- # reset burst counter after @sendfreq has passed
+ if (now >= (@last_send + @sendq_delay))
+ # reset burst counter after @sendq_delay has passed
@burst = 0
debug "in spool, resetting @burst"
- elsif (@burst >= @maxburst)
+ elsif (@burst >= @sendq_burst)
# nope. can't send anything
return
end
@qmutex.synchronize do
- debug "(can send #{@maxburst - @burst} lines, there are #{@sendq.length} to send)"
- (@maxburst - @burst).times do
+ debug "(can send #{@sendq_burst - @burst} lines, there are #{@sendq.length} to send)"
+ (@sendq_burst - @burst).times do
break if @sendq.empty?
puts_critical(@sendq.shift)
end
diff --git a/rbot/keywords.rb b/rbot/keywords.rb
index f1997829..3305af29 100644
--- a/rbot/keywords.rb
+++ b/rbot/keywords.rb
@@ -415,9 +415,9 @@ module Irc
end
else
# in channel message, not to me
- if(m.message =~ /^'(.*)$/ || (@bot.config["NO_KEYWORD_ADDRESS"] == "true" && m.message =~ /^(.*\S)\s*\?\s*$/))
+ if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.noaddress"] && m.message =~ /^(.*\S)\s*\?\s*$/))
keyword m, $1, false if(@bot.auth.allow?("keyword", m.source))
- elsif(@bot.config["KEYWORD_LISTEN"] == "true" && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/))
+ elsif(@bot.config["keyword.listen"] == true && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/))
# TODO MUCH more selective on what's allowed here
keyword_command(m.sourcenick, m.replyto, $1, $2, $3, true) if(@bot.auth.allow?("keycmd", m.source))
end
diff --git a/rbot/message.rb b/rbot/message.rb
index c217b1da..d7f614ab 100644
--- a/rbot/message.rb
+++ b/rbot/message.rb
@@ -5,6 +5,9 @@ module Irc
# nick/channel and a message part)
class BasicUserMessage
+ # associated bot
+ attr_reader :bot
+
# when the message was received
attr_reader :time
@@ -175,6 +178,12 @@ module Irc
@replied = true
end
+ # convenience method to reply "okay" in the current language to the
+ # message
+ def okay
+ @bot.say @replyto, @bot.lang.get("okay")
+ end
+
end
# class to manage IRC PRIVMSGs
diff --git a/rbot/messagemapper.rb b/rbot/messagemapper.rb
new file mode 100644
index 00000000..d03721c6
--- /dev/null
+++ b/rbot/messagemapper.rb
@@ -0,0 +1,158 @@
+module Irc
+ class MessageMapper
+ attr_writer :fallback
+
+ def initialize(parent)
+ @parent = parent
+ @routes = Array.new
+ @fallback = 'usage'
+ end
+
+ def map(*args)
+ @routes << Route.new(*args)
+ end
+
+ def each
+ @routes.each {|route| yield route}
+ end
+ def last
+ @routes.last
+ end
+
+ def handle(m)
+ return false if @routes.empty?
+ failures = []
+ @routes.each do |route|
+ options, failure = route.recognize(m)
+ if options.nil?
+ failures << [route, failure]
+ else
+ action = route.options[:action] ? route.options[:action] : route.items[0]
+ next unless @parent.respond_to?(action)
+ auth = route.options[:auth] ? route.options[:auth] : action
+ if m.bot.auth.allow?(auth, m.source, m.replyto)
+ debug "route found and auth'd: #{action.inspect} #{options.inspect}"
+ @parent.send(action, m, options)
+ return true
+ end
+ # if it's just an auth failure but otherwise the match is good,
+ # don't try any more handlers
+ break
+ end
+ end
+ debug failures.inspect
+ debug "no handler found, trying fallback"
+ if @fallback != nil && @parent.respond_to?(@fallback)
+ if m.bot.auth.allow?(@fallback, m.source, m.replyto)
+ @parent.send(@fallback, m, {})
+ return true
+ end
+ end
+ return false
+ end
+
+ end
+
+ class Route
+ attr_reader :defaults # The defaults hash
+ attr_reader :options # The options hash
+ attr_reader :items
+ def initialize(template, hash={})
+ raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
+ @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
+ @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
+ self.items = template
+ @options = hash
+ end
+ def items=(str)
+ items = str.split(/\s+/).collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if str.kind_of?(String) # split and convert ':xyz' to symbols
+ items.shift if items.first == ""
+ items.pop if items.last == ""
+ @items = items
+
+ if @items.first.kind_of? Symbol
+ raise ArgumentError, "Illegal template -- first component cannot be dynamic\n #{str.inspect}"
+ end
+
+ # Verify uniqueness of each component.
+ @items.inject({}) do |seen, item|
+ if item.kind_of? Symbol
+ raise ArgumentError, "Illegal template -- duplicate item #{item}\n #{str.inspect}" if seen.key? item
+ seen[item] = true
+ end
+ seen
+ end
+ end
+
+ # Recognize the provided string components, returning a hash of
+ # recognized values, or [nil, reason] if the string isn't recognized.
+ def recognize(m)
+ components = m.message.split(/\s+/)
+ options = {}
+
+ @items.each do |item|
+ if /^\*/ =~ item.to_s
+ if components.empty?
+ value = @defaults.has_key?(item) ? @defaults[item].clone : []
+ else
+ value = components.clone
+ end
+ components = []
+ def value.to_s() self.join(' ') end
+ options[item.to_s.sub(/^\*/,"").intern] = value
+ elsif item.kind_of? Symbol
+ value = components.shift || @defaults[item]
+ return nil, requirements_for(item) unless passes_requirements?(item, value)
+ options[item] = value
+ else
+ return nil, "No value available for component #{item.inspect}" if components.empty?
+ component = components.shift
+ return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
+ end
+ end
+
+ return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
+
+ return nil, "route is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private?
+ return nil, "route is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private?
+
+ options.delete_if {|k, v| v.nil?} # Remove nil values.
+ return options, nil
+ end
+
+ def inspect
+ when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
+ default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
+ "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>"
+ end
+
+ # Verify that the given value passes this route's requirements
+ def passes_requirements?(name, value)
+ return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
+
+ case @requirements[name]
+ when nil then true
+ when Regexp then
+ value = value.to_s
+ match = @requirements[name].match(value)
+ match && match[0].length == value.length
+ else
+ @requirements[name] == value.to_s
+ end
+ end
+
+ def requirements_for(name)
+ name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
+ presence = (@defaults.key?(name) && @defaults[name].nil?)
+ requirement = case @requirements[name]
+ when nil then nil
+ when Regexp then "match #{@requirements[name].inspect}"
+ else "be equal to #{@requirements[name].inspect}"
+ end
+ if presence && requirement then "#{name} must be present and #{requirement}"
+ elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
+ else "#{name} has no requirements"
+ end
+ end
+ end
+end
diff --git a/rbot/plugins.rb b/rbot/plugins.rb
index b99fe562..5db047fb 100644
--- a/rbot/plugins.rb
+++ b/rbot/plugins.rb
@@ -1,8 +1,50 @@
module Irc
+ require 'rbot/messagemapper'
# base class for all rbot plugins
# certain methods will be called if they are provided, if you define one of
# the following methods, it will be called as appropriate:
+ #
+ # map(template, options)::
+ # map is the new, cleaner way to respond to specific message formats
+ # without littering your plugin code with regexps
+ # examples:
+ # plugin.map 'karmastats', :action => 'karma_stats'
+ #
+ # # while in the plugin...
+ # def karma_stats(m, params)
+ # m.reply "..."
+ # end
+ #
+ # # the default action is the first component
+ # plugin.map 'karma'
+ #
+ # # attributes can be pulled out of the match string
+ # plugin.map 'karma for :key'
+ # plugin.map 'karma :key'
+ #
+ # # while in the plugin...
+ # def karma(m, params)
+ # item = params[:key]
+ # m.reply 'karma for #{item}'
+ # end
+ #
+ # # you can setup defaults, to make parameters optional
+ # plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
+ #
+ # # the default auth check is also against the first component
+ # # but that can be changed
+ # plugin.map 'karmastats', :auth => 'karma'
+ #
+ # # maps can be restricted to public or private message:
+ # plugin.map 'karmastats', :private false,
+ # plugin.map 'karmastats', :public false,
+ # end
+ #
+ # To activate your maps, you simply register them
+ # plugin.register_maps
+ # This also sets the privmsg handler to use the map lookups for
+ # handling messages. You can still use listen(), kick() etc methods
#
# listen(UserMessage)::
# Called for all messages of any type. To
@@ -43,14 +85,28 @@ module Irc
# plugin reload or bot quit - close any open
# files/connections or flush caches here
class Plugin
+ attr_reader :bot # the associated bot
# initialise your plugin. Always call super if you override this method,
# as important variables are set up for you
def initialize
@bot = Plugins.bot
@names = Array.new
+ @handler = MessageMapper.new(self)
@registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
end
+ def map(*args)
+ @handler.map(*args)
+ # register this map
+ name = @handler.last.items[0]
+ self.register name
+ unless self.respond_to?('privmsg')
+ def self.privmsg(m)
+ @handler.handle(m)
+ end
+ end
+ end
+
# return an identifier for this plugin, defaults to a list of the message
# prefixes handled (used for error messages etc)
def name
@@ -70,15 +126,17 @@ module Irc
# this can be called multiple times for a plugin to handle multiple
# message prefixes
def register(name)
+ return if Plugins.plugins.has_key?(name)
Plugins.plugins[name] = self
@names << name
end
- # is this plugin listening to all messages?
- def listen?
- @listen
+ # default usage method provided as a utility for simple plugins. The
+ # MessageMapper uses 'usage' as its default fallback method.
+ def usage(m, params)
+ m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
end
-
+
end
# class to manage multiple plugins and delegate messages to them for
diff --git a/rbot/plugins/autoop.rb b/rbot/plugins/autoop.rb
index 094ee343..fdbcf6e0 100644
--- a/rbot/plugins/autoop.rb
+++ b/rbot/plugins/autoop.rb
@@ -38,7 +38,7 @@ class AutoOP < Plugin
@registry[ma[1]] = channels.split(/,\s*/).collect { |x|
x.strip
}
- @bot.okay m.replyto
+ m.okay
else
m.reply @bot.lang.get('dunno')
end
@@ -48,7 +48,7 @@ class AutoOP < Plugin
if(!@registry.delete(params))
m.reply @bot.lang.get('dunno')
else
- @bot.okay m.replyto
+ m.okay
end
end
diff --git a/rbot/plugins/cal.rb b/rbot/plugins/cal.rb
index 1e823194..4f28310b 100644
--- a/rbot/plugins/cal.rb
+++ b/rbot/plugins/cal.rb
@@ -2,13 +2,14 @@ class CalPlugin < Plugin
def help(plugin, topic="")
"cal [options] => show current calendar [unix cal options]"
end
- def privmsg(m)
- if m.params && m.params.length > 0
- m.reply Utils.safe_exec("cal", m.params)
+ def cal(m, params)
+ if params.has_key?(:month)
+ m.reply Utils.safe_exec("cal", params[:month], params[:year])
else
m.reply Utils.safe_exec("cal")
end
end
end
plugin = CalPlugin.new
-plugin.register("cal")
+plugin.map 'cal :month :year', :requirements => {:month => /^\d+$/, :year => /^\d+$/}
+plugin.map 'cal'
diff --git a/rbot/plugins/eightball.rb b/rbot/plugins/eightball.rb
index 6d123b34..64748490 100644
--- a/rbot/plugins/eightball.rb
+++ b/rbot/plugins/eightball.rb
@@ -8,11 +8,12 @@ class EightBallPlugin < Plugin
def help(plugin, topic="")
"magic 8-ball ruby bot module written by novex for nvinfo on #dumber@quakenet, usage:<botname> 8ball will i ever beat this cancer?"
end
- def privmsg(m)
+ def eightball(m, params)
answers = @answers[rand(@answers.length)]
action = "shakes the magic 8-ball... #{answers}"
@bot.action m.replyto, action
end
end
plugin = EightBallPlugin.new
-plugin.register("8ball")
+plugin.map '8ball', :action => 'usage'
+plugin.map '8ball *params', :action => 'eightball'
diff --git a/rbot/plugins/karma.rb b/rbot/plugins/karma.rb
index 1bed175a..148427a5 100644
--- a/rbot/plugins/karma.rb
+++ b/rbot/plugins/karma.rb
@@ -27,60 +27,59 @@ class KarmaPlugin < Plugin
end
end
+
+ def stats(m, params)
+ if (@registry.length)
+ max = @registry.values.max
+ min = @registry.values.min
+ best = @registry.to_hash.index(max)
+ worst = @registry.to_hash.index(min)
+ m.reply "#{@registry.length} items. Best: #{best} (#{max}); Worst: #{worst} (#{min})"
+ end
+ end
+
+ def karma(m, params)
+ thing = params[:key]
+ thing = m.sourcenick unless thing
+ thing = thing.to_s
+ karma = @registry[thing]
+ if(karma != 0)
+ m.reply "karma for #{thing}: #{@registry[thing]}"
+ else
+ m.reply "#{thing} has neutral karma"
+ end
+ end
+
+
def help(plugin, topic="")
- "karma module: <thing>++/<thing>-- => increase/decrease karma for <thing>, karma for <thing>? => show karma for <thing>. Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own."
+ "karma module: <thing>++/<thing>-- => increase/decrease karma for <thing>, karma for <thing>? => show karma for <thing>, karmastats => show stats. Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own."
end
def listen(m)
- if(m.kind_of?(PrivMessage) && m.public?)
- # in channel message, the kind we are interested in
- if(m.message =~ /(\+\+|--)/)
- string = m.message.sub(/\W(--|\+\+)(\(.*?\)|[^(++)(\-\-)\s]+)/, "\2\1")
- seen = Hash.new
- while(string.sub!(/(\(.*?\)|[^(++)(\-\-)\s]+)(\+\+|--)/, ""))
- key = $1
- change = $2
- next if seen[key]
- seen[key] = true
+ return unless m.kind_of?(PrivMessage) && m.public?
+ # in channel message, the kind we are interested in
+ if(m.message =~ /(\+\+|--)/)
+ string = m.message.sub(/\W(--|\+\+)(\(.*?\)|[^(++)(\-\-)\s]+)/, "\2\1")
+ seen = Hash.new
+ while(string.sub!(/(\(.*?\)|[^(++)(\-\-)\s]+)(\+\+|--)/, ""))
+ key = $1
+ change = $2
+ next if seen[key]
+ seen[key] = true
- key.sub!(/^\((.*)\)$/, "\1")
- key.gsub!(/\s+/, " ")
- next unless(key.length > 0)
- next if(key == m.sourcenick)
- if(change == "++")
- @registry[key] += 1
- elsif(change == "--")
- @registry[key] -= 1
- end
+ key.sub!(/^\((.*)\)$/, "\1")
+ key.gsub!(/\s+/, " ")
+ next unless(key.length > 0)
+ next if(key == m.sourcenick)
+ if(change == "++")
+ @registry[key] += 1
+ elsif(change == "--")
+ @registry[key] -= 1
end
end
end
end
- def privmsg(m)
- if (m.plugin == "karmastats")
- if (@registry.length)
- max = @registry.values.max
- min = @registry.values.min
- best = @registry.to_hash.index(max)
- worst = @registry.to_hash.index(min)
- m.reply "#{@registry.length} votes. Best: #{best} (#{max}); Worst: #{worst} (#{min})"
- return
- end
- end
- unless(m.params)
- m.reply "incorrect usage: " + m.plugin
- return
- end
- if(m.params =~ /^(?:for\s+)?(\S+?)\??$/)
- thing = $1
- karma = @registry[thing]
- if(karma != 0)
- m.reply "karma for #{thing}: #{@registry[thing]}"
- else
- m.reply "#{thing} has neutral karma"
- end
- end
- end
end
plugin = KarmaPlugin.new
-plugin.register("karma")
-plugin.register("karmastats")
+plugin.map 'karmastats', :action => 'stats'
+plugin.map 'karma :key', :defaults => {:key => false}
+plugin.map 'karma for :key'
diff --git a/rbot/plugins/lart.rb b/rbot/plugins/lart.rb
index 385b17c3..1c72c648 100644
--- a/rbot/plugins/lart.rb
+++ b/rbot/plugins/lart.rb
@@ -130,25 +130,25 @@ class LartPlugin < Plugin
#{{{
def handle_addlart(m)
@larts << m.params
- @bot.okay m.replyto
+ m.okay
end
#}}}
#{{{
def handle_rmlart(m)
@larts.delete m.params
- @bot.okay m.replyto
+ m.okay
end
#}}}
#{{{
def handle_addpraise(m)
@praises << m.params
- @bot.okay m.replyto
+ m.okay
end
#}}}
#{{{
def handle_rmpraise(m)
@praises.delete m.params
- @bot.okay m.replyto
+ m.okay
end
#}}}
#}}}
diff --git a/rbot/plugins/nickserv.rb b/rbot/plugins/nickserv.rb
index 94c57e6d..1ef2baf7 100644
--- a/rbot/plugins/nickserv.rb
+++ b/rbot/plugins/nickserv.rb
@@ -38,23 +38,23 @@ class NickServPlugin < Plugin
nick = $1
passwd = $2
@registry[nick] = passwd
- @bot.okay m.replyto
+ m.okay
when (/^register$/)
passwd = genpasswd
@bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd
@registry[@bot.nick] = passwd
- @bot.okay m.replyto
+ m.okay
when (/^register\s*(\S*)\s*(.*)$/)
passwd = $1
email = $2
@bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + " " + email
@registry[@bot.nick] = passwd
- @bot.okay m.replyto
+ m.okay
when (/^register\s*(.*)\s*$/)
passwd = $1
@bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd
@registry[@bot.nick] = passwd
- @bot.okay m.replyto
+ m.okay
when (/^listnicks$/)
if @bot.auth.allow?("config", m.source, m.replyto)
if @registry.length > 0
@@ -68,7 +68,7 @@ class NickServPlugin < Plugin
when (/^identify$/)
if @registry.has_key?(@bot.nick)
@bot.sendmsg "PRIVMSG", "NickServ", "IDENTIFY " + @registry[@bot.nick]
- @bot.okay m.replyto
+ m.okay
else
m.reply "I dunno the nickserv password for the nickname #{@bot.nick} :("
end
diff --git a/rbot/plugins/opmeh.rb b/rbot/plugins/opmeh.rb
index eb392513..2776de60 100644
--- a/rbot/plugins/opmeh.rb
+++ b/rbot/plugins/opmeh.rb
@@ -12,6 +12,7 @@ class OpMehPlugin < Plugin
end
target = m.sourcenick
@bot.sendq("MODE #{channel} +o #{target}")
+ m.okay
end
end
plugin = OpMehPlugin.new
diff --git a/rbot/plugins/quotes.rb b/rbot/plugins/quotes.rb
index 0e46b495..674a9ed6 100644
--- a/rbot/plugins/quotes.rb
+++ b/rbot/plugins/quotes.rb
@@ -186,7 +186,7 @@ class QuotePlugin < Plugin
num = $2.to_i
if(@bot.auth.allow?("delquote", m.source, m.replyto))
if(delquote(channel, num))
- @bot.okay m.replyto
+ m.okay
else
m.reply "quote not found!"
end
@@ -288,7 +288,7 @@ class QuotePlugin < Plugin
num = $1.to_i
if(@bot.auth.allow?("delquote", m.source, m.replyto))
if(delquote(m.target, num))
- @bot.okay m.replyto
+ m.okay
else
m.reply "quote not found!"
end
diff --git a/rbot/plugins/remind.rb b/rbot/plugins/remind.rb
index 402e2d08..5ad980ae 100644
--- a/rbot/plugins/remind.rb
+++ b/rbot/plugins/remind.rb
@@ -145,7 +145,7 @@ class RemindPlugin < Plugin
m.reply "incorrect usage: " + help(m.plugin)
return
end
- @bot.okay m.replyto
+ m.okay
end
end
plugin = RemindPlugin.new
diff --git a/rbot/plugins/roulette.rb b/rbot/plugins/roulette.rb
index a3d102f3..c9d585ea 100644
--- a/rbot/plugins/roulette.rb
+++ b/rbot/plugins/roulette.rb
@@ -30,7 +30,7 @@ class RoulettePlugin < Plugin
elsif m.params == "clearstats"
if @bot.auth.allow?("config", m.source, m.replyto)
@registry.clear
- @bot.okay m.replyto
+ m.okay
end
return
elsif m.params