commit 3e05fc5b4c22248733f430b7935c96321bccc88a Author: Neale Pickett Date: Fri Aug 24 10:58:41 2007 -0600 Import from darcs 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()