From 3e05fc5b4c22248733f430b7935c96321bccc88a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 24 Aug 2007 10:58:41 -0600 Subject: [PATCH] Import from darcs --- README | 159 +++++++++++++ acrobot.py | 66 ++++++ addfacts.py | 29 +++ arsenic.py | 140 +++++++++++ async_proc.py | 69 ++++++ bindingsbot.py | 114 +++++++++ convert.py | 44 ++++ daemon.py | 40 ++++ dbdump.py | 9 + dorkbot.py | 63 +++++ filedbm.py | 381 ++++++++++++++++++++++++++++++ finger.py | 34 +++ firebot.py | 457 ++++++++++++++++++++++++++++++++++++ gallium.py | 114 +++++++++ health.sh | 7 + infobot.py | 301 ++++++++++++++++++++++++ irc.py | 604 ++++++++++++++++++++++++++++++++++++++++++++++++ linkbot.py | 96 ++++++++ opbot.py | 38 +++ procbot.py | 74 ++++++ seedyb.py | 146 ++++++++++++ shorturl.py | 325 ++++++++++++++++++++++++++ testbot.py | 22 ++ webretriever.py | 75 ++++++ 24 files changed, 3407 insertions(+) create mode 100644 README create mode 100755 acrobot.py create mode 100755 addfacts.py create mode 100755 arsenic.py create mode 100755 async_proc.py create mode 100644 bindingsbot.py create mode 100755 convert.py create mode 100755 daemon.py create mode 100755 dbdump.py create mode 100755 dorkbot.py create mode 100755 filedbm.py create mode 100644 finger.py create mode 100755 firebot.py create mode 100755 gallium.py create mode 100755 health.sh create mode 100644 infobot.py create mode 100644 irc.py create mode 100755 linkbot.py create mode 100755 opbot.py create mode 100755 procbot.py create mode 100644 seedyb.py create mode 100755 shorturl.py create mode 100644 testbot.py create mode 100755 webretriever.py diff --git a/README b/README new file mode 100644 index 0000000..858d847 --- /dev/null +++ b/README @@ -0,0 +1,159 @@ +== Firebot == + +FireBot is a winner! + +Firebot is an IRC bot combining the functionality of a Linkbot, an +Infobot, and a Clickolinko, which is this cool thing Emad El-Haraty and +I came up with to make short URLs out of stuff posted into the channel, +for people with text browsers that wrap URLs. + +Note that, in addition to interacting with FireBot within a channel, you +can also communicate directly with FireBot via `/msg` commands. Just in +case you need a little one-on-one action and don't want spew your +playtime around some channel with other folks watching. That can be *so* +distracting. Some commands still require you to preface them with +FireBot's name. Example: + + /msg firebot firebot: literal ... + + +Downloading +----------- + + darcs get http://woozle.org/~neale/repos/firebot + + +LinkBot Features +---------------- + +Firebot can link channels across networks. It is present in all +channels and the same functions can be accessed on either side. +Everything said on one channel is relayed to the others. + +It is possible to link multiple channels on multiple servers, including +multiple channels on a single server. + + + +ClickLinko (UrlBot) +------------------- + +Whenever FireBot sees a URL in the channel, he makes a note of it and +creates a shorter URL out of it. + + + +InfoBot +------- + +As an InfoBot, FireBot listens in the channel for anything of the form +"x is y", and then stores that little tidbit. Later, when someone asks +a question about x ("what is x?", "who is x?", "wtf is x?"), FireBot +answers with the factoid he gathered. + +
+
firebot, _x_
+
look up a factoid for _x_
+ +
firebot, _x_ is _y_
+
store _y_ as a factiod about _x_
+ +
firebot, _x_ is also _y_
+
store _y_ as another factoid about _x_
+ +
firebot, append _x_ <= _y_
+
store _y_ as another factoid about _x_. You'd use this for things where _x_ has the word "is" in it, or other things that you can't store with the preceding commands.
+ +
no, firebot, _x_ is _y_
+
store _y_ as the only factoid about _x_, even if _x_ already has factoids
+ +
firebot, literal _x_
+
display all factoids about _x_
+ +
firebot, lock _x_
+
do not learn any more factoids about _x_
+ +
firebot, unlock _x_
+
resume learning factoids about _x_
+ +
firebot, forget _x_
+
forget all factoids about _x_
+ +
firebot, forget _x_ from _y_
+
forget a single entry (x) that is listed in _y_; _x_ can be a single word, it does not need to be the whole entry
+ +
+ +In addition, the following tricks can be used within factiods: + +* Any factoid beginning with `\\` (a backslash) is displayed directly. + That is, instead of saying " x is y", FireBot just says + " y". +* Any factoid beginning with : (a colon) is + displayed an action. That is, instead of saying " x is y", + FireBot says "* firebot y" +* You may put `%(sender)s` in the factoid to print the name of the + person who asked about the factoid (when sent to a user in a private + message, it's the recipient of the message) + + + +Utilities +--------- + +
+ +
firebot, later tell _whom_ _what_
+
The next time _whom_ says something in the channel, deliver the message _what_ publically.
+ +
firebot, in _time_ say _what_
+
after _time_ (eg. "15 seconds", "2 hours", "5 days"), announce _what_ in the channel
+ +
seen _whom_
+
Report the last thing said in the channel by _whom_, and how long ago it was said.
+ +
dict _word_
+
look _word_ up in the dictionary
+ +
quote _symbol_
+
get a stock quote for _symbol_
+ +
pollen _zip_
+
report pollen forecast for US zip code _zip_
+ +
cdecl explain _jibberish_
+
explain the C declaration _jibberish_ (eg. "cdecl explain struct bar *(*foo)[](int)")
+ +
cdecl declare _english_
+
give the C declaration for _english_ (eg. "cdecl declare foo as pointer to array of function (int) returning pointer to struct bar")
+ +
how many _x_ in _y_ _z_
+
determine the number of _x_ items that are contained in _y_ amount of _z_ items (eg. how many miles in 1 light year)
+ +
how much is _amt_ _source_ in _dest_
+
do a currency conversion from _source_ to _dest_. Both must be three-letter currency codes. (eg. "how much is 100 USD in EUR")
+ +
calc _expr_
+
calculate _expr_ (eg. "calc 2 * 5")
+ +
+ + +Toys +---- + +
+ +
8ball, _question_
+
consult the magic 8-ball regarding _question_
+ +
_nickname_++
+
add a whuffie point for _nickname_
+ +
_nickname_--
+
remove a whuffie point for _nickname_
+ +
whuffie for _nickname_
+
check the whuffie for _nickname_
+ +
diff --git a/acrobot.py b/acrobot.py new file mode 100755 index 0000000..5a976f6 --- /dev/null +++ b/acrobot.py @@ -0,0 +1,66 @@ +#! /usr/bin/env python + +import irc +import random + +SERVER = ('woozle.org', 6667) +NAMES = ['acrobot'] +INFO = "Acrophobia!" +CHANNELS = ["#acro"] + +LETTERS = ( + 'A' * 176 + + 'B' * 167 + + 'C' * 251 + + 'D' * 136 + + 'E' * 104 + + 'F' * 101 + + 'G' * 91 + + 'H' * 107 + + 'I' * 105 + + 'J' * 30 + + 'K' * 30 + + 'L' * 89 + + 'M' * 146 + + 'N' * 53 + + 'O' * 50 + + 'P' * 195 + + 'Q' * 13 + + 'R' * 103 + + 'S' * 273 + + 'T' * 132 + + 'U' * 20 + + 'V' * 41 + + 'W' * 71 + + 'X' * 1 + + 'Y' * 11 + + 'Z' * 6) + +class AcroBot(irc.Bot): + def cmd_privmsg(self, sender, forum, addl): + if forum.name() in self.channels: + return + self.command(sender, addl) + + def command(self, sender, addl): + print (sender, addl) + + def _make_acro(self, min, max): + letters = [] + for i in range(random.randint(min, max)): + letters.append(random.choice(LETTERS)) + return letters + + def cmd_join(self, sender, forum, addl): + self.debug = True + if sender.name() in self.nicks: + self.heartbeat() + + def heartbeat(self): + if True: + acro = ''.join(self._make_acro(3, 8)) + self.announce(acro) + +l2 = AcroBot(SERVER, NAMES, INFO, CHANNELS) + +irc.run_forever() diff --git a/addfacts.py b/addfacts.py new file mode 100755 index 0000000..43218e0 --- /dev/null +++ b/addfacts.py @@ -0,0 +1,29 @@ +#! /usr/bin/env python + +import urllib2 +import shelve +import sys +import InfoBot + +def main(): + db = shelve.DbfilenameShelf('info.db') + count = 0 + for url in sys.argv[1:]: + print url + f = urllib2.urlopen(url) + while True: + line = f.readline() + if not line: + break + line = line.strip() + try: + key, val = line.split(' => ', 1) + except ValueError: + continue + db[key] = (InfoBot.locked, val) + count += 1 + print "Added %d facts." % count + +if __name__ == '__main__': + main() + diff --git a/arsenic.py b/arsenic.py new file mode 100755 index 0000000..2ecb0d6 --- /dev/null +++ b/arsenic.py @@ -0,0 +1,140 @@ +#! /usr/bin/env python + +import firebot +from finger import Finger +from procbot import ProcBot, Runner +import shorturl +import asyncore +import irc +import re +import os +import time +import socket + +class Arsenic(firebot.FireBot, ProcBot): + debug = True + bindings = [] + ping_interval = 120 + + def __init__(self, *args, **kwargs): + firebot.FireBot.__init__(self, *args, **kwargs) + self.seen = {} + self.lusers = {} + self.heartbeat_interval=3 + self.lag = 0 + self.whinecount = 0 + + def rp(self, sender, forum, addl, match): + command = 'rp' + argstr = match.group('args') + Finger(('db.nic.lanl.gov', 5833), + argstr, + lambda l: self.proc_cb('%s: ' % command, sender, forum, l, 0)) + bindings.append((re.compile(r"^(?Prp) +(?P.*)$"), + rp)) + + def finger(self, sender, forum, addl, match): + command = 'finger' + argstr = match.group('args') + Finger(('finger.lanl.gov', 79), + argstr, + lambda l: self.proc_cb('%s: ' % command, sender, forum, l, 0)) + bindings.append((re.compile(r"^(?Pfinger) +(?P.*)$"), + finger)) + + def lag(self, sender, forum, addl, match): + forum.msg("My server lag is %.3f seconds." % self.lag) + bindings.append((re.compile(r"^\008[,: ]+ (what is the )?(server )?lag"), + lag)) + + def note(self, sender, forum, addl, match): + whom = match.group('whom') + what = match.group('what') + when = time.time() + key = '\013notes:%s' % whom + print key + note = (when, sender.name(), what) + try: + n = self.db[key] + except KeyError: + n = [] + n.append(note) + self.db[key] = n + forum.msg(self.gettext('okay', sender=sender.name())) + bindings.append((re.compile(r"^\008[:, ]+note (to )?(?P[^: ]+):? +(?P.*)"), + note)) + + bindings.extend(firebot.FireBot.bindings) + + ## + ## IRC protocol-level extensions + ## + + def add_luser(self, luser, channel): + # Keeps track of what users have been on what channels, and + # sends an invite to luser for every channel in which they're + # listed. If they're already in the channel, the server just + # sends back an error. This has the effect of letting people + # get back into invite-only channels after a disconnect. + who = luser.name() + self.lusers[channel.name()][who] = luser + for chan in self.lusers.keys(): + if chan == channel.name(): + continue + t = self.lusers[chan].get(who) + if t and t.host == luser.host: + self.write('INVITE %s %s' % (who, chan)) + + def cmd_join(self, sender, forum, addl): + if sender.name() == self.nick: + # If it was me, get a channel listing and beg for ops + self.write('WHO %s' % (forum.name())) + forum.notice('If you op me, I will op everyone who joins this channel.') + self.lusers[forum.name()] = {} + else: + # Otherwise, add the user + self.add_luser(sender, forum) + forum.write(['MODE', forum.name(), '+o'], sender.name()) + + def cmd_352(self, sender, forum, addl): + # Response to WHO + forum = irc.Channel(self, addl[0]) + who = irc.User(self, addl[4], addl[1], addl[2]) + self.add_luser(who, forum) + + def cmd_invite(self, sender, forum, addl): + # Join any channel to which we're invited + self.write('JOIN', forum.name()) + + def cmd_pong(self, sender, forum, addl): + now = time.time() + print now + self.lag = now - float(addl[0]) + + def cmd_482(self, sender, forum, addl): + self.whinecount += 1 + if (self.whinecount == 2 or + self.whinecount == 4 or + self.whinecount == 8): + forum.notice("Just a reminder: I can't op anyone unless I'm opped myself.") + elif (self.whinecount == 16): + forum.notice("This is the last time I'm going to beg for ops. Puh-leaze?") + + + +if __name__ == '__main__': + # Short URL server + us = shorturl.start(('', 0)) + firebot.URLSERVER = (socket.gethostbyaddr(socket.gethostname())[0], + us.getsockname()[1]) + + NICK = ['arsenic'] + INFO = "I'm a little printf, short and stdout" + + l1 = Arsenic(('greywolf.lanl.gov', 6697), + NICK, + INFO, + ["#x"], + ssl=True) + + irc.run_forever() diff --git a/async_proc.py b/async_proc.py new file mode 100755 index 0000000..557da04 --- /dev/null +++ b/async_proc.py @@ -0,0 +1,69 @@ +#! /usr/bin/env python + +"""An asyncore process object. + +You'd use it with popen. See the code at the bottom of +this file for an example. +""" + +import asyncore +import fcntl +import os + + +class process_wrapper: + """A wrapper to make a process look like a socket. + +asyncore wants things to look like sockets. So we fake it. +""" + + def __init__(self, inf): + self.inf = inf + self.fd = inf.fileno() + + def recv(self, size): + return self.inf.read(size) + + def send(self, data): + return + + def close(self): + return self.inf.close() + + def fileno(self): + return self.fd + +class process_dispatcher(asyncore.dispatcher): + + def __init__(self, inf=None): + asyncore.dispatcher.__init__(self) + self.connected = 1 + if inf: + flags = fcntl.fcntl(inf.fileno(), fcntl.F_GETFL, 0) + flags = flags | os.O_NONBLOCK + fcntl.fcntl(inf.fileno(), fcntl.F_SETFL, flags) + self.set_file(inf) + + def set_file(self, inf): + self.socket = process_wrapper(inf) + self._fileno = self.socket.fileno() + self.add_channel() + + def writable(self): + # It's a one-way socket + return False + +if __name__ == '__main__': + class foo(process_dispatcher): + def handle_read(self): + r = self.recv(1024) + if r: + print '[' + r + ']' + + def handle_close(self): + print "returned", self.close() + + f = os.popen('ls', 'r') + p = foo(f) + asyncore.loop() + diff --git a/bindingsbot.py b/bindingsbot.py new file mode 100644 index 0000000..6e835a7 --- /dev/null +++ b/bindingsbot.py @@ -0,0 +1,114 @@ +import irc +import re +import random +import types + +class Match: + """A wrapper around a regex match, to replace \008 with a word. + + """ + + def __init__(self, m, txt): + self.m = m + self.txt = txt + + def group(self, grp): + g = self.m.group(grp) + if g: + g = g.replace('\008', self.txt) + return g + + +class BindingsBot(irc.Bot): + """An IRC bot with regex function bindings + + You can bind functions to things said in the channel by regular + expression with this. See wouldmatch for an example of how to do + this. + """ + + msg_cat = {} # message catalog + bindings = [] # function/catalog bindings to regexen + + def __init__(self, *gar): + irc.Bot.__init__(self, *gar) + self.last_tb = "Nothing's gone wrong yet!" + + def err(self, exception): + """Save the traceback for later inspection""" + irc.Bot.err(self, exception) + t,v,tb = exception + tbinfo = [] + while 1: + tbinfo.append (( + tb.tb_frame.f_code.co_filename, + tb.tb_frame.f_code.co_name, + str(tb.tb_lineno) + )) + tb = tb.tb_next + if not tb: + break + # just to be safe + del tb + file, function, line = tbinfo[-1] + info = '[' + '] ['.join(map(lambda x: '|'.join(x), tbinfo)) + ']' + self.last_tb = '%s %s %s' % (t, v, info) + print self.last_tb + + def matches(self, text): + matches = [] + btext = text.replace(self.nick, '\008') + for b in self.bindings: + m = b[0].match(btext) + if m: + matches.append((m, b)) + return matches + + def cmd_privmsg(self, sender, forum, addl): + for m, b in self.matches(addl[0]): + f = b[1] + if callable(f): + cont = f(self, sender, forum, addl, Match(m, self.nick)) + elif type(f) == types.StringType: + forum.msg(self.gettext(f, sender=sender.name(), + forum=forum.name(), me=self.nick)) + cont = False + else: + raise ValueError("Can't handle type of %s", `f`) + if not cont: + break + + def gettext(self, msg, **dict): + """Format a message from the message catalog. + + Retrieve from the message catalog the message specified by msg, + filling in arguments as specified by dict. + + """ + + m = random.choice(self.msg_cat[msg]) + return m % dict + + def tbinfo(self, sender, forum, addl, match): + forum.msg(self.last_tb) + bindings.append((re.compile(r"^\008[,: ]+(tbinfo|traceback)$"), + tbinfo)) + + def wouldmatch(self, sender, forum, addl, match): + """Show what binding would be matched""" + + text = match.group(1) + matches = self.matches(text) + m = [i[1][1] for i in matches] + forum.msg('%s => %s' % (`text`, `m`)) + bindings.append((re.compile(r"^\008[,: ]+match (.+)$"), + wouldmatch)) + + # + # Message catalog + # + + msg_cat['okay'] = ('Okay, %(sender)s.',) + + + diff --git a/convert.py b/convert.py new file mode 100755 index 0000000..53e5c16 --- /dev/null +++ b/convert.py @@ -0,0 +1,44 @@ +#! /usr/bin/env python + +import seedyb +import shelve +import cPickle as pickle +import codecs + +def main(): + a = shelve.open('new.db') + d = seedyb.open('info.cdb') + + dec = codecs.getdecoder('utf-8') + enc = codecs.getencoder('utf-8') + + for k,l in a.iteritems(): + try: + tl = type(l) + if tl == type(13) and k[0] == '\x0b': + # Whuffie + k = k[1:] + d.set(k, str(l), special='whuffie') + elif tl == type(()): + locked = False + try: + k = dec(k)[0] + except UnicodeDecodeError: + continue + # Factoid + if l and l[0] == ('locked',): + locked = True + l = l[1:] + try: + d.set(k, l) + except UnicodeDecodeError: + continue + if locked: + d.lock(k) + except: + print (k, l) + raise + + d.sync() + +main() diff --git a/daemon.py b/daemon.py new file mode 100755 index 0000000..8ef50e0 --- /dev/null +++ b/daemon.py @@ -0,0 +1,40 @@ +#! /usr/bin/env python + +import os +import sys + +def daemon(pidfile=None, stdout=None, stderr=None): + # Do this first so errors print out right away + if pidfile: + f = file(pidfile, 'w') + else: + f = None + + pid = os.fork() + if pid: + # Exit first parent + os._exit(0) + + # Decouple from parent + os.setsid() + + # Second fork + pid = os.fork() + if pid: + # Exit second parent + os._exit(0) + + # Remap std files + os.close(0) + if stdout: + sys.stdout = stdout + os.close(1) + if stderr: + sys.stderr = stderr + os.close(2) + + # Write pid + if f: + f.write(str(os.getpid())) + f.close() + diff --git a/dbdump.py b/dbdump.py new file mode 100755 index 0000000..b5449da --- /dev/null +++ b/dbdump.py @@ -0,0 +1,9 @@ +#! /usr/bin/env python +import anydbm + +d = anydbm.open('info.db') +n = anydbm.open('new.db', 'c') + +for k,v in d.iteritems(): + n[k] = v + diff --git a/dorkbot.py b/dorkbot.py new file mode 100755 index 0000000..a851364 --- /dev/null +++ b/dorkbot.py @@ -0,0 +1,63 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +import firebot +import irc +import re +import os +import random + +class Gallium(firebot.FireBot): + bindings = [] + + bindings.extend(firebot.FireBot.bindings) + + def __init__(self, *args, **kwargs): + firebot.FireBot.__init__(self, *args, **kwargs) + self.heartbeat_interval=3 + self.debug = True + + def randglyph(self, sender, forum, addl, match): + count = 0 + while count < 5: + i = random.randint(0, 0xffff) + r = self.get('U+%x' % i) + if r: + forum.msg('U+%X %s' % (i, r)) + return + count += 1 + forum.msg("I tried %d random numbers and none of them was defined." % count) + bindings.append((re.compile(r"^u\+rand$"), + randglyph)) + + def whuffie_up(self, sender, forum, addl, match): + nick = match.group('nick') + if nick.lower() == sender.name().lower(): + forum.msg(self.gettext('whuffie whore', sender=sender.name())) + return + if match.group('dir') == 'up': + amt = 1 + else: + amt = -1 + self.whuffie_mod(nick, amt) + bindings.append((re.compile(r"^,(?Pup|down)\s+(?P\w+)$"), + whuffie_up)) + + +if __name__ == '__main__': + import shorturl + import socket + + # Short URL server + us = shorturl.start(('', 0)) + firebot.URLSERVER = (socket.gethostbyaddr(socket.gethostname())[0], + us.getsockname()[1]) + + snowbot = Gallium(('irc.freenode.net', 6667), + ['dorkbot'], + "I'm a little printf, short and stdout", + ["#rcirc"], + dbname='dorkbot.db') + + irc.run_forever(0.5) + diff --git a/filedbm.py b/filedbm.py new file mode 100755 index 0000000..8b49afb --- /dev/null +++ b/filedbm.py @@ -0,0 +1,381 @@ +#! /usr/bin/env python +import os +import string + +class error(Exception): + pass + +def unquote(s): + """unquote('abc%20def') -> 'abc def'.""" + mychr = chr + myatoi = int + list = s.split('%') + res = [list[0]] + myappend = res.append + del list[0] + for item in list: + if item[1:2]: + try: + myappend(mychr(myatoi(item[:2], 16)) + + item[2:]) + except ValueError: + myappend('%' + item) + else: + myappend('%' + item) + return "".join(res) + +def quote(s, safe): + """quote('abc def') -> 'abc%20def'.""" + res = list(s) + for i in range(len(res)): + c = res[i] + if c not in safe: + res[i] = '%%%02X' % ord(c) + return ''.join(res) + +class FileDBM: + """File Database class. + + This stores strings as files in a directory. + + Note, no locking is done. It would be wise to make sure there is + only one writer at any given time. + + """ + + safe = string.letters + string.digits + ',!@#$^()-_+=' + + def __init__(self, base, mode='r'): + self.base = os.path.abspath(base) + if mode in ('r', 'w'): + if not os.path.isdir(base): + raise error("need 'c' or 'n' flag to open new db") + if mode == 'r': + self.writable = True + else: + self.writable = False + elif mode == 'c': + if not os.path.isdir(base): + os.mkdir(base) + self.writable = True + elif mode == 'n': + if os.path.isdir(base): + os.removedirs(base) + os.mkdir(base) + self.writable = True + else: + raise error("flags should be one of 'r', 'w', 'c', or 'n'") + + def key2path(self, key): + """Transform key to a pathname. + + By default this does URL quoting on safe characters. + Be sure to provide a path2key method if you override this. + + """ + + return os.path.join(self.base, + quote(key, self.safe)) + + def path2key(self, path): + """Transform a pathname to a key.""" + + if not path.startswith(self.base): + raise error("Not a valid path") + key = path[len(self.base) + 1:] # +1 gets the / + if os.path.sep in key: + raise error("Not a valid path") + return unquote(key) + + def __len__(self): + count = 0 + for i in self.iterkeys(): + count += 1 + return count + + def __getitem__(self, key): + if not (type(key) == type('')): + raise TypeError("keys must be strings") + path = self.key2path(key) + try: + return file(path).read() + except IOError: + raise KeyError + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def __setitem__(self, key, val): + if not (type(key) == type(val) == type('')): + raise TypeError("keys and values must be strings") + path = self.key2path(key) + file(path, 'w').write(val) + + def setdefault(self, key, default): + try: + return self[key] + except KeyError: + self[key] = default + return default + + def __delitem__(self, key): + path = self.key2path(key) + try: + os.remove(path) + except OSError: + raise KeyError() + + def __contains__(self, value): + # This could be a lot slower than the user would expect. If you + # need it, use has_value. Of course, you could make a derived + # class that sets __contains__ = has_value + raise error("You didn't really want to do this.") + + def has_key(self, key): + return os.path.exists(self.key2path(key)) + + def has_value(self, value): + for val in self.itervalues(): + if val == value: + return True + return False + + def iterkeys(self): + for root, dirs, files in os.walk(self.base): + for f in files: + path = os.path.join(root, f) + try: + yield self.path2key(path) + except error: + pass + + def __iter__(self): + return self.iterkeys() + + def itervalues(self): + for key, val in self.itervalues(): + yield val + + def iteritems(self): + for k in self.iterkeys(): + yield (k, self[k]) + + def keys(self): + keys = [] + for k in self.iterkeys(): + keys.append(k) + return keys + + def items(self): + items = [] + for i in self.iteritems(): + items.append(i) + return items + + def values(self): + values = [] + for v in self.itervalues(): + values.append(v) + return values + + + +class LongFileDBM(FileDBM): + """A file database supporting any-length keys. + + It does this by splitting keys up into directories. + + """ + + # A special string to append to directories, so that no file will + # ever have the same path as a directory + dirsuffix = '%%' + + # In the worst case, quote makes the string 3x bigger. + # So any key longer than 80 characters gets split up. This + # gives us plenty of room with a 255-character filename limit, + # which seems to be the minimum limit on any OS these days. + dirlen = 80 + + def split(self, key): + """Split a key into its path components. + + Each component in the list returned will be a directory. Called + before quoting parts. + + This is probably what you want to override. You may need to do + join() too. + + """ + + parts = [] + while key: + parts.append(key[:self.dirlen]) + key = key[self.dirlen:] + return parts + + def join(self, parts): + """Join directory parts into a single string. + + This is called after unquoting parts. + + """ + return ''.join(parts) + + def key2path(self, key, makedirs=False): + parts = self.split(key) + path = self.base + + for part in parts[:-1]: + # Escape the part + d = quote(part, self.safe) + + # Append a safe string so no shorter key can have this + # path + d = d + self.dirsuffix + + # Stick it on the end + path = os.path.join(path, d) + + # Make directory if requested + if makedirs and not os.path.isdir(path): + os.mkdir(path) + + # Now we can add the filename + path = os.path.join(path, quote(parts[-1], self.safe)) + + return path + + def path2key(self, path): + """Transform a pathname to a key.""" + + if not path.startswith(self.base): + raise error("Not a valid path") + key = "" + parts = path[len(self.base) + 1:].split(os.path.sep) + parts_ = [] + for p in parts: + # Strip the special string + if p.endswith(self.dirsuffix): + p = p[:-len(self.dirsuffix)] + parts_.append(unquote(p)) + + key = self.join(parts_) + return key + + def __setitem__(self, key, val): + if not self.writable: + raise IOError('database was not opened writable') + if not (type(key) == type(val) == type('')): + raise TypeError("keys and values must be strings") + path = self.key2path(key, True) + file(path, 'w').write(val) + + def __delitem__(self, key): + path = self.key2path(key) + try: + os.remove(path) + except OSError: + raise KeyError() + + # Now try to clean up any directories + while True: + path = os.path.dirname(path) + if len(path) <= len(self.base): + break + try: + os.rmdir(path) + except OSError: + # Guess it's not empty + break + + def iterkeys(self): + for root, dirs, files in os.walk(self.base): + for f in files: + path = os.path.join(root, f) + try: + yield self.path2key(path) + except error: + pass + +class WordFileDBM(LongFileDBM): + """A layout using the first word as the top-level directory. + + I use this in my firebot, but it's included here more as an example + of how you could extend LongFileDBM. + + """ + + # I like having spaces in my filenames + safe = LongFileDBM.safe + ' ' + + def split(self, key): + # Three cases: + # + # 1. no_spaces,_short + # 2. one/one or more spaces + # 3. _long/really_really_really_really_..._long + # + # This means that keys beginning with "_long " will be filed + # with long keys. + # + # In any case, the first directory, if any, can be stripped + # completely. + + split = LongFileDBM.split(self, key) + + # Split up into words + parts = key.split(' ', 1) + if len(parts) == 1 and len(split) == 1: + # No spaces + return split + elif len(parts[0]) <= self.dirlen: + # >= 2 words, first word <= dirlen chars + return [parts[0]] + split + else: + return ['_long'] + split + + def join(self, parts): + # Two cases: + # + # ["one_part"] + # ["more", "more than one part"] + + if len(parts) == 1: + return parts[0] + else: + return LongFileDBM.join(self, parts[1:]) + +open = LongFileDBM + +if __name__ == '__main__': + def asserteq(a, b): + assert a == b, "%s != %s" % (`a`, `b`) + + f = LongFileDBM('/tmp/db', 'n') + asserteq(f.key2path('this is a thing'), '/tmp/db/this%20is%20a%20thing') + asserteq(f.key2path('1234567890' * 8), '/tmp/db/12345678901234567890123456789012345678901234567890123456789012345678901234567890') + asserteq(f.key2path('1234567890' * 20), '/tmp/db/12345678901234567890123456789012345678901234567890123456789012345678901234567890%%/12345678901234567890123456789012345678901234567890123456789012345678901234567890%%/1234567890123456789012345678901234567890') + + f = WordFileDBM('/tmp/db', 'n') + asserteq(f.path2key(f.key2path('this is a thing')), 'this is a thing') + asserteq(f.path2key(f.key2path('1234567890' * 8)), '1234567890' * 8) + asserteq(f.path2key(f.key2path('1234567890' * 20)), '1234567890' * 20) + + asserteq(f.get('grape'), None) + asserteq(f.setdefault('grape', 'red'), 'red') + asserteq(f.get('grape'), 'red') + asserteq(f.setdefault('grape', 'green'), 'red') + + longstr = '1234567890' * 10 + f[longstr] = '1' + asserteq(f[longstr], '1') + + asserteq(f.keys(), ['grape', longstr]) + + del f['grape'] + del f[longstr] + asserteq(f.keys(), []) diff --git a/finger.py b/finger.py new file mode 100644 index 0000000..67d0ee3 --- /dev/null +++ b/finger.py @@ -0,0 +1,34 @@ +#! /usr/bin/env python + +import asynchat +import socket + +class Finger(asynchat.async_chat): + def __init__(self, host, query, callback): + asynchat.async_chat.__init__(self) + self.query = query + self.callback = callback + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.push(self.query + '\n') + self.connect(host) + self.inbuf = '' + self.set_terminator(None) + + def handle_connect(self): + pass + + def collect_incoming_data(self, data): + self.inbuf += data + + def handle_close(self): + self.callback(self.inbuf) + self.close() + +if __name__ == '__main__': + import asyncore + + def p(x): + print x + + r = finger(('finger.lanl.gov', 79), '121726', p) + asyncore.loop() diff --git a/firebot.py b/firebot.py new file mode 100755 index 0000000..d2344f5 --- /dev/null +++ b/firebot.py @@ -0,0 +1,457 @@ +#! /usr/bin/env python + +import os +import sys +import re +import random +from webretriever import WebRetriever +import asynchat, asyncore +import socket +import csv +import adns +import time + +import procbot +import shorturl +import infobot + +Runner = procbot.Runner +esc = procbot.esc + +URLSERVER = ("", 0) + +class SSLSock: + def __init__(self, sock): + self.sock = sock + self.sock.setblocking(1) + self.ssl = socket.ssl(sock) + + def send(self, data): + self.ssl.write(data) + + def recv(self, bufsize): + return self.ssl.read(bufsize) + + def close(self): + self.sock.close() + +class FireBot(infobot.InfoBot, procbot.ProcBot): + #debug = True + + bindings = [] + msg_cat = {} + heartbeat_interval = 0.5 + ping_interval = 120 + + def __init__(self, host, nicks, gecos, channels, dbname='info.db', ssl=False, **kwargs): + infobot.InfoBot.__init__(self, host, nicks, gecos, channels, + **kwargs) + self.ssl = ssl + self.nosy = True + self.seen = {} + + def handle_connect(self): + if self.ssl: + self.plain_sock = self.socket + self.socket = SSLSock(self.socket) + infobot.InfoBot.handle_connect(self) + + def send_ping(self): + # Send keepalives to the server to see if we've lost + # connection. For some reason, using SSL prevents us from + # getting a RST. + self.write('PING %f' % time.time()) + self.add_timer(self.ping_interval, + self.send_ping) + + def cmd_001(self, sender, forum, addl): + infobot.InfoBot.cmd_001(self, sender, forum, addl) + self.add_timer(self.ping_interval, + self.send_ping) + + def note(self, sender, forum, addl, match): + whom = match.group('whom') + what = match.group('what') + when = time.time() + note = "%f:%s:%s" % (when, sender.name(), what) + n = self.getall(whom, special="note") + n.append(note) + self.set(whom, n, special="note") + forum.msg(self.gettext('okay', sender=sender.name())) + bindings.append((re.compile(r"^\008[:, ]+note (to )?(?P[^: ]+):? +(?P.*)"), + note)) + bindings.append((re.compile(r"^\008[:, ]+later tell (?P[^: ]+):? +(?P.*)"), + note)) + + def cmd_privmsg(self, sender, forum, addl): + infobot.InfoBot.cmd_privmsg(self, sender, forum, addl) + + if forum.is_channel(): + who = sender.name() + + # Update seen + text = addl[0] + now = time.time() + self.seen[who] = (now, text) + + # Deliver notes + n = self.getall(who, special="note") + if n: + notes = ["Welcome back, %s. You have %d notes:" % (who, len(n))] + for note in n: + when, whom, what = note.split(':', 2) + try: + notes.append(u"%s: %s <%s> %s" % (who, + time.ctime(float(when)), + whom, + what)) + except UnicodeDecodeError: + notes.append(u"%s" % ((who, + time.ctime(note[0]), + note[1], + note[2]),)) + self.despool(forum, notes) + self.delete(who, special="note") + + ## + ## Firebot stuff + ## + + def seen(self, sender, forum, addl, match): + whom = match.group('whom') + if whom == sender.name(): + forum.msg('Cute, %s.' % whom) + return + last = self.seen.get(whom) + now = time.time() + if last: + when = now - last[0] + units = 'seconds' + if when > 120: + when /= 60 + units = 'minutes' + if when > 120: + when /= 60 + units = 'hours' + if when > 48: + when /= 24 + units = 'days' + forum.msg('I last saw %s %d %s ago, saying "%s"' % + (whom, when, units, last[1])) + else: + forum.msg("I've never seen %s!" % (whom)) + bindings.append((re.compile(r"^seen +(?P.*)$"), + seen)) + + def evalstr(self, sender, forum, addl, match): + code = match.group('code') + if code in (')', '-)'): + return True + try: + ret = repr(eval(code, {"__builtins__": {}}, {})) + if len(ret) > 400: + ret = ret[:400] + '\026...\026' + except: + t, v, tb = sys.exc_info() + forum.msg(self.gettext('eval', code=code, ret='\002%s\002: %s' % (t, v), sender=sender.name())) + else: + forum.msg(self.gettext('eval', code=code, ret=ret, sender=sender.name())) + #bindings.append((re.compile(r"^\; *(?P.+)$"), evalstr)) + #msg_cat['eval'] = ('%(code)s ==> %(ret)s',) + + def shorturl(self, sender, forum, addl, match): + url = match.group('url') + print ('url', url) + idx = shorturl.add(url) + forum.msg('http://%s:%d/%d' % (URLSERVER[0], URLSERVER[1], idx)) + bindings.append((re.compile(r".*\b(?P\b[a-z]+://[-a-z0-9_=!?#$@~%&*+/:;.,\w]+[-a-z0-9_=#$@~%&*+/\w])"), + shorturl)) + + def cdecl(self, sender, forum, addl, match): + jibberish = match.group('jibberish') + o, i = os.popen2('/usr/bin/cdecl') + o.write(jibberish + '\n') + o.close() + res = i.read().strip() + if '\n' in res: + forum.msg("Lots of output, sending in private message") + self.despool(sender, res.split('\n')) + else: + forum.msg('cdecl | %s' % res) + bindings.append((re.compile(r"^cdecl (?P.*)$"), + cdecl)) + + def delayed_say(self, sender, forum, addl, match): + delay = int(match.group('delay')) + unit = match.group('unit') + what = match.group('what') + + if not unit or unit[0] == 's': + pass + elif unit[0] == 'm': + delay *= 60 + elif unit[0] == 'h': + delay *= 3600 + elif unit[0] == 'd': + delay *= 86400 + elif unit[0] == 'w': + delay *= 604800 + else: + forum.msg("I don't know what a %s is." % unit) + return + + self.add_timer(delay, lambda : forum.msg(what)) + forum.msg(self.gettext('okay', sender=sender.name())) + bindings.append((re.compile(r"^\008[:, ]+in (?P[0-9]+) ?(?P[a-z]*) say (?P.*)"), + delayed_say)) + + msg_cat['nodict'] = ("Sorry, boss, dict returns no lines for %(jibberish)s",) + def dict(self, sender, forum, addl, match): + jibberish = match.group('jibberish') + i = os.popen('/usr/bin/dict %s 2>&1' % esc(jibberish)) + res = i.readlines() + if not res: + forum.msg(self.gettext('nodict', jibberish=jibberish)) + return + res = [l.strip() for l in res] + if match.group('long'): + self.despool(sender, res) + else: + if len(res) <= 5: + self.despool(forum, res) + else: + del res[:4] + short = res[:] + while short and ((not short[0]) or (short[0][0] not in '0123456789')): + del short[0] + if not short: + short = res + short = ['%s: %s' % (jibberish, r) for r in short[:4]] + self.despool(forum, short + ['[truncated: use the --long option to see it all]']) + bindings.append((re.compile(r"^dict (?P--?l(ong)? +)?(?P.*)$"), + dict)) + + def units(self, sender, forum, addl, match): + f = match.group('from') + t = match.group('to') + if f.startswith('a '): + f = '1 ' + f[2:] + Runner('/usr/bin/units -v %s %s' % (esc(f), esc(t)), + lambda l,r: self.proc_cb(None, sender, forum, l, r)) + bindings.append((re.compile(r"^units +(?P.*) +in +(?P.*)$"), + units)) + bindings.append((re.compile(r"^how many (?P.*) in (?P[^?]*)[?.!]*$"), + units)) + + def calc(self, sender, forum, addl, match): + e = match.group('expr') + Runner("echo %s | /usr/bin/bc -l" % procbot.esc(e), + lambda l,r: self.proc_cb('%s = ' % e, sender, forum, l, r)) + bindings.append((re.compile(r"^(?P[0-9.]+\s*[-+*/^%]\s*[0-9.]+)$"), + calc)) + bindings.append((re.compile(r"^calc (?P.+)$"), + calc)) + + def generic_cmd(self, sender, forum, addl, match): + cmd = match.group('cmd') + args = match.group('args').split(' ') + argstr = ' '.join(procbot.lesc(args)) + Runner('%s %s' % (cmd, argstr), + lambda l,r: self.proc_cb(None, sender, forum, l, r)) + bindings.append((re.compile(r"^(?Phost) (?P.+)$"), + generic_cmd)) + bindings.append((re.compile(r"^(?Pwhois) (?P.+)$"), + generic_cmd)) + + def pollen(self, sender, forum, addl, match): + forecast_re = re.compile('fimages/std/(?P[0-9]+\.[0-9])\.gif') + predom_re = re.compile('Predominant pollen: (?P[^<]*)') + zip = match.group('zip') + def cb(lines): + forecast = [] + predom = '' + for line in lines: + match = forecast_re.search(line) + if match: + forecast.append(match.group('count')) + match = predom_re.search(line) + if match: + predom = match.group('pollens') + forum.msg('%s: 4-day forecast (out of 12.0): %s; predominant pollen: %s' % + (zip, ', '.join(forecast), predom)) + WebRetriever('http://www.pollen.com/forecast.asp?PostalCode=%s&Logon=Enter' % zip, + cb) + bindings.append((re.compile('pollen (?P[0-9]{5})'), + pollen)) + + def weather(self, sender, forum, addl, match): + zip = match.group('zip') + def cb(lines): + print lines + forum.msg('*HURR*') + WebRetriever('http://www.srh.noaa.gov/zipcity.php?inputstring=%s' % zip, + cb) + bindings.append((re.compile('weather (?P[0-9]{5})'), + weather)) + + def quote(self, sender, forum, addl, match): + def cb(lines): + if not lines: + forum.msg('oops, no data from server') + return + c = csv.reader([lines[0].strip()]) + vals = zip(('symbol', 'value', 'day', 'time', 'change', + 'open', 'high', 'low', 'volume', + 'market cap', 'previous close', + 'percent change', 'open2', 'range', + 'eps', 'pe_ratio', 'name'), + c.next()) + d = dict(vals) + forum.msg(('%(name)s (%(symbol)s)' + ' last:%(value)s@%(time)s' + ' vol:%(volume)s' + ' cap:%(market cap)s' + ' prev-close:%(previous close)s' + ' chg:%(change)s(%(percent change)s)' + ' open:%(open)s' + ' 1d:%(low)s - %(high)s' + ' 52wk:%(range)s') % + d) + + symbol = match.group('symbol') + WebRetriever('http://quote.yahoo.com/d/quotes.csv?s=%s&f=sl1d1t1c1ohgvj1pp2owern&e=.csv' % symbol, + cb) + bindings.append((re.compile(r"^quote +(?P[.a-zA-Z]+)$"), + quote)) + + def currency(self, sender, forum, addl, match): + amt = float(match.group('amt')) + frm = match.group('from') + to = match.group('to') + + def cb(lines): + if not lines: + forum.msg('oops, no data from server') + return + c = csv.reader([lines[0].strip()]) + vals = zip(('symbol', 'value', 'day', 'time', 'change', + 'open', 'high', 'low', 'volume', + 'market cap', 'previous close', + 'percent change', 'open2', 'range', + 'eps', 'pe_ratio', 'name'), + c.next()) + d = dict(vals) + v = float(d['value']) + ans = v * amt + forum.msg(('%0.4f %s = %0.4f %s') % + (amt, frm, ans, to)) + + WebRetriever(('http://quote.yahoo.com/d/quotes.csv?s=%s%s%%3DX&f=sl1d1t1c1ohgvj1pp2owern&e=.csv' % + (frm, to)), + cb) + bindings.append((re.compile(r"^how much is (?P[0-9.]+) ?(?P[A-Z]{3}) in (?P[A-Z]{3})\??$"), + currency)) + + def whuffie_mod(self, nick, amt): + vs = self.get(nick, "0", special="whuffie") + try: + val = int(vs) + except: + val = 0 + val += amt + self.set(nick, [str(val)], special="whuffie") + + def whuffie_modify(self, sender, forum, addl, match): + nick = match.group('nick') + if nick.lower() == sender.name().lower(): + forum.msg(self.gettext('whuffie whore', sender=sender.name())) + return + if match.group('mod') == '++': + amt = 1 + else: + amt = -1 + self.whuffie_mod(nick, amt) + bindings.append((re.compile(r"^(?P\w+)(?P\+\+|\-\-)[? ]*$"), + whuffie_modify)) + msg_cat['whuffie whore'] = ("Nothing happens.", + 'A hollow voice says, "Fool."') + + def whuffie(self, sender, forum, addl, match): + nick = match.group('nick') + val = self.get(nick, special="whuffie") + if val and val != "0": + forum.msg("%s has whuffie of %s" % (nick, val)) + else: + forum.msg("%s has neutral whuffie" % nick) + bindings.append((re.compile(r"^(\008[,:] +)?([Ww]huffie|[Kk]arma) (for )?(?P\w+)[? ]*$"), + whuffie)) + + # + # This is all stuff that should just be stored in the usual manner. + # But I wrote it here before I realized how programmable an Infobot + # really is, so here it stays. + # + + msg_cat['8ball'] = ("%(sender)s: Outlook good.", + "%(sender)s: Outlook not so good.", + "%(sender)s: My reply is no.", + "%(sender)s: Don't count on it.", + "%(sender)s: You may rely on it.", + "%(sender)s: Ask again later.", + "%(sender)s: Most likely.", + "%(sender)s: Cannot predict now.", + "%(sender)s: Yes.", + "%(sender)s: Yes, definitely.", + "%(sender)s: Better not tell you now.", + "%(sender)s: It is certain.", + "%(sender)s: Very doubtful.", + "%(sender)s: It is decidedly so.", + "%(sender)s: Concentrate and ask again.", + "%(sender)s: Signs point to yes.", + "%(sender)s: My sources say no.", + "%(sender)s: Without a doubt.", + "%(sender)s: Reply hazy, try again.", + "%(sender)s: As I see it, yes.") + msg_cat['me'] = ('%(sender)s?', + '%(sender)s: Yes?', + 'At your service, %(sender)s.', + 'May I help you, %(sender)s?') + msg_cat['thanks'] = ('It is my pleasure, %(sender)s.', + 'Of course, %(sender)s.', + 'I live but to serve, %(sender)s.', + "All in a day's work, %(sender)s.") + bindings.append((re.compile(r"^(magic )?(8|eight ?)-?ball", re.IGNORECASE), + '8ball')) + bindings.append((re.compile(r"^\008\?$", re.IGNORECASE), + 'me')) + bindings.append((re.compile(r"^thank(s| you),? *\008", re.IGNORECASE), + 'thanks')) + + msg_cat.update(infobot.InfoBot.msg_cat) + bindings.extend(infobot.InfoBot.bindings) + + +if __name__ == "__main__": + import irc + + # Short URL server + us = shorturl.start(('', 0)) + URLSERVER = (socket.gethostbyaddr(socket.gethostname())[0], + us.getsockname()[1]) + + + NICK = ['hal'] + INFO = 'Daisy, Daisy...' + + l1 = FireBot(("server1", 6667), + NICK, + INFO, + ["#ch1", "#ch2"]) + l2 = FireBot(('server2', 6667), + NICK, + INFO, + ["#ch3"]) + l1.set_others([l2]) + l2.set_others([l1]) + + irc.run_forever(0.5) diff --git a/gallium.py b/gallium.py new file mode 100755 index 0000000..10285d0 --- /dev/null +++ b/gallium.py @@ -0,0 +1,114 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +import firebot +import irc +import re +import os +import random +from procbot import ProcBot, Runner + +def esc(arg): + return "'" + arg.replace("'", r"'\''") + "'" + +def lesc(args): + return [esc(arg) for arg in args] + +class Gallium(firebot.FireBot, ProcBot): + opall = False + bindings = [] + + def cmd_invite(self, sender, forum, addl): + # Join any channel to which we're invited + self.write('JOIN', forum.name()) + + def cmd_join(self, sender, forum, addl): + #firebot.FireBot.cmd_join(self, sender, forum, addl) + if self.opall: + if sender.name() == self.nick: + # If it was me, get a channel listing and beg for ops + self.write('WHO %s' % (forum.name())) + forum.notice('If you op me, I will op everyone who joins this channel.') + else: + # Otherwise, op the user + forum.write(['MODE', forum.name(), '+o'], sender.name()) + + def cmd_352(self, sender, forum, addl): + # Response to WHO + forum = irc.Channel(self, addl[0]) + who = irc.User(self, addl[4], addl[1], addl[2]) + self.add_luser(who, forum) + + def server_status(self, sender, forum, addl, match): + loadavg = file('/proc/loadavg').read().strip() + io_status = file('/proc/io_status').read().strip() + forum.msg('%s; load %s' % (io_status, loadavg)) + bindings.append((re.compile(r"^\008[:, ]+server status"), + server_status)) + + def unsafe_eval(self, sender, forum, addl, match): + if self.debug: + txt = match.group(1) + r = eval(txt) + forum.msg('%s: %r' % (sender.name(), r)) + bindings.append((re.compile(r"^\008[:, ]+eval (.*)$"), + unsafe_eval)) + + def randglyph(self, sender, forum, addl, match): + count = 0 + tries = [] + while count < 6: + i = random.randint(0, 0xffff) + k = 'U+%04x' % i + tries.append(k) + r = self.get(k) + if r: + forum.msg('%s %s' % (k, r)) + return + count += 1 + forum.msg("Nothing found (tried %s)" % tries) + bindings.append((re.compile(r"^u\+rand$"), + randglyph)) + + def runcmd(self, sender, forum, addl, match): + command = match.group('command') + args = match.group('args').split(' ') + args = [x.replace("'", "'\\''") for x in args] + argstr = ' '.join(args) + Runner('%s %s' % (command, argstr), + lambda l,r: self.proc_cb('%s: ' % command, sender, forum, l, r)) + bindings.append((re.compile(r"^(?Pwhois) +(?P.*)$"), + runcmd)) + bindings.append((re.compile(r"^(?Phost) +(?P.*)$"), + runcmd)) + + bindings.extend(firebot.FireBot.bindings) + + +if __name__ == '__main__': + import shorturl + import socket + import daemon + import sys + + debug = False + if "-d" in sys.argv: + debug = True + + if not debug: + # Become a daemon + log = file('gallium.log', 'a') + daemon.daemon('gallium.pid', log, log) + + # Short URL server + us = shorturl.start(('', 0)) + firebot.URLSERVER = (socket.gethostbyaddr(socket.gethostname())[0], + us.getsockname()[1]) + + gallium = Gallium(('fozzie.woozle.org', 6667), + ['gallium'], + "I'm a little printf, short and stdout", + ["#woozle", "#gallium"]) + gallium.debug = debug + + irc.run_forever(0.5) diff --git a/health.sh b/health.sh new file mode 100755 index 0000000..dccf8a8 --- /dev/null +++ b/health.sh @@ -0,0 +1,7 @@ +#! /bin/sh +## Restart the bot if it's not running + +# Gallium assumes everything's in the cwd +cd /home/neale/src/firebot + +kill -0 `cat gallium.pid` 2>/dev/null || ./gallium.py diff --git a/infobot.py b/infobot.py new file mode 100644 index 0000000..b6ffcdc --- /dev/null +++ b/infobot.py @@ -0,0 +1,301 @@ +from bindingsbot import BindingsBot +import re +import irc +import seedyb +import time + +class InfoBot(BindingsBot): + """A cheap knock-off of the famous InfoBot. + + """ + + msg_cat = {} + msg_cat.update(BindingsBot.msg_cat) + bindings = [] + + def __init__(self, host, nicks, gecos, channels, dbname='info.cdb'): + BindingsBot.__init__(self, host, nicks, gecos, channels) + self._db = seedyb.open(dbname) + self.seen = {} + self.ignore_case = True + + def sync(self): + now = time.time() + self._db.sync() + + def close(self): + self.sync() + BindingsBot.close(self) + + def cmd_ping(self, sender, forum, addl): + BindingsBot.cmd_ping(self, sender, forum, addl) + self.sync() + + + msg_cat['unknown'] = ("I don't know anything about %(key)s, %(sender)s.",) + msg_cat['stats'] = ("I know about %(things)s things.",) + msg_cat['is'] = ("Rumor has it that %(key)s is %(val)s", + "I believe %(key)s is %(val)s", + "My sources tell me %(key)s is %(val)s", + 'Gosh, %(sender)s, I think %(key)s is %(val)s', + "%(key)s is %(val)s") + msg_cat['_is_'] = ("%(key)s is %(val)s",) + msg_cat['dunno'] = ("Search me, %(sender)s.", + "I have no earthly idea, %(sender)s.", + "I wish I knew, %(sender)s.") + msg_cat['same'] = ('I already had it that way, %(sender)s.', + "That's what I have for %(key)s too, %(sender)s.") + msg_cat['but'] = ('...but %(key)s is %(old)s',) + msg_cat['locked'] = ('Sorry, %(sender)s, %(key)s is locked.',) + msg_cat['tell'] = ('%(sender)s wants you to know: %(string)s',) + msg_cat['synced'] = ('Synchronized in %(time)f seconds.',) + + + def do_sync(self, sender, forum, addl, match): + now = time.time() + self.sync() + forum.msg(self.gettext('synced', + sender=sender.name(), + time=(time.time() - now))) + bindings.append((re.compile(r"^\008[,: ]+(sync|synchronize|flush)$"), + do_sync)) + + def encode_key(self, key): + if self.ignore_case: + key = key.lower() + return key + + def get(self, key, *args, **kwargs): + return self._db.get(self.encode_key(key), *args, **kwargs) + + def getall(self, key, **kwargs): + return self._db.getall(self.encode_key(key), **kwargs) + + def set(self, key, val, **kwargs): + return self._db.set(self.encode_key(key), val, **kwargs) + + def delete(self, key, **kwargs): + self._db.delete(self.encode_key(key), **kwargs) + + def lock(self, key): + return self._db.lock(self.encode_key(key)) + + def unlock(self, key): + return self._db.unlock(self.encode_key(key)) + + def stats(self, sender, forum, addl, match): + forum.msg(self.gettext('stats', things=len(self._db))) + bindings.append((re.compile(r"^\008[,: ]+statu?s$"), + stats)) + + + # Delete part of an entry + def forget_from(self, sender, forum, key, substr): + val = self.getall(key) + if not val: + raise KeyError() + + possibilities = [] + newval = [] + for i in val: + if substr in i: + possibilities.append(i) + else: + newval.append(i) + + if len(possibilities) == 1: + try: + self.set(key, tuple(newval)) + except seedyb.Locked: + forum.msg(self.gettext('locked', key=key, + sender=sender.name())) + return + forum.msg(self.gettext('forgot', + key=key, + val=possibilities[0], + sender=sender.name())) + elif len(possibilities) == 0: + forum.msg(self.gettext('not in', + key=key, + substr=substr, + sender=sender.name())) + else: + forum.msg(self.gettext('ambiguous forget', + key=key, + substr=substr, + num=len(possibilities), + sender=sender.name())) + msg_cat['not in'] = ("I don't see any entries for %(key)s containing %(substr)s, %(sender)s",) + msg_cat['ambiguous forget'] = ("There are %(num)d matches for %(substr)s in %(key)s. Try a more specific substring!",) + msg_cat['forgot'] = ('Okay, %(sender)s, I forgot \"%(val)s\" from \"%(key)s\".',) + + # Delete an entry + def forget(self, sender, forum, addl, match): + key = match.group('key') + ekey = self.encode_key(key) + try: + self.delete(ekey) + forum.msg(self.gettext('okay', key=key, sender=sender.name())) + except KeyError: + if ' from ' in key: + substr, k = key.split(' from ', 1) + try: + return self.forget_from(sender, forum, k, substr) + except KeyError: + pass + forum.msg(self.gettext('unknown', key=key, sender=sender.name())) + except seedyb.Locked: + forum.msg(self.gettext('locked', key=key, + sender=sender.name())) + bindings.append((re.compile(r"^\008[,: ]+forget (?P.+)$", re.IGNORECASE), + forget)) + + # Lock an entry + def lock_entry(self, sender, forum, addl, match): + key = match.group('key') + self.lock(key) + forum.msg(self.gettext('okay', key=key, sender=sender.name())) + bindings.append((re.compile(r"^\008[,: ]+lock (?P.+)$", re.IGNORECASE), + lock_entry)) + + # Unlock an entry + def unlock_entry(self, sender, forum, addl, match): + key = match.group('key') + self.unlock(key) + forum.msg(self.gettext('okay', key=key, sender=sender.name())) + bindings.append((re.compile(r"^\008[,: ]+unlock (?P.+)$", re.IGNORECASE), + unlock_entry)) + + # Literal entry + def literal(self, sender, forum, addl, match): + key = match.group('key') + val = self.getall(key) + if val: + sv = `val` + out = [] + while len(sv) > 300: + s = sv[:300] + sv = sv[300:] + out.append('db[%r] == %s ...' % (key, s)) + out.append('db[%r] == %s' % (key, sv)) + self.despool(forum, out) + else: + forum.msg(self.gettext('unknown', key=key, sender=sender.name())) + bindings.append((re.compile(r"^\008[,: ]+literal (?P.+)$", re.IGNORECASE), + literal)) + + # Look something up in the DB + def lookup(self, sender, forum, addl, match): + key = match.group('key') + + # Try looking it up verbatim + val = self.get(key) + if not val: + # Try the cleaned version + key = key.rstrip('.?! ') + val = self.get(key) + if val: + val = val % {'me': self.nick, + 'forum': forum.name(), + 'sender': sender.name()} + if len(val) > 300: + val = val[:297] + '...' + if val[0] == '\\': + forum.msg(val[1:]) + elif val[0] == ':': + forum.act(val[1:]) + else: + forum.msg(self.gettext('is', key=key, val=val, sender=sender.name())) + elif match.group('me'): + forum.msg(self.gettext('dunno', key=key, sender=sender.name())) + elif match.group('question'): + # Don't allow storage of things like 'what is that?' + pass + else: + return True + + def do_store(self, sender, forum, key, val, me, no, also): + resp = False + old = self.getall(key) + okay = self.gettext('okay', sender=sender.name()) + if old: + if val in old: + if me: + resp = self.gettext('same', key=key, val=val, old=old, + sender=sender.name()) + else: + # Ignore duplicates + resp = self.gettext('same', key=key, val=val, old=old, + sender=sender.name()) + pass + elif me: + if also: + self.set(key, old + [val]) + resp = okay + elif no: + self.set(key, [val]) + resp = okay + else: + if len(old) == 1: + old = old[0] + resp = self.gettext('but', key=key, val=val, old=old, + sender=sender.name()) + else: + self.set(key, old + [val]) + resp = okay + else: + self.set(key, (val,)) + resp = okay + + if resp: + if me: + forum.msg(resp) + return False + return True + + # Write a new value to the DB + def store(self, sender, forum, addl, match): + key = match.group('key') + val = match.group('val') + # Change % to %%, except for %( + val = val.replace('%', '%%') + val = val.replace('%(', '(') + me = match.group('me') + no = match.group('no') and me + also = val.startswith('also ') + if also: + val = val[5:] + return self.do_store(sender, forum, key, val, me, no, also) + + def append_cmd(self, sender, forum, addl, match): + key = match.group('key') + val = match.group('val') + return self.do_store(sender, forum, key, val, me=True, no=True, also=True) + + # Pull in BindingsBot things + bindings.extend(BindingsBot.bindings) + + # This is first to prevent storing "firebot: what is foo?" + bindings.append((re.compile(r"^(?P\008[,: ]+)?(?P(what|who|where|wtf).*('s|'re| is| are) )(?P.+)$", + re.IGNORECASE), + lookup)) + bindings.append((re.compile(r"^(?P\008[,: ]+)append (?P.+) <= (?P.+)", + re.IGNORECASE), + append_cmd)) + bindings.append((re.compile((r"^(?P\008)[,: ]+(?Pno, *)" + r"(?P.+?) (is|are) (?P.+)$"), + re.IGNORECASE), + store)) + bindings.append((re.compile((r"^(?Pno,? *)?(?P\008)[,: ]+" + r"(?P.+?) (is|are) (?P.+)$"), + re.IGNORECASE), + store)) + bindings.append((re.compile(r"^([^:, ]+[:,] *)?(?P)(?P)(?P.+) (is|are) (?P.+)$", + re.IGNORECASE), + store)) + bindings.append((re.compile(r"^(?P\008[,: ]+)?(?P)(?P.+)$", + re.IGNORECASE), + lookup)) + + + diff --git a/irc.py b/irc.py new file mode 100644 index 0000000..67d3887 --- /dev/null +++ b/irc.py @@ -0,0 +1,604 @@ +import asynchat +import asyncore +import socket +import string +import types +import sys +import traceback +import time + +class IRCHandler(asynchat.async_chat): + """IRC Server connection. + + This is the one you want to derive your connection classes from. + + """ + + debug = False + heartbeat_interval = 1 # seconds per heartbeat + + def __init__(self, host=None, nick=None, gecos=None): + asynchat.async_chat.__init__(self) + self.line = '' + self.timers = [] + self.last_heartbeat = 0 + self.set_terminator('\r\n') + if host: + self.open_connection(host, nick, gecos) + + def dbg(self, msg): + if self.debug: + print msg + + def open_connection(self, host, nick, gecos): + self.nick = nick + self.gecos = gecos + self.host = host + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect(host) + + def handle_connect(self): + self.write(['NICK', self.nick]) + self.write(['USER', self.nick, '+iw', self.nick], self.gecos) + + def connect(self, host): + self.waiting = False + asynchat.async_chat.connect(self, host) + + def heartbeat(self): + """Invoke all timers.""" + + if not self.timers: + return + timers, self.timers = self.timers, [] + now = time.time() + for t, cb in timers: + if t > now: + self.timers.append((t, cb)) + else: + cb() + + def add_timer(self, secs, callback): + """After secs seconds, call callback""" + self.timers.append((time.time() + secs, callback)) + + def readable(self): + """Called by asynchat to see if we're readable. + + We hook our heartbeat in here. + """ + + now = time.time() + if now > self.last_heartbeat + self.heartbeat_interval: + self.heartbeat() + self.last_heartbeat = now + + if self.connected: + return asynchat.async_chat.readable(self) + else: + return False + + def collect_incoming_data(self, data): + """Called by asynchat when data arrives""" + self.line += data + + def found_terminator(self): + """Called by asynchat when it finds the terminating character. + """ + line = self.line + self.line = '' + self.parse_line(line) + + def write(self, args, *text): + """Send out an IRC command + + This function helps to prevent you from shooting yourself in the + foot, by forcing you to send commands that are in a valid format + (although it doesn't check the validity of the actual commands). + + As we all know, IRC commands take the form + + :COMMAND ARG1 ARG2 ARG3 ... :text string + + where 'text string' is optional. Well, that's exactly how this + function works. Args is a list of length at least one, and text + string can be any number of strings. You can call the function + like this: + + write(['PRIVMSG', nick], 'Hello 12') + + or like this: + + write(['PRIVMSG', nick], 'Hello', '12') + + or even like this: + + write(['PRIVMSG', nick], 'Hello', 12) + + And you'll get the same result with all three. + + """ + + if (type(args) == types.StringType): + cmd = args + else: + cmd = u' '.join(args) + cmdstr = cmd + if (text): + txt = '' + for t in (text): + if type(t) in (types.StringType, types.UnicodeType): + txt = txt + ' ' + t + elif type(t) in (types.ListType, types.TupleType): + for i in (t): + try: + txt = ' '.join([txt, i]) + except TypeError: + txt = ' '.join([txt, repr(i)]) + else: + txt = ' '.join([txt, repr(t)]) + txt = txt[1:] + cmdstr = "%s :%s" % (cmdstr, txt) + encstr = cmdstr.encode('utf8', 'replace') + self.dbg("-> %s " % encstr) + try: + self.send(encstr + '\n') + except socket.error: + pass + + + def parse_line(self, line): + """Parse a server-provided line + + This does all the magic of parsing those ill-formatted IRC + messages. It will also decide if a PRIVMSG or NOTICE is using + CTCP (the client-to-client protocol, which by convention is any + of the above messages with ^A on both ends of the text. + + This function goes on to invoke self.eval_triggers on the parsed + data like this: + + self.eval_triggers(operation, arguments, text) + + where operation and text are strings, and arguments is a list. + + It returns the same tuple (op, args, text). + + """ + line = line.decode('utf8', 'replace') + if (line[0] == ':'): + with_uname = 1 + line = line [1:] + else: + with_uname = 0 + try: + [args, text] = line.split(' :', 1) + args = args.split() + except ValueError: + args = line.split() + text = '' + if (with_uname != 1): + op = args[0] + elif ((args[1] in ["PRIVMSG", "NOTICE"]) and + (text and (text[0] == '\001') and (text[-1] == '\001'))): + op = "C" + args[1] + text = text[1:-1] + else: + op = args[1] + self.dbg("<- %s %s %s" % (op, args, text)) + self.handle(op, args, text) + return (op, args, text) + + + def handle(self, op, args, text): + """Take action on a server message + + Right now, just invokes + + self.do_[op](args, text) + + where [op] is the operation passed in. + + This is a good method to overload if you want a really advanced + client supporting bindings. + + """ + try: + method = getattr(self, "do_" + lower(op)) + except AttributeError: + self.dbg("Unhandled: %s" % (op, args, text)) + return + method(args, text) + + +class Recipient: + """Abstract recipient object""" + + def __init__(self, interface, name): + self._interface = interface + self._name = name + + def __repr__(self): + return 'Recipient(%s)' % self.name() + + def name(self): + return self._name + + def is_channel(self): + return False + + def write(self, cmd, addl): + """Write a raw IRC command to our interface""" + + self._interface.write(cmd, addl) + + def cmd(self, cmd, text): + """Send a command to ourself""" + + self.write([cmd, self._name], text) + + def msg(self, text): + """Tell the recipient something""" + + self.cmd("PRIVMSG", text) + + def notice(self, text): + """Send a notice to the recipient""" + + self.cmd("NOTICE", text) + + def ctcp(self, command, text): + """Send a CTCP command to the recipient""" + + return self.msg("\001%s %s\001" % (upper(command), text)) + + def act(self, text): + """Send an action to the recipient""" + + return self.ctcp("ACTION", text) + + def cnotice(self, command, text): + """Send a CTCP notice to the recipient""" + + return self.notice("\001%s %s\001" % (upper(command), text)) + +class Channel(Recipient): + def __repr__(self): + return 'Channel(%s)' % self.name() + + def is_channel(self): + return True + +class User(Recipient): + def __init__(self, interface, name, user, host, op=False): + Recipient.__init__(self, interface, name) + self.user = user + self.host = host + self.op = op + + def __repr__(self): + return 'User(%s, %s, %s)' % (self.name(), self.user, self.host) + +def recipient(interface, name): + if name[0] in ["&", "#"]: + return Channel(interface, name) + else: + return User(interface, name, None, None) + +class SmartIRCHandler(IRCHandler): + """This is like the IRCHandler, except it creates Recipient objects + for IRC messages. The intent is to make it easier to write stuff + without knowledge of the IRC protocol. + + """ + + def err(self, exception): + if self.debug: + traceback.print_exception(*exception) + + def handle(self, op, args, text): + """Parse more, creating objects and stuff + + makes a call to self.handle_op(sender, forum, addl) + + sender is always a Recipient object; if you want to reply + privately, you can send your reply to sender. + + forum is a Recipient object corresponding with the forum over + which the message was carried. For user-to-user PRIVMSG and + NOTICE commands, this is the same as sender. For those same + commands sent to a channel, it is the channel. Thus, you can + always send a reply to forum, and it will be sent back in an + appropriate manner (ie. the way you expect). + + addl is a tuple, containing additional information which might + be relelvant. Here's what it will contain, based on the server + operation: + + op | addl + ---------+---------------- + PRIVMSG | text of the message + NOTICE | text of the notice + CPRIVMSG | CTCP command, text of the command + CNOTICE | CTCP response, text of the response + KICK * | victim of kick, kick text + MODE * | all mode args + JOIN * | empty + PART * | empty + QUIT | quit message + PING | ping text + NICK ! | old nickname + others | all arguments; text is last element + + * The forum in these items is the channel to which the action + pertains. + ! The sender for the NICK command is the *new* nickname. This + is so you can send messages to the sender object and they'll + go to the right place. + """ + + try: + sender = User(self, *unpack_nuhost(args)) + except ValueError: + sender = None + forum = None + addl = () + + if op in ("PRIVMSG", "NOTICE"): + # PRIVMSG ['neale!~user@127.0.0.1', 'PRIVMSG', '#hydra'] firebot, foo + # PRIVMSG ['neale!~user@127.0.0.1', 'PRIVMSG', 'firebot'] firebot, foo + try: + if args[2][0] in '#&': + forum = recipient(self, args[2]) + else: + forum = sender + addl = (text,) + except IndexError: + addl = (text, args[1]) + elif op in ("CPRIVMSG", "CNOTICE"): + forum = recipient(self, args[2]) + splits = text.split(" ") + if splits[0] == "DCC": + op = "DC" + op + addl = (splits[1],) + tuple(splits[2:]) + else: + addl = (splits[0],) + tuple(splits[1:]) + elif op in ("KICK",): + forum = recipient(self, args[2]) + addl = (recipient(self, args[3]), text) + elif op in ("MODE",): + forum = recipient(self, args[2]) + addl = args[3:] + elif op in ("JOIN", "PART"): + try: + forum = recipient(self, args[2]) + except IndexError: + forum = recipient(self, text) + elif op in ("QUIT",): + addl = (text,) + elif op in ("PING", "PONG"): + # PING ['PING'] us.boogernet.org. + # PONG ['irc.foonet.com', 'PONG', 'irc.foonet.com'] 1114199424 + addl = (text,) + elif op in ("NICK",): + # NICK ['brad!~brad@10.168.2.33', 'NICK'] bradaway + # + # The sender is the new nickname here, in case you want to + # send something to the sender. + + # Apparently there are two different standards for this + # command. + if text: + sender = recipient(self, text) + else: + sender = recipient(self, args[2]) + addl = (unpack_nuhost(args)[0],) + elif op in ("INVITE",): + forum = recipient(self, text) + else: + try: + int(op) + except ValueError: + self.dbg("WARNING: unknown server code: %s" % op) + addl = tuple(args[3:]) + (text,) + + try: + self.handle_cooked(op, sender, forum, addl) + except SystemExit: + raise + except: + self.err(sys.exc_info()) + + def handle_cooked(self, op, sender, forum, addl): + try: + func = getattr(self, 'cmd_' + op.lower()) + except AttributeError: + self.unhandled(op, sender, forum, addl) + return + func(sender, forum, addl) + + def cmd_ping(self, sender, forum, addl): + self.write('PONG', addl) + + def unhandled(self, op, sender, forum, addl): + """Handle all the stuff that had no handler. + + This is a special handler in that it also gets the server code + as the first argument. + + """ + + self.dbg("unhandled: %s" % ((op, sender, forum, addl),)) + + +class Bot(SmartIRCHandler): + """A simple bot. + + This automatically joins the channels you pass to the constructor, + tries to use one of the nicks provided, and reconnects if it gets + booted. You can use this as a base for more sophisticated bots. + + """ + + def __init__(self, host, nicks, gecos, channels): + self.nicks = nicks + self.channels = channels + self.waiting = True + self._spool = [] + SmartIRCHandler.__init__(self, host, nicks[0], gecos) + + def despool(self, target, lines): + """Slowly despool a bunch of lines to a target + + Since the IRC server will block all output if we send it too + fast, use this to send large multi-line responses. + + """ + + self._spool.append((target, list(lines))) + + def heartbeat(self): + SmartIRCHandler.heartbeat(self) + + # Despool data + if self._spool: + # Take the first one on the queue, and put it on the end + which = self._spool[0] + del self._spool[0] + self._spool.append(which) + + # Despool a line + target, lines = which + line = lines[0] + target.msg(line) + del lines[0] + if not lines: + self._spool.remove((target, lines)) + + def announce(self, text): + for c in self.channels: + self.write(['PRIVMSG', c], text) + + def err(self, exception): + SmartIRCHandler.err(self, exception) + self.announce('*bzert*') + + def cmd_001(self, sender, forum, addl): + for c in self.channels: + self.write('JOIN', c) + + def writable(self): + if not self.waiting: + return asynchat.async_chat.writable(self) + else: + return False + + def write(self, *args): + SmartIRCHandler.write(self, *args) + + def close(self, final=False): + SmartIRCHandler.close(self) + if not final: + self.dbg("Connection closed, reconnecting...") + self.waiting = True + self.connected = 0 + # Wait a bit and reconnect + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.add_timer(23, lambda : self.connect(self.host)) + + + def handle_close(self): + self.close() + + +## +## Miscellaneous IRC functions +## + +ucletters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ[]\\" +lcletters = "abcdefghijklmnopqrstuvwxyz{}|" + +ultrans = string.maketrans(ucletters, lcletters) +lutrans = string.maketrans(lcletters, ucletters) +casetrans = string.maketrans(''.join([ucletters, lcletters]), + ''.join([lcletters, ucletters])) + +def upper(s): + """Convert a string to upper case. + + Because IRC was developed in a nordic country, there are three extra + letters. In order to do case conversions properly, you need to use + the IRC-specific functions. + + """ + + return string.translate(s, lutrans) + +def lower(s): + """Convert a string to lower case + + Because IRC was developed in a nordic country, there are three extra + letters. In order to do case conversions properly, you need to use + the IRC-specific functions. + + """ + + return string.translate(s, ultrans) + +def swapcase(s): + """Invert a string's case + + Because IRC was developed in a nordic country, there are three extra + letters. In order to do case conversions properly, you need to use + the IRC-specific functions. + + """ + + return string.translate(s, casetrans) + + +def strcmp(s1, s2): + """Case-insensitively compare two strings + + Because IRC was developed in a nordic country, there are three extra + letters. In order to do case-insensitive comparisons properly, you + need to use this IRC-specific function. + + """ + + return (lower(s1) == lower(s2)) + + +def unpack_nuhost(nuhost): + """Unpack nick!user@host + + Frequently, the first argument in a server message is in + nick!user@host format. You can just pass your whole argument list + to this function and get back a tuple containing: + + (nick, user, host) + + """ + + try: + [nick, uhost] = string.split(nuhost[0], '!', 1) + [user, host] = string.split(uhost, '@', 1) + except ValueError: + raise ValueError, "not in nick!user@host format" + return (nick, user, host) + +def run_forever(timeout=2.0): + """Run your clients forever. + + Just a handy front-end to asyncore.loop, so you don't have to import + asyncore yourself. + + """ + + if False: + map = asyncore.socket_map + while map: + print map + asyncore.poll(timeout, map) + else: + asyncore.loop(timeout) diff --git a/linkbot.py b/linkbot.py new file mode 100755 index 0000000..ba10785 --- /dev/null +++ b/linkbot.py @@ -0,0 +1,96 @@ +#! /usr/bin/env python + +import irc + +NAME = ['arsenic'] +INFO = "I'm a little teapot, short and stout" + +class MultiChannel(irc.Channel): + """Multiple-channel recipient + + The idea is that this object can represent multiple channels, so + when it's told to do something, it will happen in more than one + place. + + """ + + def __init__(self, ifchans, name): + self._ifchans = ifchans + self._name = name + + def cmd(self, cmd, text): + for iface, chans in self._ifchans: + for chan in chans: + iface.write([cmd, chan], text) + + +class LinkBot(irc.Bot): + """Linkbot stuff. + + The strategy here is to relay messages to the + others, then get the others to act as if they had just seen the + message from their server. + + """ + + def __init__(self, *data): + self.others = [] + self.fora = None + if data: + irc.Bot.__init__(self, *data) + + def handle_cooked(self, op, sender, forum, addl): + """The crux of the linkbot. + + By replacing forum with a multi-channel forum, forum-directed + replies go to all channels. + + """ + + if self.fora and forum and forum.is_channel(): + forum = MultiChannel(self.fora, forum.name()) + irc.Bot.handle_cooked(self, op, sender, forum, addl) + + def set_others(self, others): + self.others = others + self.fora = [] + for i in [self] + others: + self.fora.append((i, i.channels)) + + def broadcast(self, text): + for i in self.others: + i.announce(text) + + def cmd_privmsg(self, sender, forum, addl): + if forum.is_channel(): + self.broadcast('<%s> %s' % (sender.name(), addl[0])) + + def cmd_cprivmsg(self, sender, forum, addl): + if forum.is_channel(): + cmd = addl[0] + text = ' '.join(addl[1:]) + if cmd == 'ACTION': + self.broadcast('* %s %s' % (sender.name(), text)) + + def cmd_nick(self, sender, forum, addl): + self.broadcast(' *** %s is now known as %s' % (addl[0], sender.name())) + + def cmd_join(self, sender, forum, addl): + self.broadcast(' *** %s has joined' % (sender.name())) + + def cmd_part(self, sender, forum, addl): + self.broadcast(' *** %s has left' % (sender.name())) + cmd_quit = cmd_part + +if __name__ == '__main__': + l1 = LinkBot(('209.67.60.33', 6667), + NAME, + INFO, + ['#disney']) + l2 = LinkBot(('woozle.org', 6667), + NAME, + INFO, + ['#woozle']) + l1.set_others([l2]) + l2.set_others([l1]) + irc.run_forever() diff --git a/opbot.py b/opbot.py new file mode 100755 index 0000000..b820470 --- /dev/null +++ b/opbot.py @@ -0,0 +1,38 @@ +#! /usr/bin/env python + +"""OpBot -- Hands out channel ops + +This bot joins every channel on the server, and if opped in a channel +will op anyone who joins. It will poll the server for a channel list +and join any new channels as they appear. Once it has joined, it never +leaves a channel. + +""" + +import irc + +class NopBot(irc.Bot): + #debug = True + heartbeat_interval = 60 + + def cmd_001(self, sender, forum, addl): + irc.Bot.cmd_001(self, sender, forum, addl) + self.write(['LIST']) + + def cmd_322(self, sender, forum, addl): + self.write(['JOIN', addl[0]]) + + def cmd_join(self, sender, forum, addl): + if sender.name() == self.nick: + forum.notice('If you op me, I will op everyone who joins this channel.') + forum.write(['MODE', forum.name(), '+o'], sender.name()) + + def heartbeat(self): + irc.Bot.heartbeat(self) + self.write(['LIST']) + +n = NopBot(('woozle.org', 6667), + ['OpBot'], + 'Op me!', + []) +irc.run_forever() diff --git a/procbot.py b/procbot.py new file mode 100755 index 0000000..b256d26 --- /dev/null +++ b/procbot.py @@ -0,0 +1,74 @@ +#! /usr/bin/env python + +import os +import irc +import async_proc + +class Runner(async_proc.process_dispatcher): + def __init__(self, cmdline, outfunc): + f = os.popen('%s 2>&1' % (cmdline), 'r') + self.outfunc = outfunc + self.linebuf = "" + async_proc.process_dispatcher.__init__(self, f) + + def handle_read(self): + self.linebuf += self.recv(4098) + + def handle_close(self): + ret = self.close() + if self.linebuf: + self.outfunc(self.linebuf, ret) + + +def esc(arg): + "Shell-escape an argument" + + return "'" + arg.replace("'", "'\''") + "'" + + +def lesc(args): + "Shell-escape a list of arguments" + + return [esc(arg) for arg in args] + + +class ProcBot(irc.Bot): + maxlines = 5 + + def proc_cb(self, pfx, sender, forum, linebuf, ret): + if not pfx: + pfx = "" + lines = [] + for line in linebuf.split('\n'): + line = line.strip() + if line: + lines.append("%s%s" % (pfx, line)) + if ret and not lines: + lines = ["%sThat generates an error (%d)." % (pfx, ret)] + if len(lines) > self.maxlines: + forum.msg("%sToo many lines, sending privately" % pfx) + self.despool(sender, lines) + else: + self.despool(forum, lines) + + +if __name__ == '__main__': + import bindingsbot + import re + + class LsBot(ProcBot, bindingsbot.BindingsBot): + bindings = bindingsbot.BindingsBot.bindings + + def ls(self, sender, forum, addl, match): + r = Runner('ls', lambda linebuf, ret: self.proc_cb("ls: ", + sender, forum, + linebuf, ret)) + bindings.append((re.compile(r"^ls", re.IGNORECASE), + ls)) + + + p = LsBot(('irc.woozle.org', 6667), + 'procbot', + 'hi asl', + ["#ch"]) + irc.run_forever() diff --git a/seedyb.py b/seedyb.py new file mode 100644 index 0000000..b9fa9fd --- /dev/null +++ b/seedyb.py @@ -0,0 +1,146 @@ +#! /usr/bin/env python + +import cdb +import random +import codecs + +_encode = codecs.getencoder('utf-8') +(_encode, _decode, _, _) = codecs.lookup('utf-8') +def encode(str): + return _encode(str)[0] + +def decode(str): + return _decode(str)[0] + +class Locked(Exception): + pass + +class SeedyB: + """firebot-specific database using cdb. + + Why CDB? Because everything else keeps going corrupt. + + Notes: + * This doesn't preserve unsynced additions. If you crash before + running sync(), you lose. + * If you set a value to [], is is effectively deleted. + * You can lock something that's not in the database. You might + want to do this for words like 'that', so the bot doesn't pick + up on them. + """ + + def __init__(self, filename): + self.filename = filename + self.tempfile = "%s.tmp" % filename + + self.db = {} + try: + self.cdb = cdb.init(self.filename) + except cdb.error: + d = cdb.cdbmake(self.filename, self.tempfile) + d.finish() + del d + self.cdb = cdb.init(self.filename) + + def __del__(self): + self.sync(force=True) + + def __len__(self): + return len(self.cdb) + len(self.db) + + def __delitem__(self, key): + self.delete(key) + + def __getitem__(self, key): + val = self.get(key) + if val is None: + raise KeyError(key) + + def __contains__(self, key): + return (key in self.db) or (key in self.cdb) + + def length(self): + return (len(self.cdb), len(self.db)) + + def sync(self, force=False): + if not self.db: + return + + tmp = cdb.cdbmake(self.filename, self.tempfile) + + # Copy original + r = self.cdb.each() + while r: + k,v = r + dk = decode(k) + if k not in self.db: + tmp.add(*r) + r = self.cdb.each() + + # Add new stuff + for k,l in self.db.iteritems(): + for v in l: + try: + tmp.add(k,v) + except: + print (k,v) + raise + + tmp.finish() + self.cdb = cdb.init(self.filename) + self.db = {} + + def getall(self, key, special=None): + """Return all values for a key""" + + if special: + key = '\016%s:%s\017' % (special, key) + ekey = encode(key) + vals = self.db.get(ekey, None) + if vals is None: + vals = self.cdb.getall(ekey) + return [decode(v) for v in vals] + + def get(self, key, default=None, special=None): + """Get a value at random""" + + vals = self.getall(key, special) + if vals: + return random.choice(vals) + else: + return default + + def set(self, key, val, special=None): + if special: + key = '\016%s:%s\017' % (special, key) + ekey = encode(key) + if type(val) not in (type([]), type(())): + val = [val] + if not special and self.is_locked(key): + raise Locked() + self.db[ekey] = [encode(v) for v in val] + + def delete(self, key, special=None): + val = self.get(key, special=special) + if not val: + raise KeyError(key) + self.set(key, [], special) + + ## + ## Locking + ## + + def lock(self, key): + self.set(key, [''], special='lock') + + def unlock(self, key): + self.set(key, [], special='lock') + + def is_locked(self, key): + l = self.get(key, special='lock') + if l: + return True + return False + + +open = SeedyB diff --git a/shorturl.py b/shorturl.py new file mode 100755 index 0000000..23a25c4 --- /dev/null +++ b/shorturl.py @@ -0,0 +1,325 @@ +#! /usr/bin/env python2.2 + +import asyncore +import asynchat +import socket +import sys +import time + +__version__ = '1.0' + +DEFAULT_ERROR_MESSAGE = """\ + +Error response + + +

Error response

+

Error code %(code)d. +

Message: %(message)s. +

Error code explanation: %(code)s = %(explain)s. + +""" + +URLS = [] + +class URLServer(asyncore.dispatcher): + def __init__(self, bind, connFactory): + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind(bind) + self.listen(4) + self.connFactory = connFactory + + def handle_accept(self): + conn, addr = self.accept() + self.connFactory(conn) + + +class HTTPHandler(asynchat.async_chat): + def __init__(self, conn): + asynchat.async_chat.__init__(self, conn=conn) + self.client_address = self.getpeername() + self.set_terminator('\r\n\r\n') + self.data = '' + + def collect_incoming_data(self, data): + self.data += data + + def found_terminator(self): + try: + self.headers = self.data.split('\r\n') + self.requestline = self.headers[0] + self.command, self.path, self.request_version = self.requestline.split() + except: + self.send_error(500) + raise + + try: + func = getattr(self, "do_" + self.command) + except AttributeError: + self.send_error(501, "Unsupported method (%s)" % `self.command`) + return + + try: + func() + except: + self.send_error(500) + raise + + self.close() + + + # The Python system version, truncated to its first component. + sys_version = "Python/" + sys.version.split()[0] + + # The server software version. You may want to override this. + # The format is multiple whitespace-separated strings, + # where each string is of the form name[/version]. + server_version = "AsyncoreBaseHTTP/" + __version__ + + # The version of the HTTP protocol we support. + # Don't override unless you know what you're doing (hint: incoming + # requests are required to have exactly this version string). + protocol_version = "HTTP/1.0" + + # Table mapping response codes to messages; entries have the + # form {code: (shortmessage, longmessage)}. + # See http://www.w3.org/hypertext/WWW/Protocols/HTTP/HTRESP.html + responses = { + 200: ('OK', 'Request fulfilled, document follows'), + 201: ('Created', 'Document created, URL follows'), + 202: ('Accepted', + 'Request accepted, processing continues off-line'), + 203: ('Partial information', 'Request fulfilled from cache'), + 204: ('No response', 'Request fulfilled, nothing follows'), + + 301: ('Moved', 'Object moved permanently -- see URI list'), + 302: ('Found', 'Object moved temporarily -- see URI list'), + 303: ('Method', 'Object moved -- see Method and URL list'), + 304: ('Not modified', + 'Document has not changed singe given time'), + + 400: ('Bad request', + 'Bad request syntax or unsupported method'), + 401: ('Unauthorized', + 'No permission -- see authorization schemes'), + 402: ('Payment required', + 'No payment -- see charging schemes'), + 403: ('Forbidden', + 'Request forbidden -- authorization will not help'), + 404: ('Not found', 'Nothing matches the given URI'), + + 500: ('Internal error', 'Server got itself in trouble'), + 501: ('Not implemented', + 'Server does not support this operation'), + 502: ('Service temporarily overloaded', + 'The server cannot process the request due to a high load'), + 503: ('Gateway timeout', + 'The gateway server did not receive a timely response'), + + } + + error_message_format = DEFAULT_ERROR_MESSAGE + + def send_error(self, code, message=None): + """Send and log an error reply. + + Arguments are the error code, and a detailed message. + The detailed message defaults to the short entry matching the + response code. + + This sends an error response (so it must be called before any + output has been generated), logs the error, and finally sends + a piece of HTML explaining the error to the user. + + """ + + try: + short, long = self.responses[code] + except KeyError: + short, long = '???', '???' + if not message: + message = short + explain = long + self.log_error("code %d, message %s", code, message) + self.send_response(code, message) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.send(self.error_message_format % + {'code': code, + 'message': message, + 'explain': explain}) + + def send_response(self, code, message=None): + """Send the response header and log the response code. + + Also send two standard headers with the server software + version and the current date. + + """ + self.log_request(code) + if message is None: + if self.responses.has_key(code): + message = self.responses[code][0] + else: + message = '' + if self.request_version != 'HTTP/0.9': + self.send("%s %s %s\r\n" % + (self.protocol_version, str(code), message)) + self.send_header('Server', self.version_string()) + self.send_header('Date', self.date_time_string()) + + def send_header(self, keyword, value): + """Send a MIME header.""" + if self.request_version != 'HTTP/0.9': + self.send("%s: %s\r\n" % (keyword, value)) + + def end_headers(self): + """Send the blank line ending the MIME headers.""" + if self.request_version != 'HTTP/0.9': + self.send("\r\n") + + def log_request(self, code='-', size='-'): + """Log an accepted request. + + This is called by send_reponse(). + + """ + + self.log_message('"%s" %s %s', + self.requestline, str(code), str(size)) + + def log_error(self, *args): + """Log an error. + + This is called when a request cannot be fulfilled. By + default it passes the message on to log_message(). + + Arguments are the same as for log_message(). + + XXX This should go to the separate error log. + + """ + + apply(self.log_message, args) + + def log_message(self, format, *args): + """Log an arbitrary message. + + This is used by all other logging functions. Override + it if you have specific logging wishes. + + The first argument, FORMAT, is a format string for the + message to be logged. If the format string contains + any % escapes requiring parameters, they should be + specified as subsequent arguments (it's just like + printf!). + + The client host and current date/time are prefixed to + every message. + + """ + + return + sys.stderr.write("%s - - [%s] %s\n" % + (self.address_string(), + self.log_date_time_string(), + format%args)) + + def version_string(self): + """Return the server software version string.""" + return self.server_version + ' ' + self.sys_version + + def date_time_string(self): + """Return the current date and time formatted for a message header.""" + now = time.time() + year, month, day, hh, mm, ss, wd, y, z = time.gmtime(now) + s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( + self.weekdayname[wd], + day, self.monthname[month], year, + hh, mm, ss) + return s + + def log_date_time_string(self): + """Return the current time formatted for logging.""" + now = time.time() + year, month, day, hh, mm, ss, x, y, z = time.localtime(now) + s = "%02d/%3s/%04d %02d:%02d:%02d" % ( + day, self.monthname[month], year, hh, mm, ss) + return s + + weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + + monthname = [None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + + def address_string(self): + """Return the client address formatted for logging. + + This version looks up the full hostname using gethostbyaddr(), + and tries to find a name that contains at least one dot. + + """ + + host, port = self.client_address + return socket.getfqdn(host) + + +class URLHandler(HTTPHandler): + def do_GET(self): + global URLS + + if self.path == '/': + self.list_urls() + return + + try: + idx = int(self.path[1:]) + url = URLS[idx] + except (ValueError, IndexError): + self.send_error(404) + return + + self.send_response(301) + self.send_header('Location', url) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.send('%s' % (url, url)) + + def log_message(self, format, *args): + # Don't do anything, so we can run in the background. + pass + + def list_urls(self): + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.send('URLs

URLs

    \n') + for url in URLS: + self.send('
  1. %s
  2. \n' % (url, url)) + self.send('
\n') + +def add(url): + URLS.append(url) + return len(URLS) - 1 + +def unpackHost(str): + host, port = str.split(':') + port = int(port) + return (host, port) + +def start(bindaddr): + return URLServer(bindaddr, URLHandler) + +def main(): + import sys + + (bind,) = sys.argv[1:] + bindaddr = unpackHost(bind) + m = start(bindaddr) + asyncore.loop() + +if __name__ == '__main__': + main() diff --git a/testbot.py b/testbot.py new file mode 100644 index 0000000..660e831 --- /dev/null +++ b/testbot.py @@ -0,0 +1,22 @@ +from firebot import FireBot +import irc +import re + +class Gallium(FireBot): + #debug = True + bindings = [] + + bindings.extend(FireBot.bindings) + + +NICK = ['gallium'] +INFO = "I'm a little printf, short and stdout" +HOSTS = [('woozle.org', 6667), + ('209.67.60.33', 6667)] + +l1 = Gallium(("woozle.org", 6667), + NICK, + INFO, + ["#test"]) + +irc.run_forever() diff --git a/webretriever.py b/webretriever.py new file mode 100755 index 0000000..1c7e7d3 --- /dev/null +++ b/webretriever.py @@ -0,0 +1,75 @@ +#! /usr/bin/env python + +import asynchat +import adns +import urlparse +import socket + +resolver = adns.init() + +class WebRetriever(asynchat.async_chat): + def __init__(self, url, body_cb): + asynchat.async_chat.__init__(self) + self.body_cb = body_cb + self.url = url + (self.scheme, + self.netloc, + self.path, + self.query, + self.fragment) = urlparse.urlsplit(url) + assert self.scheme == 'http' + try: + self.host, port = self.netloc.split(':') + self.port = int(port) + except ValueError: + self.host = self.netloc + self.port = 80 + self.set_terminator('\n') + self.in_headers = True + self.inbuf = '' + self.body = [] + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.dnsq = resolver.submit(self.host, adns.rr.A) + self.resolved = False + + def readable(self): + if not self.resolved: + try: + self.resolved = self.dnsq.check() + self.connect((self.resolved[3][0], self.port)) + except adns.NotReady: + return False + return asynchat.async_chat.readable(self) + + def writable(self): + return self.resolved and asynchat.async_chat.writable(self) + + def collect_incoming_data(self, data): + self.inbuf += data + + def handle_connect(self): + path = urlparse.urlunsplit((None, None, self.path, self.query, self.fragment)) + self.push('GET %s HTTP/1.0\r\n' % path) + self.push('Host: %s\r\n' % self.host) + self.push('\r\n') + + def found_terminator(self): + data, self.inbuf = self.inbuf, '' + if self.in_headers: + if not data.strip(): + self.in_headers = False + else: + self.body.append(data + self.get_terminator()) + + def handle_close(self): + asynchat.async_chat.close(self) + self.body_cb(self.body) + +if __name__ == '__main__': + import asyncore + + def p(data): + print ''.join(data) + + e = WebRetriever('http://quote.yahoo.com/d/quotes.csv?s=wgrd&f=sl1d1t1c1ohgvj1pp2owern&e=.csv', p) + asyncore.loop()