From 0f3e302547363ea237454dda891ddb5de1be4476 Mon Sep 17 00:00:00 2001 From: Tom Gilbert Date: Sat, 9 Oct 2004 01:51:05 +0000 Subject: [PATCH] initial import of rbot --- AUTHORS | 16 + COPYING | 21 + ChangeLog | 258 +++++++++ INSTALL | 8 + REQUIREMENTS | 41 ++ TODO | 8 + contrib/plugins/figlet.rb | 20 + contrib/plugins/ri.rb | 83 +++ contrib/plugins/stats.rb | 232 ++++++++ contrib/plugins/vandale.rb | 49 ++ docgen | 3 + rbot.rb | 60 ++ rbot/auth.rb | 199 +++++++ rbot/channel.rb | 50 ++ rbot/config.rb | 40 ++ rbot/dbhash.rb | 126 +++++ rbot/ircbot.rb | 736 +++++++++++++++++++++++++ rbot/ircsocket.rb | 186 +++++++ rbot/keywords.rb | 382 +++++++++++++ rbot/language.rb | 55 ++ rbot/languages/dutch.lang | 73 +++ rbot/languages/english.lang | 71 +++ rbot/message.rb | 237 ++++++++ rbot/plugins.rb | 242 +++++++++ rbot/plugins/autorejoin.rb | 14 + rbot/plugins/cal.rb | 14 + rbot/plugins/dice.rb | 81 +++ rbot/plugins/eightball.rb | 18 + rbot/plugins/excuse.rb | 470 ++++++++++++++++ rbot/plugins/fish.rb | 84 +++ rbot/plugins/fortune.rb | 24 + rbot/plugins/freshmeat.rb | 107 ++++ rbot/plugins/google.rb | 51 ++ rbot/plugins/host.rb | 14 + rbot/plugins/insult.rb | 258 +++++++++ rbot/plugins/karma.rb | 75 +++ rbot/plugins/lart.rb | 177 ++++++ rbot/plugins/math.rb | 122 +++++ rbot/plugins/nickserv.rb | 92 ++++ rbot/plugins/nslookup.rb | 56 ++ rbot/plugins/opmeh.rb | 18 + rbot/plugins/quotes.rb | 321 +++++++++++ rbot/plugins/remind.rb | 154 ++++++ rbot/plugins/roshambo.rb | 54 ++ rbot/plugins/rot13.rb | 14 + rbot/plugins/roulette.rb | 147 +++++ rbot/plugins/seen.rb | 89 +++ rbot/plugins/slashdot.rb | 89 +++ rbot/plugins/spell.rb | 36 ++ rbot/plugins/tube.rb | 76 +++ rbot/plugins/url.rb | 98 ++++ rbot/plugins/weather.rb | 55 ++ rbot/plugins/wserver.rb | 94 ++++ rbot/registry.rb | 294 ++++++++++ rbot/rfc2812.rb | 1027 +++++++++++++++++++++++++++++++++++ rbot/timer.rb | 123 +++++ rbot/utils.rb | 778 ++++++++++++++++++++++++++ rbotconf/conf.rbot | 13 + rbotconf/keywords.rbot | 4 + rbotconf/lart/larts | 98 ++++ rbotconf/lart/praises | 2 + rbotconf/levels.rbot | 20 + rbotconf/users.rbot | 1 + 63 files changed, 8458 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 INSTALL create mode 100644 REQUIREMENTS create mode 100644 TODO create mode 100644 contrib/plugins/figlet.rb create mode 100644 contrib/plugins/ri.rb create mode 100644 contrib/plugins/stats.rb create mode 100644 contrib/plugins/vandale.rb create mode 100755 docgen create mode 100755 rbot.rb create mode 100644 rbot/auth.rb create mode 100644 rbot/channel.rb create mode 100644 rbot/config.rb create mode 100644 rbot/dbhash.rb create mode 100644 rbot/ircbot.rb create mode 100644 rbot/ircsocket.rb create mode 100644 rbot/keywords.rb create mode 100644 rbot/language.rb create mode 100644 rbot/languages/dutch.lang create mode 100644 rbot/languages/english.lang create mode 100644 rbot/message.rb create mode 100644 rbot/plugins.rb create mode 100644 rbot/plugins/autorejoin.rb create mode 100644 rbot/plugins/cal.rb create mode 100644 rbot/plugins/dice.rb create mode 100644 rbot/plugins/eightball.rb create mode 100644 rbot/plugins/excuse.rb create mode 100644 rbot/plugins/fish.rb create mode 100644 rbot/plugins/fortune.rb create mode 100644 rbot/plugins/freshmeat.rb create mode 100644 rbot/plugins/google.rb create mode 100644 rbot/plugins/host.rb create mode 100644 rbot/plugins/insult.rb create mode 100644 rbot/plugins/karma.rb create mode 100644 rbot/plugins/lart.rb create mode 100644 rbot/plugins/math.rb create mode 100644 rbot/plugins/nickserv.rb create mode 100644 rbot/plugins/nslookup.rb create mode 100644 rbot/plugins/opmeh.rb create mode 100644 rbot/plugins/quotes.rb create mode 100644 rbot/plugins/remind.rb create mode 100644 rbot/plugins/roshambo.rb create mode 100644 rbot/plugins/rot13.rb create mode 100644 rbot/plugins/roulette.rb create mode 100644 rbot/plugins/seen.rb create mode 100644 rbot/plugins/slashdot.rb create mode 100644 rbot/plugins/spell.rb create mode 100644 rbot/plugins/tube.rb create mode 100644 rbot/plugins/url.rb create mode 100644 rbot/plugins/weather.rb create mode 100644 rbot/plugins/wserver.rb create mode 100644 rbot/registry.rb create mode 100644 rbot/rfc2812.rb create mode 100644 rbot/timer.rb create mode 100644 rbot/utils.rb create mode 100644 rbotconf/conf.rbot create mode 100644 rbotconf/keywords.rbot create mode 100644 rbotconf/lart/larts create mode 100644 rbotconf/lart/praises create mode 100644 rbotconf/levels.rbot create mode 100644 rbotconf/users.rbot diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..e045b6f8 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,16 @@ +Main Author +o Tom Gilbert (giblet) + +Patch contributors +o Peter Suschlik (pesu) + +Module contributors +o dice.rb - David Dorward (Dorward) +o lart.rb - Michael Brailsford +o stats.rb - Michael Brailsford +o ri.rb - Michael Brailsford + +"Testing" (having fun breaking rbot creatively) +o Paul Duncan (pabs) +o Richard Lowe (richlowe) + diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..7899637e --- /dev/null +++ b/COPYING @@ -0,0 +1,21 @@ +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. + diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..f59fe521 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,258 @@ +Fri Oct 08 00:40:07 BST 2004 Tom Gilbert + + * fixed insult plugin + * fixed excuse plugin + +Thu Oct 07 23:28:05 BST 2004 Tom Gilbert + + * searching for urls in the url plugin + * roshambo (rock/paper/scissors) plugin from Hans Fugal + +Sat Apr 17 20:56:50 BST 2004 Tom Gilbert + + * Oh, found new tv plugin in my inbox from ages ago, but it's still not + working so I guess it changed again since then + * New eightball plugin from Daniel Free + +Sat Apr 17 20:44:43 BST 2004 Tom Gilbert + + * Fixed the babelfish parser so translate works again. + * Misc other fixes + * Note some plugins are broken (excuse,insult) because the server they use + went away. I don't know of a replacement right now. + * tv plugin seems broken, perhaps the html changed. + +Thu Jan 15 21:37:38 GMT 2004 Tom Gilbert + + * Fixes for ruby 1.8 + +0.9.8 +* new plugin from Alan Third , allows you to search and list + UK TV programmes. + +0.9.7 +* various plugin updates +* fix (again) for C to F temp conversion in weather plugin +* channel topic patch from Peter Suschlik, gives plugin better access to topic + changes and related information + +0.9.6 +* changes to layout of slashdot plugin output +* freshmeat plugin, show latest updates or search +* fix to C to F temp conversion in weather plugin +* status command returns some bot status +* fortune plugin +* using BDB::Btree everywhere now, instead of BDB::Hash, because the Btree api + allows me to set my own key comparison function. This is needed to keep + supporting case insensitivity (vital for IRC), which was sadly broken in + 0.9.5 :( All existing dbs will be upgraded automatically. +* roulette plugin - play russian roulette :) also keeps game stats. +* new config option, NO_KEYWORD_ADDRESS. If set to "true", the bot will always + respond to keywords it knows even when not addressed and the message doesn't + start with '. Message must end with "?" however. +* hopefully fixed welcome message parsing from certain server types + +0.9.5 +* plugin object registry + This provides persistant storage for plugins via a hash interface. The + default mode is an object store, so you can store ruby objects and reference + them with hash keys. This is because the default store/restore methods of + the plugins' RegistryAccessor are calls to Marshal.dump and Marshal.restore, + for example: + blah = Hash.new + blah[:foo] = "fum" + @registry[:blah] = blah + then, even after the bot is shut down and disconnected, on the next run you + can access the blah object as it was, with: + blah = @registry[:blah] + The registry can of course be used to store simple strings, fixnums, etc as + well, and should be useful to store or cache plugin data or dynamic plugin + configuration. + + If you don't need to store objects, and strictly want a persistant hash of + strings, you can override the store/restore methods to suit your needs, for + example (in your plugin): + def initialize + class << @registry + def store(val) + val + end + def restore(val) + val + end + end + end + Your plugins section of the registry is private, it has its own namespace + (derived from the plugin's class name, so change it and lose your data). + Calls to registry.each etc, will only iterate over your namespace. + + The nickserv and karma plugins use the new registry and should serve as a + useful example. Basic usage of the registry is simple, just treat it as a + hash, with values that never die (unless you delete() them). +* Change to the nickserv plugin. The old method of putting the nickserv + password in rbot.conf was useless for multiple nicks or easy updates. The + plugin now uses the plugin registry to store passwords for any nicks it + owns. The plugin can be told to register the current nick (supply a password + or it'll generate one), identify for the current nick (if the password is + known), and can be told the passwords for other nicks. If NickServ asks the + bot to identify, it will automatically do so if it knows the appropriate + password. +* karma plugin now uses the plugin registry, it should automatically import + your existing, stored karma data into the registry. +* The babelfish plugin now caches results in the bot registry to speed up + common lookups. +* New message types and plugin methods to grab them, + 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 +* A plugin's listen() method will now receive any kind of Message, e.g. + PrivMessage, NoticeMessage, NickMessage, JoinMessage, etc +* New plugins: + seen: the usual "seen" stuff: + rbot: seen giblet? + giblet was last seen xxx ago doing xxx + cal: calls the unix cal program to display a calendar + math: evaluates mathematical expressions: + rbot: math 2+2 + rbot: math 4 to the power of 8 + rbot: math ((232+432) - 4) / 2 + (ported from infobot. Thanks to Kevin Lenzo, who wrote the + original infobot math module) + slashdot: displays latest headlines or searches for articles + url: stores urls mentioned in channels for regurgitation later + weather: grabs and parses METAR weather data, will remember the last weather + code you asked for so you don't have to :) +* New utility function, Util.http_get(url) for getting remote data via http, + just dumps response.body into a string and returns it, or nil if anything at + all goes wrong. Useful for simple plugins. +* random quit messages if none specified, messages set in language description + file +* keywords are now stored in bdb databases - your old keywords.rbot will be + imported. Static keywords (fact packs) are also stored in bdb databases, and + rbot will automatically convert any text .fact file dropped in the confdir's + keywords subdirectory, at startup, into a bdb file. If both a db and a text + file exist of the same name (except the extension), the text file will be + imported and merged into the database. + static keywords will be looked up in each factpack db in turn, in + alphabetical filename order - so you can prioritise using the filename if + you wish. +* fixed a bug with autsplitting long sent lines, the last line was often being + split unnecessarily. + +0.9.4 +* Massive cleanup of rfc2812.rb, contributed by Lars Christensen + , gets rid of a lot of regexps +* Fixed bug reading static keyword files - "foo <=is=> bar" may not have + worked for a couple of releases, only "foo<=is=>bar" was working - this was + not intended and should be fixed now +* Experimental send queue, to prevent the bot from flooding out, the delay + between sending messages to the server defaults to 2s, but is configurable + in conf.rbot, set SENDQ_DELAY (0 to disable queueing). You can also set/get + the value from the bot, "rbot: options get sendq_delay", and + "rbot: options set sendq_delay 1.5", if you have sufficient auth for "config" + This is a bursting sendq, most ircd's allow bursts of up to 5 + lines, with non-burst limits of 512 bytes/2 seconds. To set the burst limit, + configure SENDQ_BURST in conf.rbot, or do the same kind of stuff with + "rbot: options set sendq_burst 2", etc. + The defaults are 2s/4 burst, which seem to work okay for me. +* support for multiple, customisable, addressing prefixes. Set ADDRESS_PREFIX + in conf.rbot to a space separate list of addressing prefixes, e.g + ADDRESS_PREFIX = | ! => + Would mean that all of the following in channel messages would cause the bot + to respond: + rbot: version + |version + !version + =>version +* bb plugin removed, bb is nearly over and it doesn't work 100% anyway +* Two plugins from brailsmt (from #ruby-lang on openprojects), a stats plugin + which monitors usage of 1-word sentences, and lart, which allows you to ask + rbot to lart people - with an optional reason - larts are user-definable and + can be added on the fly. +* made google.rb work for people with 1.6 ruby's net/http + +0.9.3 + +* fix quit messages +* new plugin for handling nickserv-protected nicks, use NICKSERV_PASSWORD in + the config file. +* fixes to a few other buglets +* new plugin to grab bigbrother headlines, still buggy and only useful for UK + folks who love bb :-) +* fixes to various plugins +* Patch from akira yamada + DNS plugin: Use resolv-replace if found, do lookup in new thread + Fix bug joining channels with keys + +0.9.2 + +* better "connect failure" error message +* better option parsing, and --debug option +* access to bot's online help via commandline, eg: + ./rbot.rb --help + ./rbot.rb --help core + ./rbot.rb --help "core save" +* Fix broken help from last point release +* Plugin API modification and cleanup. You no longer need to set @listen to + true in order to get all NOTICE and PRIVMSGs, you just need to define the + method. The method is now called listen(), renamed from listener(). This + should be the last time the plugin api is changed incompatibly. +* New plugin method kick(). Use it to see kicks (duh :)) +* New plugin methods join(), part(). Obvious uses. +* Example plugin autorejoin.rb, uses kick event to rejoin channel and insult + kicker +* fix bug in remind plugins "remind me no more" recognition. + +0.9.1 + +* Fix welcome message recognition for certain IRCd's. + +0.9 + +* Allow keyword definitions which end in '?', like this: + bot: foo is bar\? +* rdoc documentation! +* fixed broken address regexp, "rbot: .foo" was being treated as an addressed + form of "foo" (lost the .) +* fix stupid bug in last release (looking for wrong default conf dir) + +0.8 + +* Tarball layout change. modules all in rbot/ now, and the rbot/ default + configuration moved to rbotconf/. This lets the thing run from an unpacked + tarball while also being ready to run with the modules installed somewhere + else. +* change hashbang to /usr/bin/env ruby, in order to use PATH looking for ruby, + it's BSD friendly! +* allow "botnick : foo" style addressing, and even "botnick... foo" +* slap plugin (contributed by oct) +* renamed bot.send to bot.sendmsg, I didn't really want to override send() ;D + (thanks Kero) + +0.7.1 +* Made sane for packagers. Looks in the right places for plugins and language + files now, so extra effort shouldn't be needed there. + +0.7 + +* Fixed "nick taken on join" bug +* Dice plugin patch from David Dorward +* fix searchquote regexp +* conf.rbot: PASSWORD -> SERVER_PASSWORD, to prevent confusion with PASSWD, + which is for master auth. + +0.6 + +* Fixed addquote (was incrementing quote ID twice) +* now strips colour/bold escapes from incoming messages (rbot was ignoring + messages addressed using a bolded colon, for example). +* minor bugfixes +* more language breadth +* Addressing works better now +* Can autojoin channels with keys, conf.rbot line is: + autojoin_channels #chan1, #chan2, #chan3 key, #chan4 key, #chan5 +* dice plugin fixes + +0.5 + +* Initial release diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000..0ae1bdd1 --- /dev/null +++ b/INSTALL @@ -0,0 +1,8 @@ +Just stick the directory somewhere for now, edit rbotconf/conf.rbot and run +./rbot.rb. + +You can maintain multiple configurations at once, start rbot with the +location you want it to store runtime data in. By default, running ./rbot.rb +uses the rbotconf subdirectory which contains a sample configuration. If you +run ./rbot.rb ~/.rbot, then the configuration will be stored and read from +there instead. diff --git a/REQUIREMENTS b/REQUIREMENTS new file mode 100644 index 00000000..068c1b9a --- /dev/null +++ b/REQUIREMENTS @@ -0,0 +1,41 @@ +Ruby modules needed for rbot +============================ + +Core requirements + bdb (berkley db) http://www.ruby-lang.org/en/raa-list.rhtml?name=bdb + (which requires libdb2 or better, from + www.sleepycat.com) + net/http 1.1+ + socket + uri + +Plugin requirements +(these are all optional, if you don't have them, the plugins just won't +function) + +babelfish, wserver: + net/http 1.2+ + + +excuse,insult: + net/telnet + +nslookup: + resolv-replace (optional) + +slashdot: + REXML + +External programs needed for rbot +================================= + +Plugin requirements +(These are all optional) + +host plugin: + host(1) + +spell plugin: + ispell(1) + + diff --git a/TODO b/TODO new file mode 100644 index 00000000..6c9bae1f --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ +o runtime language changing +o maybe runtime language configuration? +o freeze/thaw factoids (make them readonly) at higher auth +o respond to insults +o feed back errors from plugins (optional) +o Allow users to leave messages to named users. +o wtf plugin (bsdgames) +o weather plugin C->F conversion is wrong? diff --git a/contrib/plugins/figlet.rb b/contrib/plugins/figlet.rb new file mode 100644 index 00000000..ce17fe71 --- /dev/null +++ b/contrib/plugins/figlet.rb @@ -0,0 +1,20 @@ +class FigletPlugin < Plugin + def help(plugin, topic="") + "figlet [] => print using figlet" + end + def privmsg(m) + case m.params + when nil + m.reply "incorrect usage: " + help(m.plugin) + return + when (/^-/) + m.reply "incorrect usage: " + help(m.plugin) + return + else + m.reply Utils.safe_exec("/usr/bin/figlet", "-k", "-f", "mini", m.params) + return + end + end +end +plugin = FigletPlugin.new +plugin.register("figlet") diff --git a/contrib/plugins/ri.rb b/contrib/plugins/ri.rb new file mode 100644 index 00000000..99292f1c --- /dev/null +++ b/contrib/plugins/ri.rb @@ -0,0 +1,83 @@ +# Author: Michael Brailsford +# aka brailsmt +# Purpose: To respond to requests for information from the ri command line +# utility. + +class RiPlugin < Plugin + + @@handlers = { + "ri" => "ri_handler", + "msgri" => "msgri_handler" + } + + #{{{ + def initialize + super + @cache = Hash.new + end + #}}} + #{{{ + def privmsg(m) + if not m.params + m.reply "uhmm... whatever" + return + end + + meth = self.method(@@handlers[m.plugin]) + meth.call(m) + end + #}}} + #{{{ + def cleanup + @cache = nil + end + #}}} + #{{{ + def ri_handler(m) + response = "" + if @cache[m.params] + response = @cache[m.params] + else + IO.popen("-") {|p| + if(p) + response = p.readlines.join "\n" + @cache[m.params] = response + else + $stderr = $stdout + exec("ri", m.params) + end + } + @cache[m.params] = response + end + + @bot.say m.sourcenick, response + m.reply "Finished \"ri #{m.params}\"" + end + #}}} + #{{{ + def msgri_handler(m) + response = "" + tell_nick, query = m.params.split() + if @cache[query] + response = @cache[query] + else + IO.popen("-") {|p| + if(p) + response = p.readlines.join "\n" + @cache[m.params] = response + else + $stderr = $stdout + exec("ri", query) + end + } + @cache[query] = response + end + + @bot.say tell_nick, response + m.reply "Finished telling #{tell_nick} about \"ri #{query}\"" + end + #}}} +end +plugin = RiPlugin.new +plugin.register("ri") +plugin.register("msgri") diff --git a/contrib/plugins/stats.rb b/contrib/plugins/stats.rb new file mode 100644 index 00000000..4cafbbe9 --- /dev/null +++ b/contrib/plugins/stats.rb @@ -0,0 +1,232 @@ +# Author: Michael Brailsford +# aka brailsmt +# Purpose: Provides the ability to track various tokens that are spoken in a +# channel. +# 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 StatsPlugin < Plugin + + @@commands = { + "stats" => "handle_stats", + "track" => "handle_track", + "untrack" => "handle_untrack", + "listtokens" => "handle_listtokens", + "rmabuser" => "handle_rmabuser" + } + + #{{{ + def initialize + super + @listen = true + @channels = Hash.new + #check to see if a stats token file already exists for this channel... + Dir["#{@bot.botclass}/stats/*"].each { |fname| + channel = File.basename fname + tokens = Hash.new + IO.foreach(fname) { |line| + if line =~ /^(\S+)\s*<=>(.*)/ + tokens[$1] = parse_token_stats $2 + end + } + @channels[channel] = tokens + } + end + #}}} + #{{{ + def cleanup + @channels = nil + end + #}}} + #{{{ + def help(plugin, topic="") + "Stats: The stats plugin tracks various tokens from users in the channel. The tokens are only tracked if it is the only thing on a line.\nUsage: stats -- lists the stats for \n [un]track -- Adds or deletes from the list of tokens\n listtokens -- lists the tokens that are currently being tracked" + end + #}}} + #{{{ + def privmsg(m) + if not m.params and not m.plugin =~ /listtokens/ + m.reply "What a crazy fool! Did you mean |help stats?" + return + end + + meth = self.method(@@commands[m.plugin]) + meth.call(m) + end + #}}} + #{{{ + def save + Dir.mkdir("#{@bot.botclass}/stats") if not FileTest.directory?("#{@bot.botclass}/stats") + #save the tokens to a file... + @channels.each_pair { |channel, tokens| + if not tokens.empty? + File.open("#{@bot.botclass}/stats/#{channel}", "w") { |f| + tokens.each { |token, datahash| + f.puts "#{token} <=> #{datahash_to_s(datahash)}" + } + } + else + File.delete "#{@bot.botclass}/stats/#{channel}" + end + } + end + #}}} + #{{{ + def listen(m) + if not m.private? + tokens = @channels[m.target] + if not @@commands[m.plugin] + tokens.each_pair { |key, hsh| + if not m.message.scan(/#{Regexp.escape(key)}/).empty? + if hsh[m.sourcenick] + hsh[m.sourcenick] += 1 + else + hsh[m.sourcenick] = 1 + end + end + } + end + end +#This is the old code {{{ +# if not m.private? +# tokens = @channels[m.target] +# hsh = tokens[m.message] +# if hsh +# if hsh[m.sourcenick] +# hsh[m.sourcenick] += 1 +# else +# hsh[m.sourcenick] = 1 +# end +# end +# end }}} + end + #}}} + #The following are helper functions for the plugin {{{ + def datahash_to_s(dhash) + rv = "" + dhash.each { |key, val| + rv << "#{key}:#{val} " + } + rv.chomp + end + + def parse_token_stats(stats) + rv = Hash.new + stats.split(" ").each { |nickstat| + nick, stat = nickstat.split ":" + rv[nick] = stat.to_i + } + rv + end + #}}} + #The following are handler methods for dealing with each command from IRC {{{ + #{{{ + def handle_stats(m) + if not m.private? + total = 0 + tokens = @channels[m.target] + hsh = tokens[m.params] + msg1 = "" + if not hsh.empty? + sorted = hsh.sort { |i, j| j[1] <=> i[1] } + sorted.each { |a| + total += a[1] + } + + msg = "Stats for #{m.params}. Said #{total} times. The top sayers are " + if sorted[0..2] + msg << "#{sorted[0].join ':'}" if sorted[0] + msg << ", #{sorted[1].join ':'}" if sorted[1] + msg << ", and #{sorted[2].join ':'}" if sorted[2] + msg << "." + + msg1 << "#{m.sourcenick} has said it " + if hsh[m.sourcenick] + msg1 << "#{hsh[m.sourcenick]} times." + else + msg1 << "0 times." + end + else + msg << "#{m.params} has not been said yet!" + end + @bot.action m.replyto, msg + @bot.action m.replyto, msg1 if msg1 + else + m.reply "#{m.params} is not currently being tracked." + end + end + end + #}}} + #{{{ + def handle_track(m) + if not m.private? + if @channels[m.target] + tokens = @channels[m.target] + else + tokens = Hash.new + @channels[m.target] = tokens + end + tokens[m.params] = Hash.new + m.reply "now tracking #{m.params}" + end + end + #}}} + #{{{ + def handle_untrack(m) + if not m.private? + toks = @channels[m.target] + if toks.has_key? m.params + toks.delete m.params + m.reply "no longer tracking #{m.params}" + else + m.reply "Are your signals crossed? Since when have I tracked that?" + end + end + + toks = nil + end + #}}} + #{{{ + def handle_listtokens(m) + if not m.private? and not @channels.empty? + tokens = @channels[m.target] + unless tokens.empty? + toks = "" + tokens.each_key { |k| + toks << "#{k} " + } + @bot.action m.replyto, "is currently keeping stats for: #{toks}" + else + @bot.action m.replyto, "is not currently keeping stats for anything" + end + elsif not m.private? + @bot.action m.replyto, "is not currently keeping stats for anything" + end + end + #}}} + #{{{ + def handle_rmabuser(m) + m.reply "This feature has not yet been implemented" + end + #}}} + #}}} + +end +plugin = StatsPlugin.new +plugin.register("stats") +plugin.register("track") +plugin.register("untrack") +plugin.register("listtokens") +#plugin.register("rmabuser") diff --git a/contrib/plugins/vandale.rb b/contrib/plugins/vandale.rb new file mode 100644 index 00000000..7b806c85 --- /dev/null +++ b/contrib/plugins/vandale.rb @@ -0,0 +1,49 @@ +#----------------------------------------------------------------# +# Filename: vandale.rb +# Description: Rbot plugin. Looks up a word in the Dutch VanDale +# dictionary +# Author: eWoud - ewoud.nuytsstudent.kuleuven.ac.be +# requires GnuVD www.djcbsoftware.nl/projecten/gnuvd/ +#----------------------------------------------------------------# + +class VanDalePlugin < Plugin + def help(plugin, topic="") + "vandale [] => Look up in the VanDale dictionary" + end + def privmsg(m) + case m.params + when (/^([\w-]+)$/) + ret = Array.new + Utils.safe_exec("/usr/local/bin/gnuvd", m.params).each{|line| if line.length > 5 then ret << line end} + m.reply ret.delete_at(0) + while ret[0] =~ /^[[:alpha:]_]*[0-9]/ + m.reply ret.delete_at(0) + end + while ret[0] =~ /^[0-9]/ + m.reply ret.delete_at(0) + end + i = 0 + while i < ret.length + ret[i] = ret[i].slice(/^[[:graph:]_]*/) + if ret[i].length == 0 or ret[i] =~ /^[0-9]/ + then + ret.delete_at(i) + else + i = i+1 + end + end + if ret.length != 0 then + m.reply "zie ook " + ret.join(", ") + end + return + when nil + m.reply "incorrect usage: " + help(m.plugin) + return + else + m.reply "incorrect usage: " + help(m.plugin) + return + end + end +end +plugin = VanDalePlugin.new +plugin.register("vandale") diff --git a/docgen b/docgen new file mode 100755 index 00000000..bae6c807 --- /dev/null +++ b/docgen @@ -0,0 +1,3 @@ +#!/bin/sh +rdoc -a --exclude 'rbot/(db)?plugins/' --main rbot.rb -d + diff --git a/rbot.rb b/rbot.rb new file mode 100755 index 00000000..4d13a0d0 --- /dev/null +++ b/rbot.rb @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby + +# 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. + +$VERBOSE=true + +require 'getoptlong' +require 'rbot/ircbot' + +$debug = true +$version="0.9.7" +$opts = Hash.new + +# print +message+ if debugging is enabled +def debug(message=nil) + print "DEBUG: #{message}\n" if($debug && message) + #yield +end + +opts = GetoptLong.new( + [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], + [ "--help", "-h", GetoptLong::OPTIONAL_ARGUMENT ] +) + +opts.each {|opt, arg| + $debug = true if(opt == "--debug") + $opts[opt.sub(/^-+/, "")] = arg +} + +botclass = ARGV.shift +botclass = "rbotconf" unless(botclass); + +if(bot = Irc::IrcBot.new(botclass)) + if($opts["help"]) + puts bot.help($opts["help"]) + else + # run the bot + bot.mainloop + end +end + diff --git a/rbot/auth.rb b/rbot/auth.rb new file mode 100644 index 00000000..017745ab --- /dev/null +++ b/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["PASSWD"]) + @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/rbot/channel.rb b/rbot/channel.rb new file mode 100644 index 00000000..0db4c106 --- /dev/null +++ b/rbot/channel.rb @@ -0,0 +1,50 @@ +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 + + # when called like "puts @bots.channels[chan].topic" + def to_s + @name + end + end + + end + +end diff --git a/rbot/config.rb b/rbot/config.rb new file mode 100644 index 00000000..52899205 --- /dev/null +++ b/rbot/config.rb @@ -0,0 +1,40 @@ +module Irc + + # container for bot configuration + # just treat it like a hash + class BotConfig < Hash + + # bot:: parent bot class + # create a new config hash from #{botclass}/conf.rbot + def initialize(bot) + super(false) + @bot = bot + # some defaults + self["SERVER"] = "localhost" + self["PORT"] = "6667" + self["NICK"] = "rbot" + self["USER"] = "gilbertt" + self["LANGUAGE"] = "english" + self["SAVE_EVERY"] = "60" + self["KEYWORD_LISTEN"] = false + if(File.exist?("#{@bot.botclass}/conf.rbot")) + IO.foreach("#{@bot.botclass}/conf.rbot") do |line| + next if(line =~ /^\s*#/) + if(line =~ /(\S+)\s+=\s+(.*)$/) + self[$1] = $2 if($2) + end + end + end + end + + # write current configuration to #{botclass}/conf.rbot + def save + Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}")) + File.open("#{@bot.botclass}/conf.rbot", "w") do |file| + self.each do |key, value| + file.puts "#{key} = #{value}" + end + end + end + end +end diff --git a/rbot/dbhash.rb b/rbot/dbhash.rb new file mode 100644 index 00000000..1801a38f --- /dev/null +++ b/rbot/dbhash.rb @@ -0,0 +1,126 @@ +# 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 'bdb' +# 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/rbot/ircbot.rb b/rbot/ircbot.rb new file mode 100644 index 00000000..60ed5e98 --- /dev/null +++ b/rbot/ircbot.rb @@ -0,0 +1,736 @@ +# 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' + +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 + + # 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["SAVE_EVERY"].to_i) { save } + @channels = Hash.new + @logs = Hash.new + + @lang = Irc::Language.new(@config["LANGUAGE"]) + @keywords = Irc::Keywords.new(self) + @auth = Irc::IrcAuth.new(self) + @plugins = Irc::Plugins.new(self, ["#{botclass}/plugins"]) + @socket = Irc::IrcSocket.new(@config["SERVER"], @config["PORT"], @config["HOST"], @config["SENDQ_DELAY"], @config["SENDQ_BURST"]) + @nick = @config["NICK"] + @server_password = @config["SERVER_PASSWORD"] + if @config["ADDRESS_PREFIX"] + @addressing_prefixes = @config["ADDRESS_PREFIX"].split(" ") + 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["QUSER"]) + puts "authing with Q using #{@config["QUSER"]} #{@config["QAUTH"]}" + @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config["QUSER"]} #{@config["QAUTH"]}" + end + + if(@config["JOIN_CHANNELS"]) + @config["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("topic", m) + @plugins.delegate("listen", 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]] = ["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']} #{@config['PORT']}: " + e + end + @socket.puts "PASS " + @server_password if @server_password + @socket.puts "NICK #{@nick}\nUSER #{@config['USER']} 4 #{@config['SERVER']} :Ruby bot. (c) Tom Gilbert" + 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.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 + + # 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 + + 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)) + okay m.replyto + save + 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 (/^ping$/i) + say m.replyto, "pong" + when (/^rescan$/i) + if(@auth.allow?("config", m.source, m.replyto)) + okay m.replyto + rescan + end + when (/^quiet$/i) + if(auth.allow?("talk", m.source, m.replyto)) + say m.replyto, @lang.get("okay") + @channels.each_value {|c| c.quiet = true } + end + when (/^quiet in (\S+)$/i) + where = $1 + if(auth.allow?("talk", m.source, m.replyto)) + say m.replyto, @lang.get("okay") + where.gsub!(/^here$/, m.target) if m.public? + @channels[where].quiet = true if(@channels.has_key?(where)) + end + when (/^talk$/i) + if(auth.allow?("talk", m.source, m.replyto)) + @channels.each_value {|c| c.quiet = false } + okay m.replyto + end + when (/^talk in (\S+)$/i) + where = $1 + if(auth.allow?("talk", m.source, m.replyto)) + where.gsub!(/^here$/, m.target) if m.public? + @channels[where].quiet = false if(@channels.has_key?(where)) + okay m.replyto + end + # TODO break this out into an options module + when (/^options get sendq_delay$/i) + if auth.allow?("config", m.source, m.replyto) + m.reply "options->sendq_delay = #{@socket.get_sendq}" + end + when (/^options get sendq_burst$/i) + if auth.allow?("config", m.source, m.replyto) + m.reply "options->sendq_burst = #{@socket.get_maxburst}" + end + when (/^options set sendq_burst (.*)$/i) + num = $1.to_i + if auth.allow?("config", m.source, m.replyto) + @socket.set_maxburst(num) + @config["SENDQ_BURST"] = num + okay m.replyto + end + when (/^options set sendq_delay (.*)$/i) + freq = $1.to_f + if auth.allow?("config", m.source, m.replyto) + @socket.set_sendq(freq) + @config["SENDQ_DELAY"] = freq + okay m.replyto + 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? + + puts @channels[m.channel].topic + 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 + + # 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/rbot/ircsocket.rb b/rbot/ircsocket.rb new file mode 100644 index 00000000..25895644 --- /dev/null +++ b/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 + # server:: server to connect to + # port:: IRCd port + # host:: optional local host to bind to (ruby 1.7+ required) + # create a new IrcSocket + def initialize(server, port, host, sendfreq=2, maxburst=4) + @server = server.dup + @port = port.to_i + @host = host + @lines_sent = 0 + @lines_received = 0 + if sendfreq + @sendfreq = sendfreq.to_f + else + @sendfreq = 2 + end + @last_send = Time.new - @sendfreq + @burst = 0 + if maxburst + @maxburst = maxburst.to_i + else + @maxburst = 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 (@sendfreq > 0) + @qthread = Thread.new { spooler } + end + end + + def set_sendq(newfreq) + debug "changing sendq frequency to #{newfreq}" + @qmutex.synchronize do + @sendfreq = newfreq + if newfreq == 0 && @qthread + clearq + Thread.kill(@qthread) + @qthread = false + elsif(newfreq != 0 && !@qthread) + @qthread = Thread.new { spooler } + end + end + end + + def set_maxburst(newburst) + @qmutex.synchronize do + @maxburst = newburst + end + end + + def get_maxburst + return @maxburst + end + + def get_sendq + return @sendfreq + end + + # used to send lines to the remote IRCd + # message: IRC message to send + def puts(message) + @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 @sendfreq > 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.1 + end + end + + # pop a message off the queue, send it + def spool + unless @sendq.empty? + now = Time.new + if (now >= (@last_send + @sendfreq)) + # reset burst counter after @sendfreq has passed + @burst = 0 + debug "in spool, resetting @burst" + elsif (@burst >= @maxburst) + # nope. can't send anything + return + end + @qmutex.synchronize do + debug "(can send #{@maxburst - @burst} lines, there are #{@sendq.length} to send)" + (@maxburst - @burst).times do + 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/rbot/keywords.rb b/rbot/keywords.rb new file mode 100644 index 00000000..595fe22c --- /dev/null +++ b/rbot/keywords.rb @@ -0,0 +1,382 @@ +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" + else + return "Keyword module (Fact learning and regurgitation) topics: overview, set, plurals, override, also, random, get, tell, forget, keywords, , , , " + end + end + + # privmsg handler + def privmsg(m) + 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 + end + else + # in channel message, not to me + if(m.message =~ /^'(.*)$/ || (@bot.config["NO_KEYWORD_ADDRESS"] == "true" && 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/rbot/language.rb b/rbot/language.rb new file mode 100644 index 00000000..9788b2bb --- /dev/null +++ b/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/rbot/languages/dutch.lang b/rbot/languages/dutch.lang new file mode 100644 index 00000000..db116264 --- /dev/null +++ b/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/rbot/languages/english.lang b/rbot/languages/english.lang new file mode 100644 index 00000000..58427a42 --- /dev/null +++ b/rbot/languages/english.lang @@ -0,0 +1,71 @@ +okay: + okay + okay then :) + fine + done + can do! + alright + sure + aight + lemme take care of that for you +dunno: + dunno + beats me + no idea + no clue + *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 :( + but what's %s? +insult: + %s: wanker! + %s: :( + %s: I hate you :( + %s: die! + %s: I'm offended! +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/rbot/message.rb b/rbot/message.rb new file mode 100644 index 00000000..8604e1a4 --- /dev/null +++ b/rbot/message.rb @@ -0,0 +1,237 @@ +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 + + # 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 + + # 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 + + # 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 + + # 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 + 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/rbot/plugins.rb b/rbot/plugins.rb new file mode 100644 index 00000000..b99fe562 --- /dev/null +++ b/rbot/plugins.rb @@ -0,0 +1,242 @@ +module Irc + + # 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: + # + # 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 + # 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 + @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, "")) + 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) + Plugins.plugins[name] = self + @names << name + end + + # is this plugin listening to all messages? + def listen? + @listen + 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/rbot/plugins/autorejoin.rb b/rbot/plugins/autorejoin.rb new file mode 100644 index 00000000..03118f85 --- /dev/null +++ b/rbot/plugins/autorejoin.rb @@ -0,0 +1,14 @@ +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.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/rbot/plugins/cal.rb b/rbot/plugins/cal.rb new file mode 100644 index 00000000..1e823194 --- /dev/null +++ b/rbot/plugins/cal.rb @@ -0,0 +1,14 @@ +class CalPlugin < Plugin + def help(plugin, topic="") + "cal [options] => show current calendar [unix cal options]" + end + def privmsg(m) + if m.params && m.params.length > 0 + m.reply Utils.safe_exec("cal", m.params) + else + m.reply Utils.safe_exec("cal") + end + end +end +plugin = CalPlugin.new +plugin.register("cal") diff --git a/rbot/plugins/dice.rb b/rbot/plugins/dice.rb new file mode 100644 index 00000000..928da894 --- /dev/null +++ b/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/rbot/plugins/eightball.rb b/rbot/plugins/eightball.rb new file mode 100644 index 00000000..6d123b34 --- /dev/null +++ b/rbot/plugins/eightball.rb @@ -0,0 +1,18 @@ +# 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 privmsg(m) + answers = @answers[rand(@answers.length)] + action = "shakes the magic 8-ball... #{answers}" + @bot.action m.replyto, action + end +end +plugin = EightBallPlugin.new +plugin.register("8ball") diff --git a/rbot/plugins/excuse.rb b/rbot/plugins/excuse.rb new file mode 100644 index 00000000..38e85ad6 --- /dev/null +++ b/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/rbot/plugins/fish.rb b/rbot/plugins/fish.rb new file mode 100644 index 00000000..ab84ff16 --- /dev/null +++ b/rbot/plugins/fish.rb @@ -0,0 +1,84 @@ +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 privmsg(m) + + proxy_host = nil + proxy_port = nil + + if(ENV['http_proxy']) + if(ENV['http_proxy'] =~ /^http:\/\/(.+):(\d+)$/) + proxy_host = $1 + proxy_port = $2 + end + end + + langs = ["en", "fr", "de", "it", "pt", "es", "nl"] + + query = "/babelfish/tr" + if(m.params =~ /^to\s+(\S+)\s+(.*)/) + trans_from = "en" + trans_to = $1 + trans_text = $2 + elsif(m.params =~ /^from\s+(\S+)\s+(.*)/) + trans_from = $1 + trans_to = "en" + trans_text = $2 + elsif(m.params =~ /^(\S+)\s+(\S+)\s+(.*)/) + trans_from = $1 + trans_to = $2 + trans_text = $3 + else + m.reply "incorrect usage: " + help(m.plugin) + return + end + 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 = Net::HTTP.new("babelfish.altavista.com", 80, proxy_host, proxy_port) + + http.start {|http| + resp = http.post(query, data, {"content-type", + "application/x-www-form-urlencoded"}) + + if (resp.code == "200") + #puts resp.body + resp.body.each_line {|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 + } + m.reply "couldn't parse babelfish response html :(" + else + m.reply "couldn't talk to babelfish :(" + end + } + end +end +plugin = BabelPlugin.new +plugin.register("translate") + diff --git a/rbot/plugins/fortune.rb b/rbot/plugins/fortune.rb new file mode 100644 index 00000000..2f76a318 --- /dev/null +++ b/rbot/plugins/fortune.rb @@ -0,0 +1,24 @@ +class FortunePlugin < Plugin + def help(plugin, topic="") + "fortune [] => get a (short) fortune, optionally specifying fortune db" + end + def privmsg(m) + case m.params + when (/\B-/) + m.reply "incorrect usage: " + help(m.plugin) + return + when (/^([\w-]+)$/) + db = $1 + when nil + db = "fortunes" + else + m.reply "incorrect usage: " + help(m.plugin) + return + end + ret = Utils.safe_exec("/usr/games/fortune", "-n", "255", "-s", db) + m.reply ret.split("\n").join(" ") + return + end +end +plugin = FortunePlugin.new +plugin.register("fortune") diff --git a/rbot/plugins/freshmeat.rb b/rbot/plugins/freshmeat.rb new file mode 100644 index 00000000..502261a0 --- /dev/null +++ b/rbot/plugins/freshmeat.rb @@ -0,0 +1,107 @@ +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 privmsg(m) + if m.params && m.params =~ /^search\s+(.*)\s+(\d+)$/ + search = $1 + limit = $2.to_i + search_freshmeat m, search, limit + elsif m.params && m.params =~ /^search\s+(.*)$/ + search = $1 + search_freshmeat m, search + elsif m.params && m.params =~ /^(\d+)$/ + limit = $1.to_i + freshmeat m, limit + else + freshmeat m + end + end + + def search_freshmeat(m, search, max=4) + max = 8 if max > 8 + begin + xml = Utils.http_get("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, max=4) + max = 8 if max > 8 + xml = Utils.http_get("http://download.freshmeat.net/backend/fm-releases-software.rdf") + 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("*/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 reply + } + end +end +plugin = FreshmeatPlugin.new +plugin.register("freshmeat") diff --git a/rbot/plugins/google.rb b/rbot/plugins/google.rb new file mode 100644 index 00000000..5fa466e7 --- /dev/null +++ b/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 = Net::HTTP.new("www.google.com", 80, proxy_host, proxy_port) + + http.start {|http| + begin + 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/rbot/plugins/host.rb b/rbot/plugins/host.rb new file mode 100644 index 00000000..cb88eee7 --- /dev/null +++ b/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/rbot/plugins/insult.rb b/rbot/plugins/insult.rb new file mode 100644 index 00000000..5f0384e8 --- /dev/null +++ b/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/rbot/plugins/karma.rb b/rbot/plugins/karma.rb new file mode 100644 index 00000000..44b6d026 --- /dev/null +++ b/rbot/plugins/karma.rb @@ -0,0 +1,75 @@ +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 help(plugin, topic="") + "karma module: ++/-- => increase/decrease karma for , karma for ? => show karma for . Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own." + end + def listen(m) + if(m.kind_of?(PrivMessage) && m.public?) + # in channel message, the kind we are interested in + if(m.message =~ /(\+\+|--)/) + string = m.message.sub(/\W(--|\+\+)(\(.*?\)|[^(++)(\-\-)\s]+)/, "\2\1") + seen = Hash.new + while(string.sub!(/(\(.*?\)|[^(++)(\-\-)\s]+)(\+\+|--)/, "")) + key = $1 + change = $2 + next if seen[key] + seen[key] = true + + 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 + def privmsg(m) + unless(m.params) + m.reply "incorrect usage: " + m.plugin + return + end + if(m.params =~ /^(?:for\s+)?(\S+?)\??$/) + thing = $1 + karma = @registry[thing] + if(karma != 0) + m.reply "karma for #{thing}: #{@registry[thing]}" + else + m.reply "#{thing} has neutral karma" + end + end + end +end +plugin = KarmaPlugin.new +plugin.register("karma") diff --git a/rbot/plugins/lart.rb b/rbot/plugins/lart.rb new file mode 100644 index 00000000..385b17c3 --- /dev/null +++ b/rbot/plugins/lart.rb @@ -0,0 +1,177 @@ +# 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 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 + @bot.okay m.replyto + end + #}}} + #{{{ + def handle_rmlart(m) + @larts.delete m.params + @bot.okay m.replyto + end + #}}} + #{{{ + def handle_addpraise(m) + @praises << m.params + @bot.okay m.replyto + end + #}}} + #{{{ + def handle_rmpraise(m) + @praises.delete m.params + @bot.okay m.replyto + 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/rbot/plugins/math.rb b/rbot/plugins/math.rb new file mode 100644 index 00000000..4a207389 --- /dev/null +++ b/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/rbot/plugins/nickserv.rb b/rbot/plugins/nickserv.rb new file mode 100644 index 00000000..2a40bae5 --- /dev/null +++ b/rbot/plugins/nickserv.rb @@ -0,0 +1,92 @@ +# 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" + 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 + @bot.okay m.replyto + when (/^register$/) + passwd = genpasswd + @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + @registry[nick] = passwd + @bot.okay m.replyto + when (/^register\s*(.*)\s*$/) + passwd = $1 + @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + @bot.okay m.replyto + 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] + @bot.okay m.replyto + 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/rbot/plugins/nslookup.rb b/rbot/plugins/nslookup.rb new file mode 100644 index 00000000..a68f0ee8 --- /dev/null +++ b/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 =~ /^(\w|\.)+$/) + 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/rbot/plugins/opmeh.rb b/rbot/plugins/opmeh.rb new file mode 100644 index 00000000..08227925 --- /dev/null +++ b/rbot/plugins/opmeh.rb @@ -0,0 +1,18 @@ +class OpMehPlugin < Plugin + + def help(plugin, topic="") + return "opmeh => grant user ops in " + end + + def privmsg(m) + unless(m.params) + m.reply "usage: " + help(m.plugin) + return + end + target = m.sourcenick + channel = m.params + @bot.sendq("MODE #{channel} +o #{target}") + end +end +plugin = OpMehPlugin.new +plugin.register("opmeh") diff --git a/rbot/plugins/quotes.rb b/rbot/plugins/quotes.rb new file mode 100644 index 00000000..072e352a --- /dev/null +++ b/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}/ } + 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}/ } + 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)) + @bot.okay m.replyto + 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)) + @bot.okay m.replyto + 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/rbot/plugins/remind.rb b/rbot/plugins/remind.rb new file mode 100644 index 00000000..402e2d08 --- /dev/null +++ b/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