From 2a96c9198c1f6e13407d0999083f6ce5e0bc06fa Mon Sep 17 00:00:00 2001 From: Tom Gilbert Date: Wed, 27 Jul 2005 16:32:32 +0000 Subject: move rbot into lib - still rearranging for packaging/installation --- bin/rbot | 7 +- lib/rbot/auth.rb | 199 ++++++++ lib/rbot/channel.rb | 54 ++ lib/rbot/config.rb | 205 ++++++++ lib/rbot/dbhash.rb | 133 +++++ lib/rbot/httputil.rb | 88 ++++ lib/rbot/ircbot.rb | 750 ++++++++++++++++++++++++++++ lib/rbot/ircsocket.rb | 186 +++++++ lib/rbot/keywords.rb | 427 ++++++++++++++++ lib/rbot/language.rb | 55 +++ lib/rbot/languages/dutch.lang | 73 +++ lib/rbot/languages/english.lang | 75 +++ lib/rbot/languages/german.lang | 67 +++ lib/rbot/message.rb | 256 ++++++++++ lib/rbot/messagemapper.rb | 168 +++++++ lib/rbot/plugins.rb | 300 ++++++++++++ lib/rbot/plugins/autoop.rb | 68 +++ lib/rbot/plugins/autorejoin.rb | 16 + lib/rbot/plugins/cal.rb | 15 + lib/rbot/plugins/dice.rb | 81 +++ lib/rbot/plugins/eightball.rb | 19 + lib/rbot/plugins/excuse.rb | 470 ++++++++++++++++++ lib/rbot/plugins/fish.rb | 61 +++ lib/rbot/plugins/fortune.rb | 22 + lib/rbot/plugins/freshmeat.rb | 98 ++++ lib/rbot/plugins/google.rb | 51 ++ lib/rbot/plugins/host.rb | 14 + lib/rbot/plugins/httpd.rb | 35 ++ lib/rbot/plugins/insult.rb | 258 ++++++++++ lib/rbot/plugins/karma.rb | 85 ++++ lib/rbot/plugins/lart.rb | 181 +++++++ lib/rbot/plugins/math.rb | 122 +++++ lib/rbot/plugins/nickserv.rb | 99 ++++ lib/rbot/plugins/nslookup.rb | 56 +++ lib/rbot/plugins/opmeh.rb | 19 + lib/rbot/plugins/quotes.rb | 321 ++++++++++++ lib/rbot/plugins/remind.rb | 154 ++++++ lib/rbot/plugins/roshambo.rb | 54 ++ lib/rbot/plugins/rot13.rb | 14 + lib/rbot/plugins/roulette.rb | 147 ++++++ lib/rbot/plugins/seen.rb | 89 ++++ lib/rbot/plugins/slashdot.rb | 95 ++++ lib/rbot/plugins/spell.rb | 36 ++ lib/rbot/plugins/tube.rb | 77 +++ lib/rbot/plugins/url.rb | 98 ++++ lib/rbot/plugins/weather.rb | 55 +++ lib/rbot/plugins/wserver.rb | 75 +++ lib/rbot/registry.rb | 292 +++++++++++ lib/rbot/rfc2812.rb | 1027 +++++++++++++++++++++++++++++++++++++++ lib/rbot/timer.rb | 123 +++++ lib/rbot/utils.rb | 778 +++++++++++++++++++++++++++++ rbot/auth.rb | 199 -------- rbot/channel.rb | 54 -- rbot/config.rb | 205 -------- rbot/dbhash.rb | 133 ----- rbot/httputil.rb | 88 ---- rbot/ircbot.rb | 750 ---------------------------- rbot/ircsocket.rb | 186 ------- rbot/keywords.rb | 427 ---------------- rbot/language.rb | 55 --- rbot/languages/dutch.lang | 73 --- rbot/languages/english.lang | 75 --- rbot/languages/german.lang | 67 --- rbot/message.rb | 256 ---------- rbot/messagemapper.rb | 168 ------- rbot/plugins.rb | 300 ------------ rbot/registry.rb | 292 ----------- rbot/rfc2812.rb | 1027 --------------------------------------- rbot/timer.rb | 123 ----- rbot/utils.rb | 778 ----------------------------- 70 files changed, 8247 insertions(+), 5257 deletions(-) create mode 100644 lib/rbot/auth.rb create mode 100644 lib/rbot/channel.rb create mode 100644 lib/rbot/config.rb create mode 100644 lib/rbot/dbhash.rb create mode 100644 lib/rbot/httputil.rb create mode 100644 lib/rbot/ircbot.rb create mode 100644 lib/rbot/ircsocket.rb create mode 100644 lib/rbot/keywords.rb create mode 100644 lib/rbot/language.rb create mode 100644 lib/rbot/languages/dutch.lang create mode 100644 lib/rbot/languages/english.lang create mode 100644 lib/rbot/languages/german.lang create mode 100644 lib/rbot/message.rb create mode 100644 lib/rbot/messagemapper.rb create mode 100644 lib/rbot/plugins.rb create mode 100644 lib/rbot/plugins/autoop.rb create mode 100644 lib/rbot/plugins/autorejoin.rb create mode 100644 lib/rbot/plugins/cal.rb create mode 100644 lib/rbot/plugins/dice.rb create mode 100644 lib/rbot/plugins/eightball.rb create mode 100644 lib/rbot/plugins/excuse.rb create mode 100644 lib/rbot/plugins/fish.rb create mode 100644 lib/rbot/plugins/fortune.rb create mode 100644 lib/rbot/plugins/freshmeat.rb create mode 100644 lib/rbot/plugins/google.rb create mode 100644 lib/rbot/plugins/host.rb create mode 100644 lib/rbot/plugins/httpd.rb create mode 100644 lib/rbot/plugins/insult.rb create mode 100644 lib/rbot/plugins/karma.rb create mode 100644 lib/rbot/plugins/lart.rb create mode 100644 lib/rbot/plugins/math.rb create mode 100644 lib/rbot/plugins/nickserv.rb create mode 100644 lib/rbot/plugins/nslookup.rb create mode 100644 lib/rbot/plugins/opmeh.rb create mode 100644 lib/rbot/plugins/quotes.rb create mode 100644 lib/rbot/plugins/remind.rb create mode 100644 lib/rbot/plugins/roshambo.rb create mode 100644 lib/rbot/plugins/rot13.rb create mode 100644 lib/rbot/plugins/roulette.rb create mode 100644 lib/rbot/plugins/seen.rb create mode 100644 lib/rbot/plugins/slashdot.rb create mode 100644 lib/rbot/plugins/spell.rb create mode 100644 lib/rbot/plugins/tube.rb create mode 100644 lib/rbot/plugins/url.rb create mode 100644 lib/rbot/plugins/weather.rb create mode 100644 lib/rbot/plugins/wserver.rb create mode 100644 lib/rbot/registry.rb create mode 100644 lib/rbot/rfc2812.rb create mode 100644 lib/rbot/timer.rb create mode 100644 lib/rbot/utils.rb delete mode 100644 rbot/auth.rb delete mode 100644 rbot/channel.rb delete mode 100644 rbot/config.rb delete mode 100644 rbot/dbhash.rb delete mode 100644 rbot/httputil.rb delete mode 100644 rbot/ircbot.rb delete mode 100644 rbot/ircsocket.rb delete mode 100644 rbot/keywords.rb delete mode 100644 rbot/language.rb delete mode 100644 rbot/languages/dutch.lang delete mode 100644 rbot/languages/english.lang delete mode 100644 rbot/languages/german.lang delete mode 100644 rbot/message.rb delete mode 100644 rbot/messagemapper.rb delete mode 100644 rbot/plugins.rb delete mode 100644 rbot/registry.rb delete mode 100644 rbot/rfc2812.rb delete mode 100644 rbot/timer.rb delete mode 100644 rbot/utils.rb diff --git a/bin/rbot b/bin/rbot index 6f08fe57..9b0de677 100755 --- a/bin/rbot +++ b/bin/rbot @@ -23,6 +23,7 @@ $VERBOSE=true +require 'etc' require 'getoptlong' require 'rbot/ircbot' @@ -47,10 +48,14 @@ opts.each {|opt, arg| } botclass = ARGV.shift -botclass = "rbotconf" unless(botclass); +user = Etc.getlogin +botclass = "/home/#{user}/.rbot" unless(botclass); unless FileTest.directory? botclass # TODO copy in samples/templates from install directory + puts "no #{botclass} directory found, creating from templates.." + # copy DATA/rbot/templates to botclass + end if(bot = Irc::IrcBot.new(botclass)) diff --git a/lib/rbot/auth.rb b/lib/rbot/auth.rb new file mode 100644 index 00000000..7811d9e4 --- /dev/null +++ b/lib/rbot/auth.rb @@ -0,0 +1,199 @@ +module Irc + + # globmask:: glob to test with + # netmask:: netmask to test against + # Compare a netmask with a standard IRC glob, e.g foo!bar@baz.com would + # match *!*@baz.com, foo!*@*, *!bar@*, etc. + def Irc.netmaskmatch(globmask, netmask) + regmask = globmask.gsub(/\*/, ".*?") + return true if(netmask =~ /#{regmask}/) + return false + end + + # check if a string is an actual IRC hostmask + def Irc.ismask(mask) + mask =~ /^.+!.+@.+$/ + end + + + # User-level authentication to allow/disallow access to bot commands based + # on hostmask and userlevel. + class IrcAuth + # create a new IrcAuth instance. + # bot:: associated bot class + def initialize(bot) + @bot = bot + @users = Hash.new(0) + @levels = Hash.new(0) + if(File.exist?("#{@bot.botclass}/users.rbot")) + IO.foreach("#{@bot.botclass}/users.rbot") do |line| + if(line =~ /\s*(\d+)\s*(\S+)/) + level = $1.to_i + mask = $2 + @users[mask] = level + end + end + end + if(File.exist?("#{@bot.botclass}/levels.rbot")) + IO.foreach("#{@bot.botclass}/levels.rbot") do |line| + if(line =~ /\s*(\d+)\s*(\S+)/) + level = $1.to_i + command = $2 + @levels[command] = level + end + end + end + end + + # save current users and levels to files. + # levels are written to #{botclass}/levels.rbot + # users are written to #{botclass}/users.rbot + def save + Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}")) + File.open("#{@bot.botclass}/users.rbot", "w") do |file| + @users.each do |key, value| + file.puts "#{value} #{key}" + end + end + File.open("#{@bot.botclass}/levels.rbot", "w") do |file| + @levels.each do |key, value| + file.puts "#{value} #{key}" + end + end + end + + # command:: command user wishes to perform + # mask:: hostmask of user + # tell:: optional recipient for "insufficient auth" message + # + # returns true if user with hostmask +mask+ is permitted to perform + # +command+ optionally pass tell as the target for the "insufficient auth" + # message, if the user is not authorised + def allow?(command, mask, tell=nil) + auth = userlevel(mask) + if(auth >= @levels[command]) + return true + else + debug "#{mask} is not allowed to perform #{command}" + @bot.say tell, "insufficient \"#{command}\" auth (have #{auth}, need #{@levels[command]})" if tell + return false + end + end + + # add user with hostmask matching +mask+ with initial auth level +level+ + def useradd(mask, level) + if(Irc.ismask(mask)) + @users[mask] = level + end + end + + # mask:: mask of user to remove + # remove user with mask +mask+ + def userdel(mask) + if(Irc.ismask(mask)) + @users.delete(mask) + end + end + + # command:: command to adjust + # level:: new auth level for the command + # set required auth level of +command+ to +level+ + def setlevel(command, level) + @levels[command] = level + end + + # specific users. + # mask:: mask of user + # returns the authlevel of user with mask +mask+ + # finds the matching user which has the highest authlevel (so you can have + # a default level of 5 for *!*@*, and yet still give higher levels to + def userlevel(mask) + # go through hostmask list, find match with _highest_ level (all users + # will match *!*@*) + level = 0 + @users.each {|user,userlevel| + if(Irc.netmaskmatch(user, mask)) + level = userlevel if userlevel > level + end + } + level + end + + # return all currently defined commands (for which auth is required) and + # their required authlevels + def showlevels + reply = "Current levels are:" + @levels.sort.each {|a| + key = a[0] + value = a[1] + reply += " #{key}(#{value})" + } + reply + end + + # return all currently defined users and their authlevels + def showusers + reply = "Current users are:" + @users.sort.each {|a| + key = a[0] + value = a[1] + reply += " #{key}(#{value})" + } + reply + end + + # module help + def help(topic="") + case topic + when "setlevel" + return "setlevel => Sets required level for to (private addressing only)" + when "useradd" + return "useradd => Add user at level (private addressing only)" + when "userdel" + return "userdel => Remove user (private addressing only)" + when "auth" + return "auth => Recognise your hostmask as bot master (private addressing only)" + when "levels" + return "levels => list commands and their required levels (private addressing only)" + when "users" + return "users => list users and their levels (private addressing only)" + else + return "Auth module (User authentication) topics: setlevel, useradd, userdel, auth, levels, users" + end + end + + # privmsg handler + def privmsg(m) + if(m.address? && m.private?) + case m.message + when (/^setlevel\s+(\S+)\s+(\d+)$/) + if(@bot.auth.allow?("auth", m.source, m.replyto)) + @bot.auth.setlevel($1, $2.to_i) + m.reply "level for #$1 set to #$2" + end + when (/^useradd\s+(\S+)\s+(\d+)/) + if(@bot.auth.allow?("auth", m.source, m.replyto)) + @bot.auth.useradd($1, $2.to_i) + m.reply "added user #$1 at level #$2" + end + when (/^userdel\s+(\S+)/) + if(@bot.auth.allow?("auth", m.source, m.replyto)) + @bot.auth.userdel($1) + m.reply "user #$1 is gone" + end + when (/^auth\s+(\S+)/) + if($1 == @bot.config["auth.password"]) + @bot.auth.useradd(Regexp.escape(m.source), 1000) + m.reply "Identified, security level maxed out" + else + m.reply "incorrect password" + end + when ("levels") + m.reply @bot.auth.showlevels if(@bot.auth.allow?("config", m.source, m.replyto)) + when ("users") + m.reply @bot.auth.showusers if(@bot.auth.allow?("config", m.source, m.replyto)) + end + end + end + end +end diff --git a/lib/rbot/channel.rb b/lib/rbot/channel.rb new file mode 100644 index 00000000..edd206bf --- /dev/null +++ b/lib/rbot/channel.rb @@ -0,0 +1,54 @@ +module Irc + + # class to store IRC channel data (users, topic, per-channel configurations) + class IRCChannel + # name of channel + attr_reader :name + + # current channel topic + attr_reader :topic + + # hash containing users currently in the channel + attr_accessor :users + + # if true, bot won't talk in this channel + attr_accessor :quiet + + # name:: channel name + # create a new IRCChannel + def initialize(name) + @name = name + @users = Hash.new + @quiet = false + @topic = Topic.new + end + + # eg @bot.channels[chan].topic = topic + def topic=(name) + @topic.name = name + end + + # class to store IRC channel topic information + class Topic + # topic name + attr_accessor :name + + # timestamp + attr_accessor :timestamp + + # topic set by + attr_accessor :by + + def initialize + @name = "" + end + + # when called like "puts @bots.channels[chan].topic" + def to_s + @name + end + end + + end + +end diff --git a/lib/rbot/config.rb b/lib/rbot/config.rb new file mode 100644 index 00000000..971a413c --- /dev/null +++ b/lib/rbot/config.rb @@ -0,0 +1,205 @@ +module Irc + + require 'yaml' + + # container for bot configuration + 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) + @bot = bot + # some defaults + @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.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/lib/rbot/dbhash.rb b/lib/rbot/dbhash.rb new file mode 100644 index 00000000..5ae2ba87 --- /dev/null +++ b/lib/rbot/dbhash.rb @@ -0,0 +1,133 @@ +# Copyright (C) 2002 Tom Gilbert. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies of the Software and its documentation and acknowledgment shall be +# given in the documentation and software packages that this Software was +# used. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +begin + require 'bdb' +rescue Exception => e + puts "Got exception: "+e + puts "rbot couldn't load the bdb module, perhaps you need to install it? try: http://www.ruby-lang.org/en/raa-list.rhtml?name=bdb" + exit 2 +end + +# make BTree lookups case insensitive +module BDB + class CIBtree < Btree + def bdb_bt_compare(a, b) + a.downcase <=> b.downcase + end + end +end + +module Irc + + # DBHash is for tying a hash to disk (using bdb). + # Call it with an identifier, for example "mydata". It'll look for + # mydata.db, if it exists, it will load and reference that db. + # Otherwise it'll create and empty db called mydata.db + class DBHash + + # absfilename:: use +key+ as an actual filename, don't prepend the bot's + # config path and don't append ".db" + def initialize(bot, key, absfilename=false) + @bot = bot + @key = key + if absfilename && File.exist?(key) + # db already exists, use it + @db = DBHash.open_db(key) + elsif File.exist?(@bot.botclass + "/#{key}.db") + # db already exists, use it + @db = DBHash.open_db(@bot.botclass + "/#{key}.db") + elsif absfilename + # create empty db + @db = DBHash.create_db(key) + else + # create empty db + @db = DBHash.create_db(@bot.botclass + "/#{key}.db") + end + end + + def method_missing(method, *args, &block) + return @db.send(method, *args, &block) + end + + def DBHash.create_db(name) + debug "DBHash: creating empty db #{name}" + return BDB::Hash.open(name, nil, + BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, + 0600, "set_pagesize" => 1024, + "set_cachesize" => [(0), (32 * 1024), (0)]) + end + + def DBHash.open_db(name) + debug "DBHash: opening existing db #{name}" + return BDB::Hash.open(name, nil, + "r+", 0600, "set_pagesize" => 1024, + "set_cachesize" => [(0), (32 * 1024), (0)]) + end + + end + + + # DBTree is a BTree equivalent of DBHash, with case insensitive lookups. + class DBTree + + # absfilename:: use +key+ as an actual filename, don't prepend the bot's + # config path and don't append ".db" + def initialize(bot, key, absfilename=false) + @bot = bot + @key = key + if absfilename && File.exist?(key) + # db already exists, use it + @db = DBTree.open_db(key) + elsif absfilename + # create empty db + @db = DBTree.create_db(key) + elsif File.exist?(@bot.botclass + "/#{key}.db") + # db already exists, use it + @db = DBTree.open_db(@bot.botclass + "/#{key}.db") + else + # create empty db + @db = DBTree.create_db(@bot.botclass + "/#{key}.db") + end + end + + def method_missing(method, *args, &block) + return @db.send(method, *args, &block) + end + + def DBTree.create_db(name) + debug "DBTree: creating empty db #{name}" + return BDB::CIBtree.open(name, nil, + BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, + 0600, "set_pagesize" => 1024, + "set_cachesize" => [(0), (32 * 1024), (0)]) + end + + def DBTree.open_db(name) + debug "DBTree: opening existing db #{name}" + return BDB::CIBtree.open(name, nil, + "r+", 0600, "set_pagesize" => 1024, + "set_cachesize" => [0, 32 * 1024, 0]) + end + + end + +end diff --git a/lib/rbot/httputil.rb b/lib/rbot/httputil.rb new file mode 100644 index 00000000..ff3216a6 --- /dev/null +++ b/lib/rbot/httputil.rb @@ -0,0 +1,88 @@ +module Irc + +require 'net/http' +Net::HTTP.version_1_2 + +# class for making http requests easier (mainly for plugins to use) +# this class can check the bot proxy configuration to determine if a proxy +# needs to be used, which includes support for per-url proxy configuration. +class HttpUtil + def initialize(bot) + @bot = bot + @headers = { + 'User-Agent' => "rbot http util #{$version} (http://linuxbrit.co.uk/rbot/)", + } + end + + # uri:: Uri to create a proxy for + # + # return a net/http Proxy object, which is configured correctly for + # proxying based on the bot's proxy configuration. + # This will include per-url proxy configuration based on the bot config + # +http_proxy_include/exclude+ options. + def get_proxy(uri) + proxy = nil + if (ENV['http_proxy']) + proxy = URI.parse ENV['http_proxy'] + end + if (@bot.config["http_proxy"]) + proxy = URI.parse ENV['http_proxy'] + end + + # if http_proxy_include or http_proxy_exclude are set, then examine the + # uri to see if this is a proxied uri + if uri + if @bot.config["http_proxy_exclude"] + # TODO + end + if @bot.config["http_proxy_include"] + end + end + + proxy_host = nil + proxy_port = nil + proxy_user = nil + proxy_pass = nil + if @bot.config["http_proxy_user"] + proxy_user = @bot.config["http_proxy_user"] + if @bot.config["http_proxy_pass"] + proxy_pass = @bot.config["http_proxy_pass"] + end + end + if proxy + proxy_host = proxy.host + proxy_port = proxy.port + end + + return Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_port) + end + + # uri:: uri to query (Uri object) + # readtimeout:: timeout for reading the response + # opentimeout:: timeout for opening the connection + # + # simple get request, returns response body if the status code is 200 and + # the request doesn't timeout. + def get(uri, readtimeout=10, opentimeout=5) + proxy = get_proxy(uri) + proxy.open_timeout = opentimeout + proxy.read_timeout = readtimeout + + begin + proxy.start() {|http| + resp = http.get(uri.request_uri(), @headers) + if resp.code == "200" + return resp.body + else + puts "HttpUtil.get return code #{resp.code} #{resp.body}" + end + return nil + } + rescue StandardError, Timeout::Error => e + $stderr.puts "HttpUtil.get exception: #{e}, while trying to get #{uri}" + end + return nil + end +end + +end diff --git a/lib/rbot/ircbot.rb b/lib/rbot/ircbot.rb new file mode 100644 index 00000000..844231dd --- /dev/null +++ b/lib/rbot/ircbot.rb @@ -0,0 +1,750 @@ +# Copyright (C) 2002 Tom Gilbert. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies of the Software and its documentation and acknowledgment shall be +# given in the documentation and software packages that this Software was +# used. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +require 'thread' + +require 'rbot/rfc2812' +require 'rbot/keywords' +require 'rbot/config' +require 'rbot/ircsocket' +require 'rbot/auth' +require 'rbot/timer' +require 'rbot/plugins' +require 'rbot/channel' +require 'rbot/utils' +require 'rbot/message' +require 'rbot/language' +require 'rbot/dbhash' +require 'rbot/registry' +require 'rbot/httputil' + +module Irc + +# Main bot class, which receives messages, handles them or passes them to +# plugins, and stores runtime data +class IrcBot + # the bot's current nickname + attr_reader :nick + + # the bot's IrcAuth data + attr_reader :auth + + # the bot's BotConfig data + attr_reader :config + + # the botclass for this bot (determines configdir among other things) + attr_reader :botclass + + # used to perform actions periodically (saves configuration once per minute + # by default) + attr_reader :timer + + # bot's Language data + attr_reader :lang + + # bot's configured addressing prefixes + attr_reader :addressing_prefixes + + # channel info for channels the bot is in + attr_reader :channels + + # bot's object registry, plugins get an interface to this for persistant + # storage (hash interface tied to a bdb file, plugins use Accessors to store + # and restore objects in their own namespaces.) + attr_reader :registry + + # bot's httputil help object, for fetching resources via http. Sets up + # proxies etc as defined by the bot configuration/environment + attr_reader :httputil + + # create a new IrcBot with botclass +botclass+ + def initialize(botclass) + @botclass = botclass.gsub(/\/$/, "") + @startup_time = Time.new + + Dir.mkdir("#{botclass}") if(!File.exist?("#{botclass}")) + Dir.mkdir("#{botclass}/logs") if(!File.exist?("#{botclass}/logs")) + + @config = Irc::BotConfig.new(self) + @timer = Timer::Timer.new + @registry = BotRegistry.new self + @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['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.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 + + @client = Irc::IrcClient.new + @client["PRIVMSG"] = proc { |data| + message = PrivMessage.new(self, data["SOURCE"], data["TARGET"], data["MESSAGE"]) + onprivmsg(message) + } + @client["NOTICE"] = proc { |data| + message = NoticeMessage.new(self, data["SOURCE"], data["TARGET"], data["MESSAGE"]) + # pass it off to plugins that want to hear everything + @plugins.delegate "listen", message + } + @client["MOTD"] = proc { |data| + data['MOTD'].each_line { |line| + log "MOTD: #{line}", "server" + } + } + @client["NICKTAKEN"] = proc { |data| + nickchg "#{@nick}_" + } + @client["BADNICK"] = proc {|data| + puts "WARNING, bad nick (#{data['NICK']})" + } + @client["PING"] = proc {|data| + # (jump the queue for pongs) + @socket.puts "PONG #{data['PINGID']}" + } + @client["NICK"] = proc {|data| + sourcenick = data["SOURCENICK"] + nick = data["NICK"] + m = NickMessage.new(self, data["SOURCE"], data["SOURCENICK"], data["NICK"]) + if(sourcenick == @nick) + @nick = nick + end + @channels.each {|k,v| + if(v.users.has_key?(sourcenick)) + log "@ #{sourcenick} is now known as #{nick}", k + v.users[nick] = v.users[sourcenick] + v.users.delete(sourcenick) + end + } + @plugins.delegate("listen", m) + @plugins.delegate("nick", m) + } + @client["QUIT"] = proc {|data| + source = data["SOURCE"] + sourcenick = data["SOURCENICK"] + sourceurl = data["SOURCEADDRESS"] + message = data["MESSAGE"] + m = QuitMessage.new(self, data["SOURCE"], data["SOURCENICK"], data["MESSAGE"]) + if(data["SOURCENICK"] =~ /#{@nick}/i) + else + @channels.each {|k,v| + if(v.users.has_key?(sourcenick)) + log "@ Quit: #{sourcenick}: #{message}", k + v.users.delete(sourcenick) + end + } + end + @plugins.delegate("listen", m) + @plugins.delegate("quit", m) + } + @client["MODE"] = proc {|data| + source = data["SOURCE"] + sourcenick = data["SOURCENICK"] + sourceurl = data["SOURCEADDRESS"] + channel = data["CHANNEL"] + targets = data["TARGETS"] + modestring = data["MODESTRING"] + log "@ Mode #{modestring} #{targets} by #{sourcenick}", channel + } + @client["WELCOME"] = proc {|data| + log "joined server #{data['SOURCE']} as #{data['NICK']}", "server" + debug "I think my nick is #{@nick}, server thinks #{data['NICK']}" + if data['NICK'] && data['NICK'].length > 0 + @nick = data['NICK'] + end + 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['irc.join_channels']) + @config['irc.join_channels'].split(", ").each {|c| + puts "autojoining channel #{c}" + if(c =~ /^(\S+)\s+(\S+)$/i) + join $1, $2 + else + join c if(c) + end + } + end + } + @client["JOIN"] = proc {|data| + m = JoinMessage.new(self, data["SOURCE"], data["CHANNEL"], data["MESSAGE"]) + onjoin(m) + } + @client["PART"] = proc {|data| + m = PartMessage.new(self, data["SOURCE"], data["CHANNEL"], data["MESSAGE"]) + onpart(m) + } + @client["KICK"] = proc {|data| + m = KickMessage.new(self, data["SOURCE"], data["TARGET"],data["CHANNEL"],data["MESSAGE"]) + onkick(m) + } + @client["INVITE"] = proc {|data| + if(data["TARGET"] =~ /^#{@nick}$/i) + join data["CHANNEL"] if (@auth.allow?("join", data["SOURCE"], data["SOURCENICK"])) + end + } + @client["CHANGETOPIC"] = proc {|data| + channel = data["CHANNEL"] + sourcenick = data["SOURCENICK"] + topic = data["TOPIC"] + timestamp = data["UNIXTIME"] || Time.now.to_i + if(sourcenick == @nick) + log "@ I set topic \"#{topic}\"", channel + else + log "@ #{sourcenick} set topic \"#{topic}\"", channel + end + m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], timestamp, data["TOPIC"]) + + ontopic(m) + @plugins.delegate("listen", m) + @plugins.delegate("topic", m) + } + @client["TOPIC"] = @client["TOPICINFO"] = proc {|data| + channel = data["CHANNEL"] + m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], data["UNIXTIME"], data["TOPIC"]) + ontopic(m) + } + @client["NAMES"] = proc {|data| + channel = data["CHANNEL"] + users = data["USERS"] + unless(@channels[channel]) + puts "bug: got names for channel '#{channel}' I didn't think I was in\n" + exit 2 + end + @channels[channel].users.clear + users.each {|u| + @channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]] + } + } + @client["UNKNOWN"] = proc {|data| + debug "UNKNOWN: #{data['SERVERSTRING']}" + } + end + + # connect the bot to IRC + def connect + trap("SIGTERM") { quit } + trap("SIGHUP") { quit } + trap("SIGINT") { quit } + begin + @socket.connect + rescue => e + raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e + end + @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 + def mainloop + socket_timeout = 0.2 + reconnect_wait = 5 + + while true + connect + + begin + while true + if @socket.select socket_timeout + break unless reply = @socket.gets + @client.process reply + end + @timer.tick + end + rescue => e + puts "connection closed: #{e}" + puts e.backtrace.join("\n") + end + + puts "disconnected" + @channels.clear + @socket.clearq + + puts "waiting to reconnect" + sleep reconnect_wait + end + end + + # type:: message type + # where:: message target + # message:: message text + # send message +message+ of type +type+ to target +where+ + # Type can be PRIVMSG, NOTICE, etc, but those you should really use the + # relevant say() or notice() methods. This one should be used for IRCd + # extensions you want to use in modules. + def sendmsg(type, where, message) + # limit it 440 chars + CRLF.. so we have to split long lines + left = 440 - type.length - where.length - 3 + begin + if(left >= message.length) + sendq("#{type} #{where} :#{message}") + log_sent(type, where, message) + return + end + line = message.slice!(0, left) + lastspace = line.rindex(/\s+/) + if(lastspace) + message = line.slice!(lastspace, line.length) + message + message.gsub!(/^\s+/, "") + end + sendq("#{type} #{where} :#{line}") + log_sent(type, where, line) + end while(message.length > 0) + end + + def sendq(message="") + # temporary + @socket.queue(message) + end + + # send a notice message to channel/nick +where+ + def notice(where, message) + message.each_line { |line| + line.chomp! + next unless(line.length > 0) + sendmsg("NOTICE", where, line) + } + end + + # say something (PRIVMSG) to channel/nick +where+ + def say(where, message) + message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line| + line.chomp! + next unless(line.length > 0) + unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet)) + sendmsg("PRIVMSG", where, line) + end + } + end + + # perform a CTCP action with message +message+ to channel/nick +where+ + def action(where, message) + sendq("PRIVMSG #{where} :\001ACTION #{message}\001") + if(where =~ /^#/) + log "* #{@nick} #{message}", where + elsif (where =~ /^(\S*)!.*$/) + log "* #{@nick}[#{where}] #{message}", $1 + else + log "* #{@nick}[#{where}] #{message}", where + end + end + + # quick way to say "okay" (or equivalent) to +where+ + def okay(where) + say where, @lang.get("okay") + end + + # log message +message+ to a file determined by +where+. +where+ can be a + # channel name, or a nick for private message logging + def log(message, where="server") + message.chomp! + stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S") + unless(@logs.has_key?(where)) + @logs[where] = File.new("#{@botclass}/logs/#{where}", "a") + @logs[where].sync = true + end + @logs[where].puts "[#{stamp}] #{message}" + #debug "[#{stamp}] <#{where}> #{message}" + end + + # set topic of channel +where+ to +topic+ + def topic(where, topic) + sendq "TOPIC #{where} :#{topic}" + end + + # message:: optional IRC quit message + # quit IRC, shutdown the bot + def quit(message=nil) + trap("SIGTERM", "DEFAULT") + trap("SIGHUP", "DEFAULT") + trap("SIGINT", "DEFAULT") + message = @lang.get("quit") if (!message || message.length < 1) + @socket.clearq + save + @plugins.cleanup + @channels.each_value {|v| + log "@ quit (#{message})", v.name + } + @socket.puts "QUIT :#{message}" + @socket.flush + @socket.shutdown + @registry.close + puts "rbot quit (#{message})" + exit 0 + end + + # call the save method for bot's config, keywords, auth and all plugins + def save + @registry.flush + @config.save + @keywords.save + @auth.save + @plugins.save + end + + # call the rescan method for the bot's lang, keywords and all plugins + def rescan + @lang.rescan + @plugins.rescan + @keywords.rescan + end + + # channel:: channel to join + # key:: optional channel key if channel is +s + # join a channel + def join(channel, key=nil) + if(key) + sendq "JOIN #{channel} :#{key}" + else + sendq "JOIN #{channel}" + end + end + + # part a channel + def part(channel, message="") + sendq "PART #{channel} :#{message}" + end + + # attempt to change bot's nick to +name+ + def nickchg(name) + sendq "NICK #{name}" + end + + # changing mode + def mode(channel, mode, target) + sendq "MODE #{channel} #{mode} #{target}" + end + + # m:: message asking for help + # topic:: optional topic help is requested for + # respond to online help requests + def help(topic=nil) + topic = nil if topic == "" + case topic + when nil + helpstr = "help topics: core, auth, keywords" + helpstr += @plugins.helptopics + helpstr += " (help for more info)" + when /^core$/i + helpstr = corehelp + when /^core\s+(.+)$/i + helpstr = corehelp $1 + when /^auth$/i + helpstr = @auth.help + when /^auth\s+(.+)$/i + helpstr = @auth.help $1 + when /^keywords$/i + helpstr = @keywords.help + when /^keywords\s+(.+)$/i + helpstr = @keywords.help $1 + else + unless(helpstr = @plugins.help(topic)) + helpstr = "no help for topic #{topic}" + end + end + return helpstr + end + + def status + secs_up = Time.new - @startup_time + uptime = Utils.secs_to_string secs_up + return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received." + end + + + private + + # handle help requests for "core" topics + def corehelp(topic="") + case topic + when "quit" + return "quit [] => quit IRC with message " + when "join" + return "join [] => join channel with secret key if specified. #{@nick} also responds to invites if you have the required access level" + when "part" + return "part => part channel " + when "hide" + return "hide => part all channels" + when "save" + return "save => save current dynamic data and configuration" + when "rescan" + return "rescan => reload modules and static facts" + when "nick" + return "nick => attempt to change nick to " + when "say" + return "say | => say to or in private message to " + when "action" + return "action | => does a /me to or in private message to " + when "topic" + return "topic => set topic of to " + when "quiet" + return "quiet [in here|] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in " + when "talk" + return "talk [in here|] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in " + when "version" + return "version => describes software version" + when "botsnack" + return "botsnack => reward #{@nick} for being good" + when "hello" + return "hello|hi|hey|yo [#{@nick}] => greet the bot" + else + return "Core help topics: quit, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello" + end + end + + # handle incoming IRC PRIVMSG +m+ + def onprivmsg(m) + # log it first + if(m.action?) + if(m.private?) + log "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick + else + log "* #{m.sourcenick} #{m.message}", m.target + end + else + if(m.public?) + log "<#{m.sourcenick}> #{m.message}", m.target + else + log "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick + end + end + + # pass it off to plugins that want to hear everything + @plugins.delegate "listen", m + + if(m.private? && m.message =~ /^\001PING\s+(.+)\001/) + notice m.sourcenick, "\001PING #$1\001" + log "@ #{m.sourcenick} pinged me" + return + end + + if(m.address?) + case m.message + when (/^join\s+(\S+)\s+(\S+)$/i) + join $1, $2 if(@auth.allow?("join", m.source, m.replyto)) + when (/^join\s+(\S+)$/i) + join $1 if(@auth.allow?("join", m.source, m.replyto)) + when (/^part$/i) + part m.target if(m.public? && @auth.allow?("join", m.source, m.replyto)) + when (/^part\s+(\S+)$/i) + part $1 if(@auth.allow?("join", m.source, m.replyto)) + when (/^quit(?:\s+(.*))?$/i) + quit $1 if(@auth.allow?("quit", m.source, m.replyto)) + when (/^hide$/i) + join 0 if(@auth.allow?("join", m.source, m.replyto)) + when (/^save$/i) + if(@auth.allow?("config", m.source, m.replyto)) + save + m.okay + end + when (/^nick\s+(\S+)$/i) + nickchg($1) if(@auth.allow?("nick", m.source, m.replyto)) + when (/^say\s+(\S+)\s+(.*)$/i) + say $1, $2 if(@auth.allow?("say", m.source, m.replyto)) + when (/^action\s+(\S+)\s+(.*)$/i) + action $1, $2 if(@auth.allow?("say", m.source, m.replyto)) + when (/^topic\s+(\S+)\s+(.*)$/i) + topic $1, $2 if(@auth.allow?("topic", m.source, m.replyto)) + when (/^mode\s+(\S+)\s+(\S+)\s+(.*)$/i) + mode $1, $2, $3 if(@auth.allow?("mode", m.source, m.replyto)) + when (/^ping$/i) + say m.replyto, "pong" + when (/^rescan$/i) + if(@auth.allow?("config", m.source, m.replyto)) + m.okay + rescan + end + when (/^quiet$/i) + if(auth.allow?("talk", m.source, m.replyto)) + 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)) + 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 } + 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)) + m.okay + end + # 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.sendq_delay}" + end + when (/^options get sendq_burst$/i) + if auth.allow?("config", m.source, m.replyto) + 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.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.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) + when (/^registry stats$/i) + if auth.allow?("config", m.source, m.replyto) + m.reply @registry.stat.inspect + end + when (/^(version)|(introduce yourself)$/i) + say m.replyto, "I'm a v. #{$version} rubybot, (c) Tom Gilbert - http://linuxbrit.co.uk/rbot/" + when (/^help(?:\s+(.*))?$/i) + say m.replyto, help($1) + when (/^(botsnack|ciggie)$/i) + say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?) + say m.replyto, @lang.get("thanks") if(m.private?) + when (/^(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$)).*/i) + say m.replyto, @lang.get("hello_X") % m.sourcenick if(m.public?) + say m.replyto, @lang.get("hello") if(m.private?) + else + delegate_privmsg(m) + end + else + # stuff to handle when not addressed + case m.message + when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$))\s+#{@nick}$/i) + say m.replyto, @lang.get("hello_X") % m.sourcenick + when (/^#{@nick}!*$/) + say m.replyto, @lang.get("hello_X") % m.sourcenick + else + @keywords.privmsg(m) + end + end + end + + # log a message. Internal use only. + def log_sent(type, where, message) + case type + when "NOTICE" + if(where =~ /^#/) + log "-=#{@nick}=- #{message}", where + elsif (where =~ /(\S*)!.*/) + log "[-=#{where}=-] #{message}", $1 + else + log "[-=#{where}=-] #{message}" + end + when "PRIVMSG" + if(where =~ /^#/) + log "<#{@nick}> #{message}", where + elsif (where =~ /^(\S*)!.*$/) + log "[msg(#{where})] #{message}", $1 + else + log "[msg(#{where})] #{message}", where + end + end + end + + def onjoin(m) + @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel)) + if(m.address?) + log "@ Joined channel #{m.channel}", m.channel + puts "joined channel #{m.channel}" + else + log "@ #{m.sourcenick} joined channel #{m.channel}", m.channel + @channels[m.channel].users[m.sourcenick] = Hash.new + @channels[m.channel].users[m.sourcenick]["mode"] = "" + end + + @plugins.delegate("listen", m) + @plugins.delegate("join", m) + end + + def onpart(m) + if(m.address?) + log "@ Left channel #{m.channel} (#{m.message})", m.channel + @channels.delete(m.channel) + puts "left channel #{m.channel}" + else + log "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel + @channels[m.channel].users.delete(m.sourcenick) + end + + # delegate to plugins + @plugins.delegate("listen", m) + @plugins.delegate("part", m) + end + + # respond to being kicked from a channel + def onkick(m) + if(m.address?) + @channels.delete(m.channel) + log "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel + puts "kicked from channel #{m.channel}" + else + @channels[m.channel].users.delete(m.sourcenick) + log "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel + end + + @plugins.delegate("listen", m) + @plugins.delegate("kick", m) + end + + def ontopic(m) + @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel)) + @channels[m.channel].topic = m.topic if !m.topic.nil? + @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil? + @channels[m.channel].topic.by = m.source if !m.source.nil? + + debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}" + end + + # delegate a privmsg to auth, keyword or plugin handlers + def delegate_privmsg(message) + [@auth, @plugins, @keywords].each {|m| + break if m.privmsg(message) + } + end + +end + +end diff --git a/lib/rbot/ircsocket.rb b/lib/rbot/ircsocket.rb new file mode 100644 index 00000000..35857736 --- /dev/null +++ b/lib/rbot/ircsocket.rb @@ -0,0 +1,186 @@ +module Irc + + require 'socket' + require 'thread' + + # wrapped TCPSocket for communication with the server. + # emulates a subset of TCPSocket functionality + 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, sendq_delay=2, sendq_burst=4) + @server = server.dup + @port = port.to_i + @host = host + @lines_sent = 0 + @lines_received = 0 + if sendq_delay + @sendq_delay = sendq_delay.to_f + else + @sendq_delay = 2 + end + @last_send = Time.new - @sendq_delay + @burst = 0 + if sendq_burst + @sendq_burst = sendq_burst.to_i + else + @sendq_burst = 4 + end + end + + # open a TCP connection to the server + def connect + if(@host) + begin + @sock=TCPSocket.new(@server, @port, @host) + rescue ArgumentError => e + $stderr.puts "Your version of ruby does not support binding to a " + $stderr.puts "specific local address, please upgrade if you wish " + $stderr.puts "to use HOST = foo" + $stderr.puts "(this option has been disabled in order to continue)" + @sock=TCPSocket.new(@server, @port) + end + else + @sock=TCPSocket.new(@server, @port) + end + @qthread = false + @qmutex = Mutex.new + @sendq = Array.new + if (@sendq_delay > 0) + @qthread = Thread.new { spooler } + end + end + + def sendq_delay=(newfreq) + debug "changing sendq frequency to #{newfreq}" + @qmutex.synchronize do + @sendq_delay = newfreq + if newfreq == 0 && @qthread + clearq + Thread.kill(@qthread) + @qthread = false + elsif(newfreq != 0 && !@qthread) + @qthread = Thread.new { spooler } + end + end + end + + def sendq_burst=(newburst) + @qmutex.synchronize do + @sendq_burst = newburst + end + end + + # used to send lines to the remote IRCd + # message: IRC message to send + def puts(message) + @qmutex.synchronize do + # debug "In puts - got mutex" + puts_critical(message) + end + end + + # get the next line from the server (blocks) + def gets + reply = @sock.gets + @lines_received += 1 + if(reply) + reply.strip! + end + debug "RECV: #{reply.inspect}" + reply + end + + def queue(msg) + if @sendq_delay > 0 + @qmutex.synchronize do + # debug "QUEUEING: #{msg}" + @sendq.push msg + end + else + # just send it if queueing is disabled + self.puts(msg) + end + end + + def spooler + while true + spool + sleep 0.2 + end + end + + # pop a message off the queue, send it + def spool + unless @sendq.empty? + now = Time.new + if (now >= (@last_send + @sendq_delay)) + # reset burst counter after @sendq_delay has passed + @burst = 0 + debug "in spool, resetting @burst" + elsif (@burst >= @sendq_burst) + # nope. can't send anything + return + end + @qmutex.synchronize 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 + end + end + end + + def clearq + unless @sendq.empty? + @qmutex.synchronize do + @sendq.clear + end + end + end + + # flush the TCPSocket + def flush + @sock.flush + end + + # Wraps Kernel.select on the socket + def select(timeout) + Kernel.select([@sock], nil, nil, timeout) + end + + # shutdown the connection to the server + def shutdown(how=2) + @sock.shutdown(how) + end + + private + + # same as puts, but expects to be called with a mutex held on @qmutex + def puts_critical(message) + # debug "in puts_critical" + debug "SEND: #{message.inspect}" + @sock.send(message + "\n",0) + @last_send = Time.new + @lines_sent += 1 + @burst += 1 + end + + end + +end diff --git a/lib/rbot/keywords.rb b/lib/rbot/keywords.rb new file mode 100644 index 00000000..3305af29 --- /dev/null +++ b/lib/rbot/keywords.rb @@ -0,0 +1,427 @@ +require 'pp' + +module Irc + + # Keyword class + # + # Encapsulates a keyword ("foo is bar" is a keyword called foo, with type + # is, and has a single value of bar). + # Keywords can have multiple values, to_s() will choose one at random + class Keyword + + # type of keyword (e.g. "is" or "are") + attr_reader :type + + # type:: type of keyword (e.g "is" or "are") + # values:: array of values + # + # create a keyword of type +type+ with values +values+ + def initialize(type, values) + @type = type.downcase + @values = values + end + + # pick a random value for this keyword and return it + def to_s + if(@values.length > 1) + Keyword.unescape(@values[rand(@values.length)]) + else + Keyword.unescape(@values[0]) + end + end + + # describe the keyword (show all values without interpolation) + def desc + @values.join(" | ") + end + + # return the keyword in a stringified form ready for storage + def dump + @type + "/" + Keyword.unescape(@values.join("<=or=>")) + end + + # deserialize the stringified form to an object + def Keyword.restore(str) + if str =~ /^(\S+?)\/(.*)$/ + type = $1 + vals = $2.split("<=or=>") + return Keyword.new(type, vals) + end + return nil + end + + # values:: array of values to add + # add values to a keyword + def <<(values) + if(@values.length > 1 || values.length > 1) + values.each {|v| + @values << v + } + else + @values[0] += " or " + values[0] + end + end + + # unescape special words/characters in a keyword + def Keyword.unescape(str) + str.gsub(/\\\|/, "|").gsub(/ \\is /, " is ").gsub(/ \\are /, " are ").gsub(/\\\?(\s*)$/, "?\1") + end + + # escape special words/characters in a keyword + def Keyword.escape(str) + str.gsub(/\|/, "\\|").gsub(/ is /, " \\is ").gsub(/ are /, " \\are ").gsub(/\?(\s*)$/, "\\?\1") + end + end + + # keywords class. + # + # Handles all that stuff like "bot: foo is bar", "bot: foo?" + # + # Fallback after core and auth have had a look at a message and refused to + # handle it, checks for a keyword command or lookup, otherwise the message + # is delegated to plugins + class Keywords + + # create a new Keywords instance, associated to bot +bot+ + def initialize(bot) + @bot = bot + @statickeywords = Hash.new + upgrade_data + @keywords = DBTree.new bot, "keyword" + + scan + + # import old format keywords into DBHash + if(File.exist?("#{@bot.botclass}/keywords.rbot")) + puts "auto importing old keywords.rbot" + IO.foreach("#{@bot.botclass}/keywords.rbot") do |line| + if(line =~ /^(.*?)\s*<=(is|are)?=?>\s*(.*)$/) + lhs = $1 + mhs = $2 + rhs = $3 + mhs = "is" unless mhs + rhs = Keyword.escape rhs + values = rhs.split("<=or=>") + @keywords[lhs] = Keyword.new(mhs, values).dump + end + end + File.delete("#{@bot.botclass}/keywords.rbot") + end + end + + # drop static keywords and reload them from files, picking up any new + # keyword files that have been added + def rescan + @statickeywords = Hash.new + scan + end + + # load static keywords from files, picking up any new keyword files that + # have been added + def scan + # first scan for old DBHash files, and convert them + Dir["#{@bot.botclass}/keywords/*"].each {|f| + next unless f =~ /\.db$/ + puts "upgrading keyword db #{f} (rbot 0.9.5 or prior) database format" + newname = f.gsub(/\.db$/, ".kdb") + old = BDB::Hash.open f, nil, + "r+", 0600, "set_pagesize" => 1024, + "set_cachesize" => [0, 32 * 1024, 0] + new = BDB::CIBtree.open newname, nil, + BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, + 0600, "set_pagesize" => 1024, + "set_cachesize" => [0, 32 * 1024, 0] + old.each {|k,v| + new[k] = v + } + old.close + new.close + File.delete(f) + } + + # then scan for current DBTree files, and load them + Dir["#{@bot.botclass}/keywords/*"].each {|f| + next unless f =~ /\.kdb$/ + hsh = DBTree.new @bot, f, true + key = File.basename(f).gsub(/\.kdb$/, "") + debug "keywords module: loading DBTree file #{f}, key #{key}" + @statickeywords[key] = hsh + } + + # then scan for non DB files, and convert/import them and delete + Dir["#{@bot.botclass}/keywords/*"].each {|f| + next if f =~ /\.kdb$/ + next if f =~ /CVS$/ + puts "auto converting keywords from #{f}" + key = File.basename(f) + unless @statickeywords.has_key?(key) + @statickeywords[key] = DBHash.new @bot, "#{f}.db", true + end + IO.foreach(f) {|line| + if(line =~ /^(.*?)\s*\s*(.*)$/) + lhs = $1 + mhs = $2 + rhs = $3 + # support infobot style factfiles, by fixing them up here + rhs.gsub!(/\$who/, "") + mhs = "is" unless mhs + rhs = Keyword.escape rhs + values = rhs.split("<=or=>") + @statickeywords[key][lhs] = Keyword.new(mhs, values).dump + end + } + File.delete(f) + @statickeywords[key].flush + } + end + + # upgrade data files found in old rbot formats to current + def upgrade_data + if File.exist?("#{@bot.botclass}/keywords.db") + puts "upgrading old keywords (rbot 0.9.5 or prior) database format" + old = BDB::Hash.open "#{@bot.botclass}/keywords.db", nil, + "r+", 0600, "set_pagesize" => 1024, + "set_cachesize" => [0, 32 * 1024, 0] + new = BDB::CIBtree.open "#{@bot.botclass}/keyword.db", nil, + BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, + 0600, "set_pagesize" => 1024, + "set_cachesize" => [0, 32 * 1024, 0] + old.each {|k,v| + new[k] = v + } + old.close + new.close + File.delete("#{@bot.botclass}/keywords.db") + end + end + + # save dynamic keywords to file + def save + @keywords.flush + end + def oldsave + File.open("#{@bot.botclass}/keywords.rbot", "w") do |file| + @keywords.each do |key, value| + file.puts "#{key}<=#{value.type}=>#{value.dump}" + end + end + end + + # lookup keyword +key+, return it or nil + def [](key) + debug "keywords module: looking up key #{key}" + if(@keywords.has_key?(key)) + return Keyword.restore(@keywords[key]) + else + # key name order for the lookup through these + @statickeywords.keys.sort.each {|k| + v = @statickeywords[k] + if v.has_key?(key) + return Keyword.restore(v[key]) + end + } + end + return nil + end + + # does +key+ exist as a keyword? + def has_key?(key) + if @keywords.has_key?(key) && Keyword.restore(@keywords[key]) != nil + return true + end + @statickeywords.each {|k,v| + if v.has_key?(key) && Keyword.restore(v[key]) != nil + return true + end + } + return false + end + + # m:: PrivMessage containing message info + # key:: key being queried + # dunno:: optional, if true, reply "dunno" if +key+ not found + # + # handle a message asking about a keyword + def keyword(m, key, dunno=true) + unless(kw = self[key]) + m.reply @bot.lang.get("dunno") if (dunno) + return + end + response = kw.to_s + response.gsub!(//, m.sourcenick) + if(response =~ /^\s*(.*)/) + m.reply "#$1" + elsif(response =~ /^\s*(.*)/) + @bot.action m.replyto, "#$1" + elsif(m.public? && response =~ /^\s*(.*)/) + topic = $1 + @bot.topic m.target, topic + else + m.reply "#{key} #{kw.type} #{response}" + end + end + + + # m:: PrivMessage containing message info + # target:: channel/nick to tell about the keyword + # key:: key being queried + # + # handle a message asking the bot to tell someone about a keyword + def keyword_tell(m, target, key) + unless(kw = self[key]) + @bot.say m.sourcenick, @bot.lang.get("dunno_about_X") % key + return + end + response = kw.to_s + response.gsub!(//, m.sourcenick) + if(response =~ /^\s*(.*)/) + @bot.say target, "#{m.sourcenick} wanted me to tell you: (#{key}) #$1" + m.reply "okay, I told #{target}: (#{key}) #$1" + elsif(response =~ /^\s*(.*)/) + @bot.action target, "#$1 (#{m.sourcenick} wanted me to tell you)" + m.reply "okay, I told #{target}: * #$1" + else + @bot.say target, "#{m.sourcenick} wanted me to tell you that #{key} #{kw.type} #{response}" + m.reply "okay, I told #{target} that #{key} #{kw.type} #{response}" + end + end + + # handle a message which alters a keyword + # like "foo is bar", or "no, foo is baz", or "foo is also qux" + def keyword_command(sourcenick, target, lhs, mhs, rhs, quiet=false) + debug "got keyword command #{lhs}, #{mhs}, #{rhs}" + overwrite = false + overwrite = true if(lhs.gsub!(/^no,\s*/, "")) + also = true if(rhs.gsub!(/^also\s+/, "")) + values = rhs.split(/\s+\|\s+/) + lhs = Keyword.unescape lhs + if(overwrite || also || !has_key?(lhs)) + if(also && has_key?(lhs)) + kw = self[lhs] + kw << values + @keywords[lhs] = kw.dump + else + @keywords[lhs] = Keyword.new(mhs, values).dump + end + @bot.okay target if !quiet + elsif(has_key?(lhs)) + kw = self[lhs] + @bot.say target, "but #{lhs} #{kw.type} #{kw.desc}" if kw && !quiet + end + end + + # return help string for Keywords with option topic +topic+ + def help(topic="") + case topic + when "overview" + return "set: is , overide: no, is , add to definition: is also , random responses: is | [| ...], plurals: are , escaping: \\is, \\are, \\|, specials: , , " + when "set" + return "set => is " + when "plurals" + return "plurals => are " + when "override" + return "overide => no, is " + when "also" + return "also => is also " + when "random" + return "random responses => is | [| ...]" + when "get" + return "asking for keywords => (with addressing) \"?\", (without addressing) \"'\"" + when "tell" + return "tell about => if is known, tell , via /msg, its definition" + when "forget" + return "forget => forget fact " + when "keywords" + return "keywords => show current keyword counts" + when "" + return " => normal response is \" is \", but if begins with , the response will be \"\"" + when "" + return " => makes keyword respnse \"/me \"" + when "" + return " => replaced with questioner in reply" + when "" + return " => respond by setting the topic to the rest of the definition" + when "search" + return "keywords search [--all] [--full] => search keywords for . If --all is set, search static keywords too, if --full is set, search definitions too." + else + return "Keyword module (Fact learning and regurgitation) topics: overview, set, plurals, override, also, random, get, tell, forget, keywords, keywords search, , , , " + end + end + + # privmsg handler + def privmsg(m) + return if m.replied? + if(m.address?) + if(!(m.message =~ /\\\?\s*$/) && m.message =~ /^(.*\S)\s*\?\s*$/) + keyword m, $1 if(@bot.auth.allow?("keyword", m.source, m.replyto)) + elsif(m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/) + keyword_command(m.sourcenick, m.replyto, $1, $2, $3) if(@bot.auth.allow?("keycmd", m.source, m.replyto)) + elsif (m.message =~ /^tell\s+(\S+)\s+about\s+(.+)$/) + keyword_tell(m, $1, $2) if(@bot.auth.allow?("keyword", m.source, m.replyto)) + elsif (m.message =~ /^forget\s+(.*)$/) + key = $1 + if((@bot.auth.allow?("keycmd", m.source, m.replyto)) && @keywords.has_key?(key)) + @keywords.delete(key) + @bot.okay m.replyto + end + elsif (m.message =~ /^keywords$/) + if(@bot.auth.allow?("keyword", m.source, m.replyto)) + length = 0 + @statickeywords.each {|k,v| + length += v.length + } + m.reply "There are currently #{@keywords.length} keywords, #{length} static facts defined." + end + elsif (m.message =~ /^keywords search\s+(.*)$/) + str = $1 + all = false + all = true if str.gsub!(/--all\s+/, "") + full = false + full = true if str.gsub!(/--full\s+/, "") + + re = Regexp.new(str, Regexp::IGNORECASE) + if(@bot.auth.allow?("keyword", m.source, m.replyto)) + matches = Array.new + @keywords.each {|k,v| + kw = Keyword.restore(v) + if re.match(k) || (full && re.match(kw.desc)) + matches << [k,kw] + end + } + if all + @statickeywords.each {|k,v| + v.each {|kk,vv| + kw = Keyword.restore(vv) + if re.match(kk) || (full && re.match(kw.desc)) + matches << [kk,kw] + end + } + } + end + if matches.length == 1 + rkw = matches[0] + m.reply "#{rkw[0]} #{rkw[1].type} #{rkw[1].desc}" + elsif matches.length > 0 + i = 0 + matches.each {|rkw| + m.reply "[#{i+1}/#{matches.length}] #{rkw[0]} #{rkw[1].type} #{rkw[1].desc}" + i += 1 + break if i == 3 + } + else + m.reply "no keywords match #{str}" + end + end + end + else + # in channel message, not to me + 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+(.*)$/)) + # 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 + end + end + end +end diff --git a/lib/rbot/language.rb b/lib/rbot/language.rb new file mode 100644 index 00000000..9788b2bb --- /dev/null +++ b/lib/rbot/language.rb @@ -0,0 +1,55 @@ +module Irc + + class Language + def initialize(language, file="") + @language = language + if file.empty? + file = File.dirname(__FILE__) + "/languages/#{@language}.lang" + end + unless(FileTest.exist?(file)) + raise "no such language: #{@language} (no such file #{file})" + end + @file = file + scan + end + + def scan + @strings = Hash.new + current_key = nil + IO.foreach(@file) {|l| + next if l =~ /^$/ + next if l =~ /^\s*#/ + if(l =~ /^(\S+):$/) + @strings[$1] = Array.new + current_key = $1 + elsif(l =~ /^\s*(.*)$/) + @strings[current_key] << $1 + end + } + end + + def rescan + scan + end + + def get(key) + if(@strings.has_key?(key)) + return @strings[key][rand(@strings[key].length)] + else + raise "undefined language key" + end + end + + def save + File.open(@file, "w") {|file| + @strings.each {|key,val| + file.puts "#{key}:" + val.each_value {|v| + file.puts " #{v}" + } + } + } + end + end + +end diff --git a/lib/rbot/languages/dutch.lang b/lib/rbot/languages/dutch.lang new file mode 100644 index 00000000..db116264 --- /dev/null +++ b/lib/rbot/languages/dutch.lang @@ -0,0 +1,73 @@ +okay: + ok + ok dan :) + goed + mooi + voila + in orde + 't is gebeurd + zeker + dat kan ik! + komt in orde + k + ik zal het eens doen +dunno: + geen idee + dat weet ik niet + dat gaat m'n petje te boven + *haal schouder op* + vraag dat aan iemand anders + dat moet je niet aan mij vragen + wie weet dat? + dat kan ik niet + laat je eens nakijken... + wat vraag je nu? +dunno_about_X: + maar ik weet niks over %s + Ik heb nog nooit van %s gehoord :( + maar wat is %s? +insult: + %s: idioot! + %s: :( + %s: Ik haat je:( + %s: val dood! + %s: Ik ben beledigd! +hello: + hallo :) + hey! + hi + yo + yow + joe + jowjowjow +hello_X: + hallo %s :) + %s: hallo + hey %s :) + %s: hi! + yo %s! + joe %s! + alles ok %s? + %s: alles goed? +welcome: + geen probleem + 't is niks + altijd welkom + graag gedaan + np :) +thanks: + danku :) + bedankt! + thx :) + =D + je bent een schatje :) +thanks_X: + %s: danku :) + %s: bedankt! + %s: =D + %s: thx :) + %s: je bent een schatje :) +quit: + ok ik ben weg + yo + ciao diff --git a/lib/rbot/languages/english.lang b/lib/rbot/languages/english.lang new file mode 100644 index 00000000..f275d82f --- /dev/null +++ b/lib/rbot/languages/english.lang @@ -0,0 +1,75 @@ +okay: + okay + okay then :) + okies! + fine + done + can do! + alright + sure + aight + lemme take care of that for you +dunno: + dunno + beats me + no idea + no clue + ...eh? + *shrug* + don't ask me + who knows? + I can't do that Dave. + you best check yo'self! +dunno_about_X: + but I dunno anything about %s + I never heard of %s :( + %s? what's that then? + but what's %s? +insult: + %s: wanker! + %s: :( + %s: I hate you :( + %s: die! + %s: I'm offended! + %s: you hurt my feelings +hello: + hello :) + hola :) + salut + hey! + word. + hi + yo + 'sup? +hello_X: + hello %s :) + %s: hey there + %s: hola :) + %s: salut + hey %s :) + %s: word + %s: hi! + yo %s! + %s: 'sup? + 'sup %s? +welcome: + no probbie + you're welcome + de nada + any time + np :) +thanks: + thanks :) + schweet! + ta :) + =D + cheers! +thanks_X: + %s: thanks :) + %s: schweet! + %s: =D + %s: ta :) + %s: cheers +quit: + okay bye + seeya diff --git a/lib/rbot/languages/german.lang b/lib/rbot/languages/german.lang new file mode 100644 index 00000000..3e23268d --- /dev/null +++ b/lib/rbot/languages/german.lang @@ -0,0 +1,67 @@ +okay: + okay + okay na dann :) + gut + gemacht + wird gemacht! + also los + sicher + klar + lass mich sorge tragen :-) +dunno: + kann nicht + schlag mich + habe keine Idee + habe keine Ahnung + *achselzuck* + frag mich nicht + wer weiss? + ich kann es nicht tun + am besten du schaust selber nach +dunno_about_X: + ich weiss nichts uber %s + was zum Teufel ist %s? +insult: + %s: Arsch! + %s: :( + %s: Ich hasse dich :( + %s: Stirb! + %s: Ich bin beleidigt! +hello: + hallo :) + hola :) + salut + hey! + sag nichts. + hi + yo + Was geht? +hello_X: + hallo %s :) + %s: guggus + %s: hola :) + %s: salut + hey %s :) + %s: sag nichts + %s: hi! + yo %s! + %s: Was geht? + Was geht %s? +welcome: + no probbie + you're welcome + de nada + any time + np :) +thanks: + Danke :) + juhu :) + :-D + Prost! +thanks_X: + %s: danke :) + %s: :-D + %s: juhu :) + %s: Prost +quit: + forkbomb rockt diff --git a/lib/rbot/message.rb b/lib/rbot/message.rb new file mode 100644 index 00000000..d7f614ab --- /dev/null +++ b/lib/rbot/message.rb @@ -0,0 +1,256 @@ +module Irc + + # base user message class, all user messages derive from this + # (a user message is defined as having a source hostmask, a target + # nick/channel and a message part) + class BasicUserMessage + + # associated bot + attr_reader :bot + + # when the message was received + attr_reader :time + + # hostmask of message source + attr_reader :source + + # nick of message source + attr_reader :sourcenick + + # url part of message source + attr_reader :sourceaddress + + # nick/channel message was sent to + attr_reader :target + + # contents of the message + attr_accessor :message + + # has the message been replied to/handled by a plugin? + attr_accessor :replied + + # instantiate a new Message + # bot:: associated bot class + # source:: hostmask of the message source + # target:: nick/channel message is destined for + # message:: message part + def initialize(bot, source, target, message) + @time = Time.now + @bot = bot + @source = source + @address = false + @target = target + @message = BasicUserMessage.stripcolour message + @replied = false + + # split source into consituent parts + if source =~ /^((\S+)!(\S+))$/ + @sourcenick = $2 + @sourceaddress = $3 + end + + if target && target.downcase == @bot.nick.downcase + @address = true + end + + end + + # returns true if the message was addressed to the bot. + # This includes any private message to the bot, or any public message + # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo", + # a kick message when bot was kicked etc. + def address? + return @address + end + + # has this message been replied to by a plugin? + def replied? + return @replied + end + + # strip mIRC colour escapes from a string + def BasicUserMessage.stripcolour(string) + return "" unless string + ret = string.gsub(/\cC\d\d?(?:,\d\d?)?/, "") + #ret.tr!("\x00-\x1f", "") + ret + end + + end + + # class for handling IRC user messages. Includes some utilities for handling + # the message, for example in plugins. + # The +message+ member will have any bot addressing "^bot: " removed + # (address? will return true in this case) + class UserMessage < BasicUserMessage + + # for plugin messages, the name of the plugin invoked by the message + attr_reader :plugin + + # for plugin messages, the rest of the message, with the plugin name + # removed + attr_reader :params + + # convenience member. Who to reply to (i.e. would be sourcenick for a + # privately addressed message, or target (the channel) for a publicly + # addressed message + attr_reader :replyto + + # channel the message was in, nil for privately addressed messages + attr_reader :channel + + # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff + # will be stripped from the message) + attr_reader :action + + # instantiate a new UserMessage + # bot:: associated bot class + # source:: hostmask of the message source + # target:: nick/channel message is destined for + # message:: message part + def initialize(bot, source, target, message) + super(bot, source, target, message) + @target = target + @private = false + @plugin = nil + @action = false + + if target.downcase == @bot.nick.downcase + @private = true + @address = true + @channel = nil + @replyto = @sourcenick + else + @replyto = @target + @channel = @target + end + + # check for option extra addressing prefixes, e.g "|search foo", or + # "!version" - first match wins + bot.addressing_prefixes.each {|mprefix| + if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "") + @address = true + break + end + } + + # even if they used above prefixes, we allow for silly people who + # combine all possible types, e.g. "|rbot: hello", or + # "/msg rbot rbot: hello", etc + if @message.gsub!(/^\s*#{bot.nick}\s*([:;,>]|\s)\s*/, "") + @address = true + end + + if(@message =~ /^\001ACTION\s(.+)\001/) + @message = $1 + @action = true + end + + # free splitting for plugins + @params = @message.dup + if @params.gsub!(/^\s*(\S+)[\s$]*/, "") + @plugin = $1.downcase + @params = nil unless @params.length > 0 + end + end + + # returns true for private messages, e.g. "/msg bot hello" + def private? + return @private + end + + # returns true if the message was in a channel + def public? + return !@private + end + + def action? + return @action + end + + # convenience method to reply to a message, useful in plugins. It's the + # same as doing: + # @bot.say m.replyto, string + # So if the message is private, it will reply to the user. If it was + # in a channel, it will reply in the channel. + def reply(string) + @bot.say @replyto, string + @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 + class PrivMessage < UserMessage + end + + # class to manage IRC NOTICEs + class NoticeMessage < UserMessage + end + + # class to manage IRC KICKs + # +address?+ can be used as a shortcut to see if the bot was kicked, + # basically, +target+ was kicked from +channel+ by +source+ with +message+ + class KickMessage < BasicUserMessage + # channel user was kicked from + attr_reader :channel + + def initialize(bot, source, target, channel, message="") + super(bot, source, target, message) + @channel = channel + end + end + + # class to pass IRC Nick changes in. @message contains the old nickame, + # @sourcenick contains the new one. + class NickMessage < BasicUserMessage + def initialize(bot, source, oldnick, newnick) + super(bot, source, oldnick, newnick) + end + end + + class QuitMessage < BasicUserMessage + def initialize(bot, source, target, message="") + super(bot, source, target, message) + end + end + + class TopicMessage < BasicUserMessage + # channel topic + attr_reader :topic + # topic set at (unixtime) + attr_reader :timestamp + # topic set on channel + attr_reader :channel + + def initialize(bot, source, channel, timestamp, topic="") + super(bot, source, channel, topic) + @topic = topic + @timestamp = timestamp + @channel = channel + end + end + + # class to manage channel joins + class JoinMessage < BasicUserMessage + # channel joined + attr_reader :channel + def initialize(bot, source, channel, message="") + super(bot, source, channel, message) + @channel = channel + # in this case sourcenick is the nick that could be the bot + @address = (sourcenick.downcase == @bot.nick.downcase) + end + end + + # class to manage channel parts + # same as a join, but can have a message too + class PartMessage < JoinMessage + end +end diff --git a/lib/rbot/messagemapper.rb b/lib/rbot/messagemapper.rb new file mode 100644 index 00000000..42563d23 --- /dev/null +++ b/lib/rbot/messagemapper.rb @@ -0,0 +1,168 @@ +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] + if passes_requirements?(item, value) + options[item] = value + else + if @defaults.has_key?(item) + debug "item #{item} doesn't pass reqs but has a default of #{@defaults[item]}" + options[item] = @defaults[item].clone + # push the test-failed component back on the stack + components.unshift value + else + return nil, requirements_for(item) + end + end + 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/lib/rbot/plugins.rb b/lib/rbot/plugins.rb new file mode 100644 index 00000000..5db047fb --- /dev/null +++ b/lib/rbot/plugins.rb @@ -0,0 +1,300 @@ +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 + # differentiate them, use message.kind_of? It'll be + # either a PrivMessage, NoticeMessage, KickMessage, + # QuitMessage, PartMessage, JoinMessage, NickMessage, + # etc. + # + # privmsg(PrivMessage):: + # called for a PRIVMSG if the first word matches one + # the plugin register()d for. Use m.plugin to get + # that word and m.params for the rest of the message, + # if applicable. + # + # kick(KickMessage):: + # Called when a user (or the bot) is kicked from a + # channel the bot is in. + # + # join(JoinMessage):: + # Called when a user (or the bot) joins a channel + # + # part(PartMessage):: + # Called when a user (or the bot) parts a channel + # + # quit(QuitMessage):: + # Called when a user (or the bot) quits IRC + # + # nick(NickMessage):: + # Called when a user (or the bot) changes Nick + # topic(TopicMessage):: + # Called when a user (or the bot) changes a channel + # topic + # + # save:: Called when you are required to save your plugin's + # state, if you maintain data between sessions + # + # cleanup:: called before your plugin is "unloaded", prior to a + # 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 + @names.join("|") + end + + # return a help string for your module. for complex modules, you may wish + # to break your help into topics, and return a list of available topics if + # +topic+ is nil. +plugin+ is passed containing the matching prefix for + # this message - if your plugin handles multiple prefixes, make sure your + # return the correct help for the prefix requested + def help(plugin, topic) + "no help" + end + + # register the plugin as a handler for messages prefixed +name+ + # 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 + + # 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 + # handling + class Plugins + # hash of registered message prefixes and associated plugins + @@plugins = Hash.new + # associated IrcBot class + @@bot = nil + + # bot:: associated IrcBot class + # dirlist:: array of directories to scan (in order) for plugins + # + # create a new plugin handler, scanning for plugins in +dirlist+ + def initialize(bot, dirlist) + @@bot = bot + @dirs = dirlist + scan + end + + # access to associated bot + def Plugins.bot + @@bot + end + + # access to list of plugins + def Plugins.plugins + @@plugins + end + + # load plugins from pre-assigned list of directories + def scan + dirs = Array.new + dirs << File.dirname(__FILE__) + "/plugins" + dirs += @dirs + dirs.each {|dir| + if(FileTest.directory?(dir)) + d = Dir.new(dir) + d.each {|file| + next if(file =~ /^\./) + next unless(file =~ /\.rb$/) + @tmpfilename = "#{dir}/#{file}" + + # create a new, anonymous module to "house" the plugin + plugin_module = Module.new + + begin + plugin_string = IO.readlines(@tmpfilename).join("") + puts "loading module: #{@tmpfilename}" + plugin_module.module_eval(plugin_string) + rescue StandardError, NameError, LoadError, SyntaxError => err + puts "plugin #{@tmpfilename} load failed: " + err + puts err.backtrace.join("\n") + end + } + end + } + end + + # call the save method for each active plugin + def save + @@plugins.values.uniq.each {|p| + next unless(p.respond_to?("save")) + begin + p.save + rescue StandardError, NameError, SyntaxError => err + puts "plugin #{p.name} save() failed: " + err + puts err.backtrace.join("\n") + end + } + end + + # call the cleanup method for each active plugin + def cleanup + @@plugins.values.uniq.each {|p| + next unless(p.respond_to?("cleanup")) + begin + p.cleanup + rescue StandardError, NameError, SyntaxError => err + puts "plugin #{p.name} cleanup() failed: " + err + puts err.backtrace.join("\n") + end + } + end + + # drop all plugins and rescan plugins on disk + # calls save and cleanup for each plugin before dropping them + def rescan + save + cleanup + @@plugins = Hash.new + scan + end + + # return list of help topics (plugin names) + def helptopics + if(@@plugins.length > 0) + # return " [plugins: " + @@plugins.keys.sort.join(", ") + "]" + return " [#{length} plugins: " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ") + "]" + else + return " [no plugins active]" + end + end + + def length + @@plugins.values.uniq.length + end + + # return help for +topic+ (call associated plugin's help method) + def help(topic="") + if(topic =~ /^(\S+)\s*(.*)$/) + key = $1 + params = $2 + if(@@plugins.has_key?(key)) + begin + return @@plugins[key].help(key, params) + rescue StandardError, NameError, SyntaxError => err + puts "plugin #{@@plugins[key].name} help() failed: " + err + puts err.backtrace.join("\n") + end + else + return false + end + end + end + + # see if each plugin handles +method+, and if so, call it, passing + # +message+ as a parameter + def delegate(method, message) + @@plugins.values.uniq.each {|p| + if(p.respond_to? method) + begin + p.send method, message + rescue StandardError, NameError, SyntaxError => err + puts "plugin #{p.name} #{method}() failed: " + err + puts err.backtrace.join("\n") + end + end + } + end + + # 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) + return unless(m.plugin) + if (@@plugins.has_key?(m.plugin) && + @@plugins[m.plugin].respond_to?("privmsg") && + @@bot.auth.allow?(m.plugin, m.source, m.replyto)) + begin + @@plugins[m.plugin].privmsg(m) + rescue StandardError, NameError, SyntaxError => err + puts "plugin #{@@plugins[m.plugin].name} privmsg() failed: " + err + puts err.backtrace.join("\n") + end + return true + end + return false + end + end + +end diff --git a/lib/rbot/plugins/autoop.rb b/lib/rbot/plugins/autoop.rb new file mode 100644 index 00000000..fdbcf6e0 --- /dev/null +++ b/lib/rbot/plugins/autoop.rb @@ -0,0 +1,68 @@ +class AutoOP < Plugin + @@handlers = { + "addop" => "handle_addop", + "rmop" => "handle_rmop", + "listop" => "handle_listop" + } + + def help(plugin, topic="") + "perform autoop based on hostmask - usage: addop , rmop , listop" + end + + def join(m) + if(!m.address?) + @registry.each { |mask,channels| + if(Irc.netmaskmatch(mask, m.source) && channels.include?(m.channel)) + @bot.mode(m.channel, "+o", m.sourcenick) + end + } + end + end + + def privmsg(m) + if(m.private?) + if (!m.params || m.params == "list") + handle_listop(m) + elsif (m.params =~ /^add\s+(.+)$/) + handle_addop(m, $1) + elsif (m.params =~ /^rm\s+(.+)$/) + handle_rmop(m, $1) + end + end + end + + def handle_addop(m, params) + ma = /^(.+?)(\s+(.+))?$/.match(params) + channels = ma[2] ? ma[2] : @bot.config['JOIN_CHANNELS'] + if(ma[1] && channels) + @registry[ma[1]] = channels.split(/,\s*/).collect { |x| + x.strip + } + m.okay + else + m.reply @bot.lang.get('dunno') + end + end + + def handle_rmop(m, params) + if(!@registry.delete(params)) + m.reply @bot.lang.get('dunno') + else + m.okay + end + end + + def handle_listop(m) + if(@registry.length) + @registry.each { |mask,channels| + m.reply "#{mask} in #{channels.join(', ')}" + } + else + m.reply "No entrys" + end + end +end + +plugin = AutoOP.new +plugin.register("autoop") + diff --git a/lib/rbot/plugins/autorejoin.rb b/lib/rbot/plugins/autorejoin.rb new file mode 100644 index 00000000..aba46507 --- /dev/null +++ b/lib/rbot/plugins/autorejoin.rb @@ -0,0 +1,16 @@ +class AutoRejoinPlugin < Plugin + def help(plugin, topic="") + "performs an automatic rejoin if the bot is kicked from a channel" + end + def kick(m) + if m.address? + @bot.timer.add_once(10, m) {|m| + @bot.join m.channel + @bot.say m.channel, @bot.lang.get("insult") % m.sourcenick + } + end + end +end + +plugin = AutoRejoinPlugin.new +plugin.register("autorejoin") diff --git a/lib/rbot/plugins/cal.rb b/lib/rbot/plugins/cal.rb new file mode 100644 index 00000000..4f28310b --- /dev/null +++ b/lib/rbot/plugins/cal.rb @@ -0,0 +1,15 @@ +class CalPlugin < Plugin + def help(plugin, topic="") + "cal [options] => show current calendar [unix cal options]" + end + 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.map 'cal :month :year', :requirements => {:month => /^\d+$/, :year => /^\d+$/} +plugin.map 'cal' diff --git a/lib/rbot/plugins/dice.rb b/lib/rbot/plugins/dice.rb new file mode 100644 index 00000000..928da894 --- /dev/null +++ b/lib/rbot/plugins/dice.rb @@ -0,0 +1,81 @@ +################## +# Filename: dice.rb +# Description: Rbot plugin. Rolls rpg style dice +# Author: David Dorward (http://david.us-lot.org/ - you might find a more up to date version of this plugin there) +# Version: 0.3.2 +# Date: Sat 6 Apr 2002 +# +# You can get rbot from: http://www.linuxbrit.co.uk/rbot/ +# +# Changelog +# 0.1 - Initial release +# 0.1.1 - bug fix, only 1 digit for number of dice sides on first roll +# 0.3.0 - Spelling correction on changelog 0.1.1 +# - Return results of each roll +# 0.3.1 - Minor documentation update +# 0.3.2 - Bug fix, could not subtract numbers (String can't be coerced into Fixnum) +# +# TODO: Test! Test! Test! +# Comment! +# Fumble/Critical counter (1's and x's where x is sides on dice) +#################################################### + +class DiceDisplay + attr_reader :total, :view + def initialize(view, total) + @total = total + @view = view + end +end + +class DicePlugin < Plugin + def help(plugin, topic="") + "dice (where is something like: d6 or 2d6 or 2d6+4 or 2d6+1d20 or 2d6+1d5+4d7-3d4-6) => Rolls that set of virtual dice" + end + + def rolldice(d) + dice = d.split(/d/) + r = 0 + unless dice[0] =~ /^[0-9]+/ + dice[0] = 1 + end + for i in 0...dice[0].to_i + r = r + rand(dice[1].to_i) + 1 + end + return r + end + + def iddice(d) + porm = d.slice!(0,1) + if d =~ /d/ + r = rolldice(d) + else + r = d + end + if porm == "-" + r = 0 - r.to_i + end + viewer = DiceDisplay.new("[" + porm.to_s + d.to_s + "=" + r.to_s + "] ", r) + return viewer + end + + def privmsg(m) + unless(m.params && m.params =~ /^[0-9]*d[0-9]+([+-]([0-9]+|[0-9]*d[0-9])+)*$/) + m.reply "incorrect usage: " + help(m.plugin) + return + end + a = m.params.scan(/^[0-9]*d[0-9]+|[+-][0-9]*d[0-9]+|[+-][0-9]+/) + r = rolldice(a[0]) + t = "[" + a[0].to_s + "=" + r.to_s + "] " + for i in 1...a.length + tmp = iddice(a[i]) + r = r + tmp.total.to_i + t = t + tmp.view.to_s + end + m.reply r.to_s + " | " + t + end +end +plugin = DicePlugin.new +plugin.register("dice") +############################################## +#fin diff --git a/lib/rbot/plugins/eightball.rb b/lib/rbot/plugins/eightball.rb new file mode 100644 index 00000000..64748490 --- /dev/null +++ b/lib/rbot/plugins/eightball.rb @@ -0,0 +1,19 @@ +# Author: novex, daniel@novex.net.nz based on code from slap.rb by oct + +class EightBallPlugin < Plugin + def initialize + super + @answers=['yes', 'no', 'outlook not so good', 'all signs point to yes', 'all signs point to no', 'why the hell are you asking me?', 'the answer is unclear'] + end + def help(plugin, topic="") + "magic 8-ball ruby bot module written by novex for nvinfo on #dumber@quakenet, usage: 8ball will i ever beat this cancer?" + end + 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.map '8ball', :action => 'usage' +plugin.map '8ball *params', :action => 'eightball' diff --git a/lib/rbot/plugins/excuse.rb b/lib/rbot/plugins/excuse.rb new file mode 100644 index 00000000..38e85ad6 --- /dev/null +++ b/lib/rbot/plugins/excuse.rb @@ -0,0 +1,470 @@ +class ExcusePlugin < Plugin + # excuses courtesy of http://www.cs.wisc.edu/~ballard/bofh/ +@@excuses = [ +"clock speed", +"solar flares", +"electromagnetic radiation from satellite debris", +"static from nylon underwear", +"static from plastic slide rules", +"global warming", +"poor power conditioning", +"static buildup", +"doppler effect", +"hardware stress fractures", +"magnetic interference from money/credit cards", +"dry joints on cable plug", +"we're waiting for [the phone company] to fix that line", +"sounds like a Windows problem, try calling Microsoft support", +"temporary routing anomaly", +"somebody was calculating pi on the server", +"fat electrons in the lines", +"excess surge protection", +"floating point processor overflow", +"divide-by-zero error", +"POSIX compliance problem", +"monitor resolution too high", +"improperly oriented keyboard", +"network packets travelling uphill (use a carrier pigeon)", +"Decreasing electron flux", +"first Saturday after first full moon in Winter", +"radiosity depletion", +"CPU radiator broken", +"It works the way the Wang did, what's the problem", +"positron router malfunction", +"cellular telephone interference", +"techtonic stress", +"piezo-electric interference", +"(l)user error", +"working as designed", +"dynamic software linking table corrupted", +"heavy gravity fluctuation, move computer to floor rapidly", +"secretary plugged hairdryer into UPS", +"terrorist activities", +"not enough memory, go get system upgrade", +"interrupt configuration error", +"spaghetti cable cause packet failure", +"boss forgot system password", +"bank holiday - system operating credits not recharged", +"virus attack, luser responsible", +"waste water tank overflowed onto computer", +"Complete Transient Lockout", +"bad ether in the cables", +"Bogon emissions", +"Change in Earth's rotational speed", +"Cosmic ray particles crashed through the hard disk platter", +"Smell from unhygienic janitorial staff wrecked the tape heads", +"Little hamster in running wheel had coronary; waiting for replacement to be Fedexed from Wyoming", +"Evil dogs hypnotised the night shift", +"Plumber mistook routing panel for decorative wall fixture", +"Electricians made popcorn in the power supply", +"Groundskeepers stole the root password", +"high pressure system failure", +"failed trials, system needs redesigned", +"system has been recalled", +"not approved by the FCC", +"need to wrap system in aluminum foil to fix problem", +"not properly grounded, please bury computer", +"CPU needs recalibration", +"system needs to be rebooted", +"bit bucket overflow", +"descramble code needed from software company", +"only available on a need to know basis", +"knot in cables caused data stream to become twisted and kinked", +"nesting roaches shorted out the ether cable", +"The file system is full of it", +"Satan did it", +"Daemons did it", +"You're out of memory", +"There isn't any problem", +"Unoptimized hard drive", +"Typo in the code", +"Yes, yes, its called a design limitation", +"Look, buddy: Windows 3.1 IS A General Protection Fault.", +"That's a great computer you have there; have you considered how it would work as a BSD machine?", +"Please excuse me, I have to circuit an AC line through my head to get this database working.", +"Yeah, yo mama dresses you funny and you need a mouse to delete files.", +"Support staff hung over, send aspirin and come back LATER.", +"Someone is standing on the ethernet cable, causing a kink in the cable", +"Windows 95 undocumented 'feature'", +"Runt packets", +"Password is too complex to decrypt", +"Boss' kid fucked up the machine", +"Electromagnetic energy loss", +"Budget cuts", +"Mouse chewed through power cable", +"Stale file handle (next time use Tupperware(tm)!)", +"Feature not yet implemented", +"Internet outage", +"Pentium FDIV bug", +"Vendor no longer supports the product", +"Small animal kamikaze attack on power supplies", +"The vendor put the bug there.", +"SIMM crosstalk.", +"IRQ dropout", +"Collapsed Backbone", +"Power company testing new voltage spike (creation) equipment", +"operators on strike due to broken coffee machine", +"backup tape overwritten with copy of system manager's favourite CD", +"UPS interrupted the server's power", +"The electrician didn't know what the yellow cable was so he yanked the ethernet out.", +"The keyboard isn't plugged in", +"The air conditioning water supply pipe ruptured over the machine room", +"The electricity substation in the car park blew up.", +"The rolling stones concert down the road caused a brown out", +"The salesman drove over the CPU board.", +"The monitor is plugged into the serial port", +"Root nameservers are out of sync", +"electro-magnetic pulses from French above ground nuke testing.", +"your keyboard's space bar is generating spurious keycodes.", +"the real ttys became pseudo ttys and vice-versa.", +"the printer thinks its a router.", +"the router thinks its a printer.", +"evil hackers from Serbia.", +"we just switched to FDDI.", +"halon system went off and killed the operators.", +"because Bill Gates is a Jehovah's witness and so nothing can work on St. Swithin's day.", +"user to computer ratio too high.", +"user to computer ration too low.", +"we just switched to Sprint.", +"it has Intel Inside", +"Sticky bits on disk.", +"Power Company having EMP problems with their reactor", +"The ring needs another token", +"new management", +"telnet: Unable to connect to remote host: Connection refused", +"SCSI Chain overterminated", +"It's not plugged in.", +"because of network lag due to too many people playing deathmatch", +"You put the disk in upside down.", +"Daemons loose in system.", +"User was distributing pornography on server; system seized by FBI.", +"BNC (brain not connected)", +"UBNC (user brain not connected)", +"LBNC (luser brain not connected)", +"disks spinning backwards - toggle the hemisphere jumper.", +"new guy cross-connected phone lines with ac power bus.", +"had to use hammer to free stuck disk drive heads.", +"Too few computrons available.", +"Communications satellite used by the military for star wars.", +"Party-bug in the Aloha protocol.", +"Insert coin for new game", +"Dew on the telephone lines.", +"Arcserve crashed the server again.", +"Some one needed the powerstrip, so they pulled the switch plug.", +"My pony-tail hit the on/off switch on the power strip.", +"Big to little endian conversion error", +"You can tune a file system, but you can't tune a fish (from most tunefs man pages)", +"Dumb terminal", +"Zombie processes haunting the computer", +"Incorrect time synchronization", +"Defunct processes", +"Stubborn processes", +"non-redundant fan failure ", +"monitor VLF leakage", +"bugs in the RAID", +"no 'any' key on keyboard", +"root rot", +"Backbone Scoliosis", +"/pub/lunch", +"excessive collisions & not enough packet ambulances", +"le0: no carrier: transceiver cable problem?", +"broadcast packets on wrong frequency", +"popper unable to process jumbo kernel", +"NOTICE: alloc: /dev/null: filesystem full", +"pseudo-user on a pseudo-terminal", +"Recursive traversal of loopback mount points", +"Backbone adjustment", +"OS swapped to disk", +"vapors from evaporating sticky-note adhesives", +"sticktion", +"short leg on process table", +"multicasts on broken packets", +"ether leak", +"Atilla the Hub", +"endothermal recalibration", +"filesystem not big enough for Jumbo Kernel Patch", +"loop found in loop in redundant loopback", +"system consumed all the paper for paging", +"permission denied", +"Reformatting Page. Wait...", +"..disk or the processor is on fire.", +"SCSI's too wide.", +"Proprietary Information.", +"Just type 'mv * /dev/null'.", +"runaway cat on system.", +"Did you pay the new Support Fee?", +"We only support a 1200 bps connection.", +"We only support a 28000 bps connection.", +"Me no internet, only janitor, me just wax floors.", +"I'm sorry a pentium won't do, you need an SGI to connect with us.", +"Post-it Note Sludge leaked into the monitor.", +"the curls in your keyboard cord are losing electricity.", +"The monitor needs another box of pixels.", +"RPC_PMAP_FAILURE", +"kernel panic: write-only-memory (/dev/wom0) capacity exceeded.", +"Write-only-memory subsystem too slow for this machine. Contact your local dealer.", +"Just pick up the phone and give modem connect sounds. 'Well you said we should get more lines so we don't have voice lines.'", +"Quantum dynamics are affecting the transistors", +"Police are examining all internet packets in the search for a narco-net-trafficker", +"We are currently trying a new concept of using a live mouse. Unfortunately, one has yet to survive being hooked up to the computer.....please bear with us.", +"Your mail is being routed through Germany ... and they're censoring us.", +"Only people with names beginning with 'A' are getting mail this week (a la Microsoft)", +"We didn't pay the Internet bill and it's been cut off.", +"Lightning strikes.", +"Of course it doesn't work. We've performed a software upgrade.", +"Change your language to Finnish.", +"Fluorescent lights are generating negative ions. If turning them off doesn't work, take them out and put tin foil on the ends.", +"High nuclear activity in your area.", +"What office are you in? Oh, that one. Did you know that your building was built over the universities first nuclear research site? And wow, aren't you the lucky one, your office is right over where the core is buried!", +"The MGs ran out of gas.", +"The UPS doesn't have a battery backup.", +"Recursivity. Call back if it happens again.", +"Someone thought The Big Red Button was a light switch.", +"The mainframe needs to rest. It's getting old, you know.", +"I'm not sure. Try calling the Internet's head office -- it's in the book.", +"The lines are all busy (busied out, that is -- why let them in to begin with?).", +"Jan 9 16:41:27 huber su: 'su root' succeeded for .... on /dev/pts/1", +"It's those computer people in X {city of world}. They keep stuffing things up.", +"A star wars satellite accidently blew up the WAN.", +"Fatal error right in front of screen", +"That function is not currently supported, but Bill Gates assures us it will be featured in the next upgrade.", +"wrong polarity of neutron flow", +"Lusers learning curve appears to be fractal", +"We had to turn off that service to comply with the CDA Bill.", +"Ionization from the air-conditioning", +"TCP/IP UDP alarm threshold is set too low.", +"Someone is broadcasting pygmy packets and the router doesn't know how to deal with them.", +"The new frame relay network hasn't bedded down the software loop transmitter yet. ", +"Fanout dropping voltage too much, try cutting some of those little traces", +"Plate voltage too low on demodulator tube", +"You did wha... oh _dear_....", +"CPU needs bearings repacked", +"Too many little pins on CPU confusing it, bend back and forth until 10-20% are neatly removed. Do _not_ leave metal bits visible!", +"_Rosin_ core solder? But...", +"Software uses US measurements, but the OS is in metric...", +"The computer fleetly, mouse and all.", +"Your cat tried to eat the mouse.", +"The Borg tried to assimilate your system. Resistance is futile.", +"It must have been the lightning storm we had (yesterday) (last week) (last month)", +"Due to Federal Budget problems we have been forced to cut back on the number of users able to access the system at one time. (namely none allowed....)", +"Too much radiation coming from the soil.", +"Unfortunately we have run out of bits/bytes/whatever. Don't worry, the next supply will be coming next week.", +"Program load too heavy for processor to lift.", +"Processes running slowly due to weak power supply", +"Our ISP is having {switching,routing,SMDS,frame relay} problems", +"We've run out of licenses", +"Interference from lunar radiation", +"Standing room only on the bus.", +"You need to install an RTFM interface.", +"That would be because the software doesn't work.", +"That's easy to fix, but I can't be bothered.", +"Someone's tie is caught in the printer, and if anything else gets printed, he'll be in it too.", +"We're upgrading /dev/null", +"The Usenet news is out of date", +"Our POP server was kidnapped by a weasel.", +"It's stuck in the Web.", +"Your modem doesn't speak English.", +"The mouse escaped.", +"All of the packets are empty.", +"The UPS is on strike.", +"Neutrino overload on the nameserver", +"Melting hard drives", +"Someone has messed up the kernel pointers", +"The kernel license has expired", +"Netscape has crashed", +"The cord jumped over and hit the power switch.", +"It was OK before you touched it.", +"Bit rot", +"U.S. Postal Service", +"Your Flux Capacitor has gone bad.", +"The Dilithium Crystals need to be rotated.", +"The static electricity routing is acting up...", +"Traceroute says that there is a routing problem in the backbone. It's not our problem.", +"The co-locator cannot verify the frame-relay gateway to the ISDN server.", +"High altitude condensation from U.S.A.F prototype aircraft has contaminated the primary subnet mask. Turn off your computer for 9 days to avoid damaging it.", +"Lawn mower blade in your fan need sharpening", +"Electrons on a bender", +"Telecommunications is upgrading. ", +"Telecommunications is downgrading.", +"Telecommunications is downshifting.", +"Hard drive sleeping. Let it wake up on it's own...", +"Interference between the keyboard and the chair.", +"The CPU has shifted, and become decentralized.", +"Due to the CDA, we no longer have a root account.", +"We ran out of dial tone and we're and waiting for the phone company to deliver another bottle.", +"You must've hit the wrong any key.", +"PCMCIA slave driver", +"The Token fell out of the ring. Call us when you find it.", +"The hardware bus needs a new token.", +"Too many interrupts", +"Not enough interrupts", +"The data on your hard drive is out of balance.", +"Digital Manipulator exceeding velocity parameters", +"appears to be a Slow/Narrow SCSI-0 Interface problem", +"microelectronic Riemannian curved-space fault in write-only file system", +"fractal radiation jamming the backbone", +"routing problems on the neural net", +"IRQ-problems with the Un-Interruptible-Power-Supply", +"CPU-angle has to be adjusted because of vibrations coming from the nearby road", +"emissions from GSM-phones", +"CD-ROM server needs recalibration", +"firewall needs cooling", +"asynchronous inode failure", +"transient bus protocol violation", +"incompatible bit-registration operators", +"your process is not ISO 9000 compliant", +"You need to upgrade your VESA local bus to a MasterCard local bus.", +"The recent proliferation of Nuclear Testing", +"Elves on strike. (Why do they call EMAG Elf Magic)", +"Internet exceeded Luser level, please wait until a luser logs off before attempting to log back on.", +"Your EMAIL is now being delivered by the USPS.", +"Your computer hasn't been returning all the bits it gets from the Internet.", +"You've been infected by the Telescoping Hubble virus.", +"Scheduled global CPU outage", +"Your Pentium has a heating problem - try cooling it with ice cold water.(Do not turn off your computer, you do not want to cool down the Pentium Chip while he isn't working, do you?)", +"Your processor has processed too many instructions. Turn it off immediately, do not type any commands!!", +"Your packets were eaten by the terminator", +"Your processor does not develop enough heat.", +"We need a licensed electrician to replace the light bulbs in the computer room.", +"The POP server is out of Coke", +"Fiber optics caused gas main leak", +"Server depressed, needs Prozac", +"quantum decoherence", +"those damn raccoons!", +"suboptimal routing experience", +"A plumber is needed, the network drain is clogged", +"50% of the manual is in .pdf readme files", +"the AA battery in the wallclock sends magnetic interference", +"the xy axis in the trackball is coordinated with the summer solstice", +"the butane lighter causes the pincushioning", +"old inkjet cartridges emanate barium-based fumes", +"manager in the cable duct", +"We'll fix that in the next (upgrade, update, patch release, service pack).", +"HTTPD Error 666 : BOFH was here", +"HTTPD Error 4004 : very old Intel cpu - insufficient processing power", +"The ATM board has run out of 10 pound notes. We are having a whip round to refill it, care to contribute ?", +"Network failure - call NBC", +"Having to manually track the satellite.", +"Your/our computer(s) had suffered a memory leak, and we are waiting for them to be topped up.", +"The rubber band broke", +"We're on Token Ring, and it looks like the token got loose.", +"Stray Alpha Particles from memory packaging caused Hard Memory Error on Server.", +"paradigm shift...without a clutch", +"PEBKAC (Problem Exists Between Keyboard And Chair)", +"The cables are not the same length.", +"Second-system effect.", +"Chewing gum on /dev/sd3c", +"Boredom in the Kernel.", +"the daemons! the daemons! the terrible daemons!", +"I'd love to help you -- it's just that the Boss won't let me near the computer. ", +"struck by the Good Times virus", +"YOU HAVE AN I/O ERROR -> Incompetent Operator error", +"Your parity check is overdrawn and you're out of cache.", +"Communist revolutionaries taking over the server room and demanding all the computers in the building or they shoot the sysadmin. Poor misguided fools.", +"Plasma conduit breach", +"Out of cards on drive D:", +"Sand fleas eating the Internet cables", +"parallel processors running perpendicular today", +"ATM cell has no roaming feature turned on, notebooks can't connect", +"Webmasters kidnapped by evil cult.", +"Failure to adjust for daylight savings time.", +"Virus transmitted from computer to sysadmins.", +"Virus due to computers having unsafe sex.", +"Incorrectly configured static routes on the corerouters.", +"Forced to support NT servers; sysadmins quit.", +"Suspicious pointer corrupted virtual machine", +"It's the InterNIC's fault.", +"Root name servers corrupted.", +"Budget cuts forced us to sell all the power cords for the servers.", +"Someone hooked the twisted pair wires into the answering machine.", +"Operators killed by year 2000 bug bite.", +"We've picked COBOL as the language of choice.", +"Operators killed when huge stack of backup tapes fell over.", +"Robotic tape changer mistook operator's tie for a backup tape.", +"Someone was smoking in the computer room and set off the halon systems.", +"Your processor has taken a ride to Heaven's Gate on the UFO behind Hale-Bopp's comet.", +"it's an ID-10-T error", +"Dyslexics retyping hosts file on servers", +"The Internet is being scanned for viruses.", +"Your computer's union contract is set to expire at midnight.", +"Bad user karma.", +"/dev/clue was linked to /dev/null", +"Increased sunspot activity.", +"We already sent around a notice about that.", +"It's union rules. There's nothing we can do about it. Sorry.", +"Interference from the Van Allen Belt.", +"Jupiter is aligned with Mars.", +"Redundant ACLs. ", +"Mail server hit by UniSpammer.", +"T-1's congested due to porn traffic to the news server.", +"Data for intranet got routed through the extranet and landed on the internet.", +"We are a 100% Microsoft Shop.", +"We are Microsoft. What you are experiencing is not a problem; it is an undocumented feature.", +"Sales staff sold a product we don't offer.", +"Secretary sent chain letter to all 5000 employees.", +"Sysadmin didn't hear pager go off due to loud music from bar-room speakers.", +"Sysadmin accidentally destroyed pager with a large hammer.", +"Sysadmins unavailable because they are in a meeting talking about why they are unavailable so much.", +"Bad cafeteria food landed all the sysadmins in the hospital.", +"Route flapping at the NAP.", +"Computers under water due to SYN flooding.", +"The vulcan-death-grip ping has been applied.", +"Electrical conduits in machine room are melting.", +"Traffic jam on the Information Superhighway.", +"Radial Telemetry Infiltration", +"Cow-tippers tipped a cow onto the server.", +"tachyon emissions overloading the system", +"Maintenance window broken", +"We're out of slots on the server", +"Computer room being moved. Our systems are down for the weekend.", +"Sysadmins busy fighting SPAM.", +"Repeated reboots of the system failed to solve problem", +"Feature was not beta tested", +"Domain controller not responding", +"Someone else stole your IP address, call the Internet detectives!", +"It's not RFC-822 compliant.", +"operation failed because: there is no message for this error (#1014)", +"stop bit received", +"internet is needed to catch the etherbunny", +"network down, IP packets delivered via UPS", +"Firmware update in the coffee machine", +"Temporal anomaly", +"Mouse has out-of-cheese-error", +"Borg implants are failing", +"Borg nanites have infested the server", +"error: one bad user found in front of screen", +"Please state the nature of the technical emergency", +"Internet shut down due to maintenance", +"Daemon escaped from pentagram", +"crop circles in the corn shell", +"sticky bit has come loose", +"Hot Java has gone cold", +"Cache miss - please take better aim next time", +"Hash table has woodworm", +"Trojan horse ran out of hay", +"Zombie processes detected, machine is haunted.", +"overflow error in /dev/null", +"Browser's cookie is corrupted -- someone's been nibbling on it.", +"Mailer-daemon is busy burning your message in hell.", +"According to Microsoft, it's by design", +"vi needs to be upgraded to vii", +"greenpeace free'd the mallocs", +"Terrorists crashed an airplane into the server room, have to remove /bin/laden. (rm -rf /bin/laden)", +"astropneumatic oscillations in the water-cooling", +"Somebody ran the operating system through a spelling checker.", +"Rhythmic variations in the voltage reaching the power supply.", +"Keyboard Actuator Failure. Order and Replace." +] + + def help(plugin, topic="") + "excuse => supply a random excuse" + end + def privmsg(m) + excuse = @@excuses[rand(@@excuses.length)] + m.reply excuse + end +end + +plugin = ExcusePlugin.new +plugin.register("excuse") + diff --git a/lib/rbot/plugins/fish.rb b/lib/rbot/plugins/fish.rb new file mode 100644 index 00000000..57aaafc2 --- /dev/null +++ b/lib/rbot/plugins/fish.rb @@ -0,0 +1,61 @@ +require 'net/http' +require 'uri/common' +Net::HTTP.version_1_2 + +class BabelPlugin < Plugin + def help(plugin, topic="") + "translate to => translate from english to , translate from => translate to english from , translate => translate from to . Languages: en, fr, de, it, pt, es, nl" + end + def translate(m, params) + langs = ["en", "fr", "de", "it", "pt", "es", "nl"] + trans_from = params[:fromlang] ? params[:fromlang] : 'en' + trans_to = params[:tolang] ? params[:tolang] : 'en' + trans_text = params[:phrase].to_s + + query = "/babelfish/tr" + lang_match = langs.join("|") + unless(trans_from =~ /^(#{lang_match})$/ && trans_to =~ /^(#{lang_match})$/) + m.reply "invalid language: valid languagess are: #{langs.join(' ')}" + return + end + + data_text = URI.escape trans_text + trans_pair = "#{trans_from}_#{trans_to}" + data = "lp=#{trans_pair}&doit=done&intl=1&tt=urltext&urltext=#{data_text}" + + # check cache for previous lookups + if @registry.has_key?("#{trans_pair}/#{data_text}") + m.reply @registry["#{trans_pair}/#{data_text}"] + return + end + + http = @bot.httputil.get_proxy(URI.parse("http://babelfish.altavista.com")) + + http.start {|http| + resp = http.post(query, data, {"content-type", + "application/x-www-form-urlencoded"}) + + if (resp.code == "200") + resp.body.each_line do |l| + if(l =~ /^\s+
(.*)<\/div>/) + answer = $1 + # cache the answer + if(answer.length > 0) + @registry["#{trans_pair}/#{data_text}"] = answer + end + m.reply answer + return + end + end + m.reply "couldn't parse babelfish response html :(" + else + m.reply "couldn't talk to babelfish :(" + end + } + end +end +plugin = BabelPlugin.new +plugin.map 'translate to :tolang *phrase' +plugin.map 'translate from :fromlang *phrase' +plugin.map 'translate :fromlang :tolang *phrase' + diff --git a/lib/rbot/plugins/fortune.rb b/lib/rbot/plugins/fortune.rb new file mode 100644 index 00000000..184b6b13 --- /dev/null +++ b/lib/rbot/plugins/fortune.rb @@ -0,0 +1,22 @@ +class FortunePlugin < Plugin + def help(plugin, topic="") + "fortune [] => get a (short) fortune, optionally specifying fortune db" + end + def fortune(m, params) + db = params[:db] + fortune = nil + ["/usr/games/fortune", "/usr/bin/fortune", "/usr/local/bin/fortune"].each {|f| + if FileTest.executable? f + fortune = f + break + end + } + m.reply "fortune binary not found" unless fortune + ret = Utils.safe_exec(fortune, "-n", "255", "-s", db) + m.reply ret.gsub(/\t/, " ").split(/\n/).join(" ") + return + end +end +plugin = FortunePlugin.new +plugin.map 'fortune :db', :defaults => {:db => 'fortunes'}, + :requirements => {:db => /^[^-][\w-]+$/} diff --git a/lib/rbot/plugins/freshmeat.rb b/lib/rbot/plugins/freshmeat.rb new file mode 100644 index 00000000..20fa7248 --- /dev/null +++ b/lib/rbot/plugins/freshmeat.rb @@ -0,0 +1,98 @@ +require 'rexml/document' +require 'uri/common' + +class FreshmeatPlugin < Plugin + include REXML + def help(plugin, topic="") + "freshmeat search [=4] => search freshmeat for , freshmeat [=4] => return up to freshmeat headlines" + end + + def search_freshmeat(m, params) + max = params[:limit].to_i + search = params[:search].to_s + max = 8 if max > 8 + begin + xml = @bot.httputil.get(URI.parse("http://freshmeat.net/search-xml/?orderby=locate_projectname_full_DESC&q=#{URI.escape(search)}")) + rescue URI::InvalidURIError, URI::BadURIError => e + m.reply "illegal search string #{search}" + return + end + unless xml + m.reply "search for #{search} failed" + return + end + doc = Document.new xml + unless doc + m.reply "search for #{search} failed" + return + end + matches = Array.new + max_width = 250 + title_width = 0 + url_width = 0 + done = 0 + doc.elements.each("*/match") {|e| + name = e.elements["projectname_short"].text + url = "http://freshmeat.net/projects/#{name}/" + desc = e.elements["desc_short"].text + title = e.elements["projectname_full"].text + #title_width = title.length if title.length > title_width + url_width = url.length if url.length > url_width + matches << [title, url, desc] + done += 1 + break if done >= max + } + if matches.length == 0 + m.reply "not found: #{search}" + end + matches.each {|mat| + title = mat[0] + url = mat[1] + desc = mat[2] + desc.gsub!(/(.{#{max_width - 3 - url_width}}).*/, '\1..') + reply = sprintf("%s | %s", url.ljust(url_width), desc) + m.reply reply + } + end + + def freshmeat(m, params) + max = params[:limit].to_i + max = 8 if max > 8 + xml = @bot.httputil.get(URI.parse("http://images.feedstermedia.com/feedcache/ostg/freshmeat/fm-releases-global.xml")) + unless xml + m.reply "freshmeat news parse failed" + return + end + doc = Document.new xml + unless doc + m.reply "freshmeat news parse failed" + return + end + matches = Array.new + max_width = 60 + title_width = 0 + done = 0 + doc.elements.each("*/channel/item") {|e| + desc = e.elements["description"].text + title = e.elements["title"].text + #title.gsub!(/\s+\(.*\)\s*$/, "") + title.strip! + title_width = title.length if title.length > title_width + matches << [title, desc] + done += 1 + break if done >= max + } + matches.each {|mat| + title = mat[0] + #desc = mat[1] + #desc.gsub!(/(.{#{max_width - 3 - title_width}}).*/, '\1..') + #reply = sprintf("%#{title_width}s | %s", title, desc) + m.reply title + } + end +end +plugin = FreshmeatPlugin.new +plugin.map 'freshmeat search :limit *search', :action => 'search_freshmeat', + :defaults => {:limit => 4}, :requirements => {:limit => /^\d+$/} +plugin.map 'freshmeat :limit', :defaults => {:limit => 4}, + :requirements => {:limit => /^\d+$/} diff --git a/lib/rbot/plugins/google.rb b/lib/rbot/plugins/google.rb new file mode 100644 index 00000000..cd96f23c --- /dev/null +++ b/lib/rbot/plugins/google.rb @@ -0,0 +1,51 @@ +require 'net/http' +require 'uri/common' + +Net::HTTP.version_1_2 + +class GooglePlugin < Plugin + def help(plugin, topic="") + "search => search google for " + end + def privmsg(m) + unless(m.params && m.params.length > 0) + m.reply "incorrect usage: " + help(m.plugin) + return + end + searchfor = URI.escape m.params + + query = "/search?q=#{searchfor}&btnI=I%27m%20feeling%20lucky" + result = "not found!" + + proxy_host = nil + proxy_port = nil + + if(ENV['http_proxy']) + if(ENV['http_proxy'] =~ /^http:\/\/(.+):(\d+)$/) + proxy_host = $1 + proxy_port = $2 + end + end + + http = @bot.httputil.get_proxy(URI.parse("http://www.google.com")) + + begin + http.start {|http| + resp = http.get(query) + if resp.code == "302" + result = resp['location'] + end + } + rescue => e + p e + if e.response && e.response['location'] + result = e.response['location'] + else + result = "error!" + end + end + m.reply "#{m.params}: #{result}" + end +end +plugin = GooglePlugin.new +plugin.register("search") diff --git a/lib/rbot/plugins/host.rb b/lib/rbot/plugins/host.rb new file mode 100644 index 00000000..ef8dc8bc --- /dev/null +++ b/lib/rbot/plugins/host.rb @@ -0,0 +1,14 @@ +class HostPlugin < Plugin + def help(plugin, topic="") + "host => query nameserver about domain names and zones for " + end + def privmsg(m) + unless(m.params =~ /^(\w|-|\.)+$/) + m.reply "incorrect usage: " + help(m.plugin) + return + end + m.reply Utils.safe_exec("host", m.params) + end +end +plugin = HostPlugin.new +plugin.register("host") diff --git a/lib/rbot/plugins/httpd.rb b/lib/rbot/plugins/httpd.rb new file mode 100644 index 00000000..92fe3a80 --- /dev/null +++ b/lib/rbot/plugins/httpd.rb @@ -0,0 +1,35 @@ +require 'webrick' + +class HttpPlugin < Plugin + include WEBrick + + + def initialize + super + @http_server = HTTPServer.new( + :Port => 5555 + ) + @http_server.mount_proc("/") { |req, resp| + resp['content-type'] = 'text/html' + resp.body = "rbot httpd plugin" + resp.body += "#{@bot.status}
" + resp.body += "hello from rbot." + resp.body += "" + raise HTTPStatus::OK + } + Thread.new { + @http_server.start + } + end + def cleanup + @http_server.shutdown + end + def help(plugin, topic="") + "no help yet" + end + def privmsg(m) + end +end + +plugin = HttpPlugin.new +plugin.register("http") diff --git a/lib/rbot/plugins/insult.rb b/lib/rbot/plugins/insult.rb new file mode 100644 index 00000000..5f0384e8 --- /dev/null +++ b/lib/rbot/plugins/insult.rb @@ -0,0 +1,258 @@ +class InsultPlugin < Plugin + +## insults courtesy of http://insulthost.colorado.edu/ + +## +# Adjectives +## +@@adj = [ +"acidic", +"antique", +"contemptible", +"culturally-unsound", +"despicable", +"evil", +"fermented", +"festering", +"foul", +"fulminating", +"humid", +"impure", +"inept", +"inferior", +"industrial", +"left-over", +"low-quality", +"malodorous", +"off-color", +"penguin-molesting", +"petrified", +"pointy-nosed", +"salty", +"sausage-snorfling", +"tastless", +"tempestuous", +"tepid", +"tofu-nibbling", +"unintelligent", +"unoriginal", +"uninspiring", +"weasel-smelling", +"wretched", +"spam-sucking", +"egg-sucking", +"decayed", +"halfbaked", +"infected", +"squishy", +"porous", +"pickled", +"coughed-up", +"thick", +"vapid", +"hacked-up", +"unmuzzled", +"bawdy", +"vain", +"lumpish", +"churlish", +"fobbing", +"rank", +"craven", +"puking", +"jarring", +"fly-bitten", +"pox-marked", +"fen-sucked", +"spongy", +"droning", +"gleeking", +"warped", +"currish", +"milk-livered", +"surly", +"mammering", +"ill-borne", +"beef-witted", +"tickle-brained", +"half-faced", +"headless", +"wayward", +"rump-fed", +"onion-eyed", +"beslubbering", +"villainous", +"lewd-minded", +"cockered", +"full-gorged", +"rude-snouted", +"crook-pated", +"pribbling", +"dread-bolted", +"fool-born", +"puny", +"fawning", +"sheep-biting", +"dankish", +"goatish", +"weather-bitten", +"knotty-pated", +"malt-wormy", +"saucyspleened", +"motley-mind", +"it-fowling", +"vassal-willed", +"loggerheaded", +"clapper-clawed", +"frothy", +"ruttish", +"clouted", +"common-kissing", +"pignutted", +"folly-fallen", +"plume-plucked", +"flap-mouthed", +"swag-bellied", +"dizzy-eyed", +"gorbellied", +"weedy", +"reeky", +"measled", +"spur-galled", +"mangled", +"impertinent", +"bootless", +"toad-spotted", +"hasty-witted", +"horn-beat", +"yeasty", +"boil-brained", +"tottering", +"hedge-born", +"hugger-muggered", +"elf-skinned", +] + +## +# Amounts +## +@@amt = [ +"accumulation", +"bucket", +"coagulation", +"enema-bucketful", +"gob", +"half-mouthful", +"heap", +"mass", +"mound", +"petrification", +"pile", +"puddle", +"stack", +"thimbleful", +"tongueful", +"ooze", +"quart", +"bag", +"plate", +"ass-full", +"assload", +] + +## +# Objects +## +@@noun = [ +"bat toenails", +"bug spit", +"cat hair", +"chicken piss", +"dog vomit", +"dung", +"fat-woman's stomach-bile", +"fish heads", +"guano", +"gunk", +"pond scum", +"rat retch", +"red dye number-9", +"Sun IPC manuals", +"waffle-house grits", +"yoo-hoo", +"dog balls", +"seagull puke", +"cat bladders", +"pus", +"urine samples", +"squirrel guts", +"snake assholes", +"snake bait", +"buzzard gizzards", +"cat-hair-balls", +"rat-farts", +"pods", +"armadillo snouts", +"entrails", +"snake snot", +"eel ooze", +"slurpee-backwash", +"toxic waste", +"Stimpy-drool", +"poopy", +"poop", +"craptacular carpet droppings", +"jizzum", +"cold sores", +"anal warts", +] + + def help(plugin, topic="") + if(plugin == "insult") + return "insult me| => insult you or " + elsif(plugin == "msginsult") + return "msginsult => insult via /msg" + else + return "insult module topics: msginsult, insult" + end + end + def privmsg(m) + suffix="" + unless(m.params) + m.reply "incorrect usage: " + help(m.plugin) + return + end + msgto = m.channel + if(m.plugin =~ /^msginsult$/) + prefix = "you are " + if (m.params =~ /^#/) + prefix += "all " + end + msgto = m.params + suffix = " (from #{m.sourcenick})" + elsif(m.params =~ /^me$/) + prefix = "you are " + else + prefix = "#{m.params} is " + end + insult = generate_insult + @bot.say msgto, prefix + insult + suffix + end + def generate_insult + adj = @@adj[rand(@@adj.length)] + adj2 = "" + loop do + adj2 = @@adj[rand(@@adj.length)] + break if adj2 != adj + end + amt = @@amt[rand(@@amt.length)] + noun = @@noun[rand(@@noun.length)] + start = "a " + start = "an " if ['a','e','i','o','u'].include?(adj[0].chr) + "#{start}#{adj} #{amt} of #{adj2} #{noun}" + end +end +plugin = InsultPlugin.new +plugin.register("insult") +plugin.register("msginsult") + diff --git a/lib/rbot/plugins/karma.rb b/lib/rbot/plugins/karma.rb new file mode 100644 index 00000000..148427a5 --- /dev/null +++ b/lib/rbot/plugins/karma.rb @@ -0,0 +1,85 @@ +class KarmaPlugin < Plugin + def initialize + super + + # this plugin only wants to store ints! + class << @registry + def store(val) + val.to_i + end + def restore(val) + val.to_i + end + end + @registry.set_default(0) + + # import if old file format found + if(File.exist?("#{@bot.botclass}/karma.rbot")) + puts "importing old karma data" + IO.foreach("#{@bot.botclass}/karma.rbot") do |line| + if(line =~ /^(\S+)<=>([\d-]+)$/) + item = $1 + karma = $2.to_i + @registry[item] = karma + end + end + File.delete("#{@bot.botclass}/karma.rbot") + 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: ++/-- => increase/decrease karma for , karma for ? => show karma for , 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) + 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 + end + end + end +end +plugin = KarmaPlugin.new +plugin.map 'karmastats', :action => 'stats' +plugin.map 'karma :key', :defaults => {:key => false} +plugin.map 'karma for :key' diff --git a/lib/rbot/plugins/lart.rb b/lib/rbot/plugins/lart.rb new file mode 100644 index 00000000..de767197 --- /dev/null +++ b/lib/rbot/plugins/lart.rb @@ -0,0 +1,181 @@ +# Author: Michael Brailsford +# aka brailsmt +# Purpose: Provide for humorous larts and praises +# Copyright: 2002 Michael Brailsford. All rights reserved. +# License: This plugin is licensed under the BSD license. The terms of +# which follow. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +class LartPlugin < Plugin + + # Keep a 1:1 relation between commands and handlers + @@handlers = { + "lart" => "handle_lart", + "praise" => "handle_praise", + "addlart" => "handle_addlart", + "rmlart" => "handle_rmlart", + "addpraise" => "handle_addpraise", + "rmpraise" => "handle_rmpraise" + } + + def name + "lart" + end + + #{{{ + def initialize + super + @larts = Array.new + @praises = Array.new + #read in the lart and praise files + if File.exists? "#{@bot.botclass}/lart/larts" + IO.foreach("#{@bot.botclass}/lart/larts") { |line| + @larts << line.chomp + } + end + if File.exists? "#{@bot.botclass}/lart/praises" + IO.foreach("#{@bot.botclass}/lart/praises") { |line| + @praises << line.chomp + } + end + end + #}}} + #{{{ + def cleanup + end + #}}} + #{{{ + def save + Dir.mkdir("#{@bot.botclass}/lart") if not FileTest.directory? "#{@bot.botclass}/lart" + File.open("#{@bot.botclass}/lart/larts", "w") { |file| + file.puts @larts + } + File.open("#{@bot.botclass}/lart/praises", "w") { |file| + file.puts @praises + } + end + #}}} + #{{{ + def privmsg(m) + if not m.params + m.reply "What a crazy fool! Did you mean |help stats?" + return + end + + meth = self.method(@@handlers[m.plugin]) + meth.call(m) if(@bot.auth.allow?(m.plugin, m.source, m.replyto)) + end + #}}} + #{{{ + def help(plugin, topic="") + "Lart: The lart plugin allows you to punish/praise someone in the channel. You can also add new punishments and new praises as well as delete them. For the curious, LART is an acronym for Luser Attitude Readjustment Tool.\nUsage: punish/lart -- punishes for . The reason is optional.\n praise -- praises for . The reason is optional.\n mod[lart|punish|praise] [add|remove] -- Add or remove a lart or praise." + end + #}}} + # The following are command handlers {{{ + #{{{ + def handle_lart(m) + for_idx = m.params =~ /\s+\bfor\b/ + if for_idx + nick = m.params[0, for_idx] + else + nick = m.params + end + lart = @larts[get_msg_idx(@larts.length)] + if lart == nil + m.reply "I dunno any larts" + return + end + if nick == @bot.nick + lart = replace_who lart, m.sourcenick + lart << " for trying to make me lart myself" + else + lart = replace_who lart, nick + lart << m.params[for_idx, m.params.length] if for_idx + end + + @bot.action m.replyto, lart + end + #}}} + #{{{ + def handle_praise(m) + for_idx = m.params =~ /\s+\bfor\b/ + if for_idx + nick = m.params[0, for_idx] + else + nick = m.params + end + praise = @praises[get_msg_idx(@praises.length)] + if not praise + m.reply "I dunno any praises" + return + end + + if nick == m.sourcenick + praise = @larts[get_msg_idx(@larts.length)] + praise = replace_who praise, nick + else + praise = replace_who praise, nick + praise << m.params.gsub(/#{nick}/, "") + end + + @bot.action m.replyto, praise + end + #}}} + #{{{ + def handle_addlart(m) + @larts << m.params + m.okay + end + #}}} + #{{{ + def handle_rmlart(m) + @larts.delete m.params + m.okay + end + #}}} + #{{{ + def handle_addpraise(m) + @praises << m.params + m.okay + end + #}}} + #{{{ + def handle_rmpraise(m) + @praises.delete m.params + m.okay + end + #}}} + #}}} + + # The following are utils for larts/praises {{{ + #{{{ + def replace_who(msg, nick) + msg.gsub(//i, "#{nick}") + end + #}}} + #{{{ + def get_msg_idx(max) + idx = rand(max) + end + #}}} + #}}} +end +plugin = LartPlugin.new +plugin.register("lart") +plugin.register("praise") + +plugin.register("addlart") +plugin.register("addpraise") + +plugin.register("rmlart") +plugin.register("rmpraise") diff --git a/lib/rbot/plugins/math.rb b/lib/rbot/plugins/math.rb new file mode 100644 index 00000000..4a207389 --- /dev/null +++ b/lib/rbot/plugins/math.rb @@ -0,0 +1,122 @@ +class MathPlugin < Plugin + @@digits = { + "first" => "1", + "second" => "2", + "third" => "3", + "fourth" => "4", + "fifth" => "5", + "sixth" => "6", + "seventh" => "7", + "eighth" => "8", + "ninth" => "9", + "tenth" => "10", + "one" => "1", + "two" => "2", + "three" => "3", + "four" => "4", + "five" => "5", + "six" => "6", + "seven" => "7", + "eight" => "8", + "nine" => "9", + "ten" => "10" + }; + + def help(plugin, topic="") + "math , evaluate mathematical expression" + end + def privmsg(m) + unless(m.params) + m.reply "incorrect usage: " + help(m.plugin) + return + end + + expr = m.params.dup + @@digits.each {|k,v| + expr.gsub!(/\b#{k}\b/, v) + } + + while expr =~ /(exp ([\w\d]+))/ + exp = $1 + val = Math.exp($2).to_s + expr.gsub!(/#{Regexp.escape exp}/, "+#{val}") + end + + while expr =~ /^\s*(dec2hex\s*(\d+))\s*\?*/ + exp = $1 + val = sprintf("%x", $2) + expr.gsub!(/#{Regexp.escape exp}/, "+#{val}") + end + + expr.gsub(/\be\b/, Math.exp(1).to_s) + + while expr =~ /(log\s*((\d+\.?\d*)|\d*\.?\d+))\s*/ + exp = $1 + res = $2 + + if res == 0 + val = "Infinity" + else + val = Math.log(res).to_s + end + + expr.gsub!(/#{Regexp.escape exp}/, "+#{val}") + end + + while expr =~ /(bin2dec ([01]+))/ + exp = $1 + val = join('', unpack('B*', pack('N', $2))) + val.gsub!(/^0+/, "") + expr.gsub!(/#{Regexp.escape exp}/, "+#{val}") + end + + expr.gsub!(/ to the power of /, " ** ") + expr.gsub!(/ to the /, " ** ") + expr.gsub!(/\btimes\b/, "*") + expr.gsub!(/\bdiv(ided by)? /, "/ ") + expr.gsub!(/\bover /, "/ ") + expr.gsub!(/\bsquared/, "**2 ") + expr.gsub!(/\bcubed/, "**3 ") + expr.gsub!(/\bto\s+(\d+)(r?st|nd|rd|th)?( power)?/, "**\1 ") + expr.gsub!(/\bpercent of/, "*0.01*") + expr.gsub!(/\bpercent/, "*0.01") + expr.gsub!(/\% of\b/, "*0.01*") + expr.gsub!(/\%/, "*0.01") + expr.gsub!(/\bsquare root of (\d+)/, "\1 ** 0.5 ") + expr.gsub!(/\bcubed? root of (\d+)/, "\1 **(1.0/3.0) ") + expr.gsub!(/ of /, " * ") + expr.gsub!(/(bit(-| )?)?xor(\'?e?d( with))?/, "^") + expr.gsub!(/(bit(-| )?)?or(\'?e?d( with))?/, "|") + expr.gsub!(/bit(-| )?and(\'?e?d( with))?/, "& ") + expr.gsub!(/(plus|and)/, "+") + + if (expr =~ /^\s*[-\d*+\s()\/^\.\|\&\*\!]+\s*$/ && + expr !~ /^\s*\(?\d+\.?\d*\)?\s*$/ && + expr !~ /^\s*$/ && + expr !~ /^\s*[( )]+\s*$/) + + begin + debug "evaluating expression \"#{expr}\"" + answer = eval(expr) + if answer =~ /^[-+\de\.]+$/ + answer = sprintf("%1.12f", answer) + answer.gsub!(/\.?0+$/, "") + answer.gsub!(/(\.\d+)000\d+/, '\1') + if (answer.length > 30) + answer = "a number with >30 digits..." + end + end + m.reply answer + rescue Exception => e + puts "couldn't evaluate expression \"#{m.params}\": #{e}" + m.reply "illegal expression \"#{m.params}\"" + return + end + else + m.reply "illegal expression \"#{m.params}\"" + return + end + end +end +plugin = MathPlugin.new +plugin.register("math") diff --git a/lib/rbot/plugins/nickserv.rb b/lib/rbot/plugins/nickserv.rb new file mode 100644 index 00000000..1ef2baf7 --- /dev/null +++ b/lib/rbot/plugins/nickserv.rb @@ -0,0 +1,99 @@ +# automatically lookup nicks in @registry and identify when asked + +class NickServPlugin < Plugin + + def help(plugin, topic="") + case topic + when "" + return "nickserv plugin: handles nickserv protected IRC nicks. topics password, register, identify, listnicks" + when "password" + return "nickserv password : remember the password for nick and use it to identify in future" + when "register" + return "nickserv register [ []]: register the current nick, choosing a random password unless is supplied - current nick must not already be registered for this to work. Also specify email if required by your services" + when "identify" + return "nickserv identify: identify with nickserv - shouldn't be needed - bot should identify with nickserv immediately on request - however this could be useful after splits or service disruptions, or when you just set the password for the current nick" + when "listnicks" + return "nickserv listnicks: lists nicknames and associated password the bot knows about - you will need config level auth access to do this one and it will reply by privmsg only" + end + end + + def initialize + super + # this plugin only wants to store strings! + class << @registry + def store(val) + val + end + def restore(val) + val + end + end + end + + def privmsg(m) + return unless m.params + + case m.params + when (/^password\s*(\S*)\s*(.*)$/) + nick = $1 + passwd = $2 + @registry[nick] = passwd + m.okay + when (/^register$/) + passwd = genpasswd + @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + @registry[@bot.nick] = passwd + m.okay + when (/^register\s*(\S*)\s*(.*)$/) + passwd = $1 + email = $2 + @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + " " + email + @registry[@bot.nick] = passwd + m.okay + when (/^register\s*(.*)\s*$/) + passwd = $1 + @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + @registry[@bot.nick] = passwd + m.okay + when (/^listnicks$/) + if @bot.auth.allow?("config", m.source, m.replyto) + if @registry.length > 0 + @registry.each {|k,v| + @bot.say m.sourcenick, "#{k} => #{v}" + } + else + m.reply "none known" + end + end + when (/^identify$/) + if @registry.has_key?(@bot.nick) + @bot.sendmsg "PRIVMSG", "NickServ", "IDENTIFY " + @registry[@bot.nick] + m.okay + else + m.reply "I dunno the nickserv password for the nickname #{@bot.nick} :(" + end + end + end + + def listen(m) + return unless(m.kind_of? NoticeMessage) + + if (m.sourcenick == "NickServ" && m.message =~ /This nickname is owned by someone else/) + puts "nickserv asked us to identify for nick #{@bot.nick}" + if @registry.has_key?(@bot.nick) + @bot.sendmsg "PRIVMSG", "NickServ", "IDENTIFY " + @registry[@bot.nick] + end + end + end + + def genpasswd + # generate a random password + passwd = "" + 8.times do + passwd += (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr + end + return passwd + end +end +plugin = NickServPlugin.new +plugin.register("nickserv") diff --git a/lib/rbot/plugins/nslookup.rb b/lib/rbot/plugins/nslookup.rb new file mode 100644 index 00000000..92da1ba7 --- /dev/null +++ b/lib/rbot/plugins/nslookup.rb @@ -0,0 +1,56 @@ +class DnsPlugin < Plugin + begin + require 'resolv-replace' + def gethostname(address) + Resolv.getname(address) + end + def getaddresses(name) + Resolv.getaddresses(name) + end + rescue LoadError + def gethostname(address) + Socket.gethostbyname(address).first + end + def getaddresses(name) + a = Socket.gethostbyname(name) + list = Socket.getaddrinfo(a[0], 'http') + addresses = Array.new + list.each {|line| + addresses << line[3] + } + addresses + end + end + + def help(plugin, topic="") + "nslookup|dns => show local resolution results for hostname or ip address" + end + def privmsg(m) + unless(m.params) + m.reply "incorrect usage: " + help(m.plugin) + return + end + Thread.new do + if(m.params =~ /^\d+\.\d+\.\d+\.\d+$/) + begin + a = gethostname(m.params) + m.reply m.params + ": " + a if a + rescue StandardError => err + m.reply "#{m.params}: not found" + end + elsif(m.params =~ /^\S+$/) + begin + a = getaddresses(m.params) + m.reply m.params + ": " + a.join(", ") + rescue StandardError => err + m.reply "#{m.params}: not found" + end + else + m.reply "incorrect usage: " + help(m.plugin) + end + end + end +end +plugin = DnsPlugin.new +plugin.register("nslookup") +plugin.register("dns") diff --git a/lib/rbot/plugins/opmeh.rb b/lib/rbot/plugins/opmeh.rb new file mode 100644 index 00000000..2776de60 --- /dev/null +++ b/lib/rbot/plugins/opmeh.rb @@ -0,0 +1,19 @@ +class OpMehPlugin < Plugin + + def help(plugin, topic="") + return "opmeh => grant user ops in " + end + + def privmsg(m) + if(m.params) + channel = m.params + else + channel = m.channel + end + target = m.sourcenick + @bot.sendq("MODE #{channel} +o #{target}") + m.okay + end +end +plugin = OpMehPlugin.new +plugin.register("opmeh") diff --git a/lib/rbot/plugins/quotes.rb b/lib/rbot/plugins/quotes.rb new file mode 100644 index 00000000..674a9ed6 --- /dev/null +++ b/lib/rbot/plugins/quotes.rb @@ -0,0 +1,321 @@ +Quote = Struct.new("Quote", "num", "date", "source", "quote") + +class QuotePlugin < Plugin + def initialize + super + @lists = Hash.new + Dir["#{@bot.botclass}/quotes/*"].each {|f| + channel = File.basename(f) + @lists[channel] = Array.new if(!@lists.has_key?(channel)) + IO.foreach(f) {|line| + if(line =~ /^(\d+) \| ([^|]+) \| (\S+) \| (.*)$/) + num = $1.to_i + @lists[channel][num] = Quote.new(num, $2, $3, $4) + end + } + } + end + def save + Dir.mkdir("#{@bot.botclass}/quotes") if(!FileTest.directory?("#{@bot.botclass}/quotes")) + @lists.each {|channel, quotes| + File.open("#{@bot.botclass}/quotes/#{channel}", "w") {|file| + quotes.compact.each {|q| + file.puts "#{q.num} | #{q.date} | #{q.source} | #{q.quote}" + } + } + } + end + def addquote(source, channel, quote) + @lists[channel] = Array.new if(!@lists.has_key?(channel)) + num = @lists[channel].length + @lists[channel][num] = Quote.new(num, Time.new, source, quote) + return num + end + def getquote(source, channel, num=nil) + return nil unless(@lists.has_key?(channel)) + return nil unless(@lists[channel].length > 0) + if(num) + if(@lists[channel][num]) + return @lists[channel][num], @lists[channel].length - 1 + end + else + # random quote + return @lists[channel].compact[rand(@lists[channel].nitems)], + @lists[channel].length - 1 + end + end + def delquote(channel, num) + return false unless(@lists.has_key?(channel)) + return false unless(@lists[channel].length > 0) + if(@lists[channel][num]) + @lists[channel][num] = nil + return true + end + return false + end + def countquote(source, channel=nil, regexp=nil) + unless(channel) + total=0 + @lists.each_value {|l| + total += l.compact.length + } + return total + end + return 0 unless(@lists.has_key?(channel)) + return 0 unless(@lists[channel].length > 0) + if(regexp) + matches = @lists[channel].compact.find_all {|a| a.quote =~ /#{regexp}/i } + else + matches = @lists[channel].compact + end + return matches.length + end + def searchquote(source, channel, regexp) + return nil unless(@lists.has_key?(channel)) + return nil unless(@lists[channel].length > 0) + matches = @lists[channel].compact.find_all {|a| a.quote =~ /#{regexp}/i } + if(matches.length > 0) + return matches[rand(matches.length)], @lists[channel].length - 1 + else + return nil + end + end + def help(plugin, topic="") + case topic + when "addquote" + return "addquote [] => Add quote for channel . You only need to supply if you are addressing #{@bot.nick} privately. Responds to !addquote without addressing if so configured" + when "delquote" + return "delquote [] => delete quote from with number . You only need to supply if you are addressing #{@bot.nick} privately. Responds to !delquote without addressing if so configured" + when "getquote" + return "getquote [] [] => get quote from with number . You only need to supply if you are addressing #{@bot.nick} privately. Without , a random quote will be returned. Responds to !getquote without addressing if so configured" + when "searchquote" + return "searchquote [] => search for quote from that matches . You only need to supply if you are addressing #{@bot.nick} privately. Responds to !searchquote without addressing if so configured" + when "topicquote" + return "topicquote [] [] => set topic to quote from with number . You only need to supply if you are addressing #{@bot.nick} privately. Without , a random quote will be set. Responds to !topicquote without addressing if so configured" + when "countquote" + return "countquote [] => count quotes from that match . You only need to supply if you are addressing #{@bot.nick} privately. Responds to !countquote without addressing if so configured" + when "whoquote" + return "whoquote [] => show who added quote . You only need to supply if you are addressing #{@bot.nick} privately" + when "whenquote" + return "whenquote [] => show when quote was added. You only need to supply if you are addressing #{@bot.nick} privately" + else + return "Quote module (Quote storage and retrieval) topics: addquote, getquote, searchquote, topicquote, countquote, whoquote, whenquote" + end + end + def listen(m) + return unless(m.kind_of? PrivMessage) + + command = m.message.dup + if(m.address? && m.private?) + case command + when (/^addquote\s+(#\S+)\s+(.*)/) + channel = $1 + quote = $2 + if(@bot.auth.allow?("addquote", m.source, m.replyto)) + if(channel =~ /^#/) + num = addquote(m.source, channel, quote) + m.reply "added the quote (##{num})" + end + end + when (/^getquote\s+(#\S+)$/) + channel = $1 + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = getquote(m.source, channel) + if(quote) + m.reply "[#{quote.num}] #{quote.quote}" + else + m.reply "quote not found!" + end + end + when (/^getquote\s+(#\S+)\s+(\d+)$/) + channel = $1 + num = $2.to_i + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = getquote(m.source, channel, num) + if(quote) + m.reply "[#{quote.num}] #{quote.quote}" + else + m.reply "quote not found!" + end + end + when (/^whoquote\s+(#\S+)\s+(\d+)$/) + channel = $1 + num = $2.to_i + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = getquote(m.source, channel, num) + if(quote) + m.reply "quote #{quote.num} added by #{quote.source}" + else + m.reply "quote not found!" + end + end + when (/^whenquote\s+(#\S+)\s+(\d+)$/) + channel = $1 + num = $2.to_i + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = getquote(m.source, channel, num) + if(quote) + m.reply "quote #{quote.num} added on #{quote.date}" + else + m.reply "quote not found!" + end + end + when (/^topicquote\s+(#\S+)$/) + channel = $1 + if(@bot.auth.allow?("topicquote", m.source, m.replyto)) + quote, total = getquote(m.source, channel) + if(quote) + @bot.topic channel, "[#{quote.num}] #{quote.quote}" + else + m.reply "quote not found!" + end + end + when (/^topicquote\s+(#\S+)\s+(\d+)$/) + channel = $1 + num = $2.to_i + if(@bot.auth.allow?("topicquote", m.source, m.replyto)) + quote, total = getquote(m.source, channel, num) + if(quote) + @bot.topic channel, "[#{quote.num}] #{quote.quote}" + else + m.reply "quote not found!" + end + end + when (/^delquote\s+(#\S+)\s+(\d+)$/) + channel = $1 + num = $2.to_i + if(@bot.auth.allow?("delquote", m.source, m.replyto)) + if(delquote(channel, num)) + m.okay + else + m.reply "quote not found!" + end + end + when (/^searchquote\s+(#\S+)\s+(.*)$/) + channel = $1 + reg = $2 + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = searchquote(m.source, channel, reg) + if(quote) + m.reply "[#{quote.num}] #{quote.quote}" + else + m.reply "quote not found!" + end + end + when (/^countquote$/) + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + total = countquote(m.source) + m.reply "#{total} quotes" + end + when (/^countquote\s+(#\S+)\s*(.*)$/) + channel = $1 + reg = $2 + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + total = countquote(m.source, channel, reg) + if(reg.length > 0) + m.reply "#{total} quotes match: #{reg}" + else + m.reply "#{total} quotes" + end + end + end + elsif (m.address? || (@bot.config["QUOTE_LISTEN"] && command.gsub!(/^!/, ""))) + case command + when (/^addquote\s+(.+)/) + if(@bot.auth.allow?("addquote", m.source, m.replyto)) + num = addquote(m.source, m.target, $1) + m.reply "added the quote (##{num})" + end + when (/^getquote$/) + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = getquote(m.source, m.target) + if(quote) + m.reply "[#{quote.num}] #{quote.quote}" + else + m.reply "no quotes found!" + end + end + when (/^getquote\s+(\d+)$/) + num = $1.to_i + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = getquote(m.source, m.target, num) + if(quote) + m.reply "[#{quote.num}] #{quote.quote}" + else + m.reply "quote not found!" + end + end + when (/^whenquote\s+(\d+)$/) + num = $1.to_i + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = getquote(m.source, m.target, num) + if(quote) + m.reply "quote #{quote.num} added on #{quote.date}" + else + m.reply "quote not found!" + end + end + when (/^whoquote\s+(\d+)$/) + num = $1.to_i + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = getquote(m.source, m.target, num) + if(quote) + m.reply "quote #{quote.num} added by #{quote.source}" + else + m.reply "quote not found!" + end + end + when (/^topicquote$/) + if(@bot.auth.allow?("topicquote", m.source, m.replyto)) + quote, total = getquote(m.source, m.target) + if(quote) + @bot.topic m.target, "[#{quote.num}] #{quote.quote}" + else + m.reply "no quotes found!" + end + end + when (/^topicquote\s+(\d+)$/) + num = $1.to_i + if(@bot.auth.allow?("topicquote", m.source, m.replyto)) + quote, total = getquote(m.source, m.target, num) + if(quote) + @bot.topic m.target, "[#{quote.num}] #{quote.quote}" + else + m.reply "quote not found!" + end + end + when (/^delquote\s+(\d+)$/) + num = $1.to_i + if(@bot.auth.allow?("delquote", m.source, m.replyto)) + if(delquote(m.target, num)) + m.okay + else + m.reply "quote not found!" + end + end + when (/^searchquote\s+(.*)$/) + reg = $1 + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + quote, total = searchquote(m.source, m.target, reg) + if(quote) + m.reply "[#{quote.num}] #{quote.quote}" + else + m.reply "quote not found!" + end + end + when (/^countquote(?:\s+(.*))?$/) + reg = $1 + if(@bot.auth.allow?("getquote", m.source, m.replyto)) + total = countquote(m.source, m.target, reg) + if(reg && reg.length > 0) + m.reply "#{total} quotes match: #{reg}" + else + m.reply "#{total} quotes" + end + end + end + end + end +end +plugin = QuotePlugin.new +plugin.register("quotes") diff --git a/lib/rbot/plugins/remind.rb b/lib/rbot/plugins/remind.rb new file mode 100644 index 00000000..5ad980ae --- /dev/null +++ b/lib/rbot/plugins/remind.rb @@ -0,0 +1,154 @@ +require 'rbot/utils' + +class RemindPlugin < Plugin + def initialize + super + @reminders = Hash.new + end + def cleanup + @reminders.each_value {|v| + v.each_value {|vv| + @bot.timer.remove(vv) + } + } + @reminders.clear + end + def help(plugin, topic="") + if(plugin =~ /^remind\+$/) + "see remind. remind+ can be used to remind someone else of something, using instead of 'me'. However this will generally require a higher auth level than remind." + else + "remind me [about] in