Import from darcs

This commit is contained in:
Neale Pickett 2007-08-24 10:58:41 -06:00
commit 3e05fc5b4c
24 changed files with 3407 additions and 0 deletions

159
README Normal file
View File

@ -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.
<dl>
<dt>firebot, _x_</dt>
<dd>look up a factoid for _x_</dd>
<dt>firebot, _x_ is _y_</dt>
<dd>store _y_ as a factiod about _x_</dd>
<dt>firebot, _x_ is also _y_</dt>
<dd>store _y_ as another factoid about _x_</dd>
<dt>firebot, append _x_ &lt;= _y_</dt>
<dd>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.</dd>
<dt>no, firebot, _x_ is _y_</dt>
<dd>store _y_ as the only factoid about _x_, even if _x_ already has factoids</dd>
<dt>firebot, literal _x_</dt>
<dd>display all factoids about _x_</dd>
<dt>firebot, lock _x_</dt>
<dd>do not learn any more factoids about _x_</dd>
<dt>firebot, unlock _x_</dt>
<dd>resume learning factoids about _x_</dd>
<dt>firebot, forget _x_</dt>
<dd>forget all factoids about _x_</dd>
<dt>firebot, forget _x_ from _y_</dt>
<dd>forget a single entry (<em>x</em>) that is listed in _y_; _x_ can be a single word, it does not need to be the whole entry</dd>
</dl>
In addition, the following tricks can be used within factiods:
* Any factoid beginning with `\\` (a backslash) is displayed directly.
That is, instead of saying "<firebot> x is y", FireBot just says
"<firebot> y".
* Any factoid beginning with <code>:</code> (a colon) is
displayed an action. That is, instead of saying "<firebot> 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
---------
<dl>
<dt>firebot, later tell _whom_ _what_</dt>
<dd>The next time _whom_ says something in the channel, deliver the message _what_ publically.</dd>
<dt>firebot, in _time_ say _what_</dt>
<dd>after _time_ (eg. "15 seconds", "2 hours", "5 days"), announce _what_ in the channel</dd>
<dt>seen _whom_</dt>
<dd>Report the last thing said in the channel by _whom_, and how long ago it was said.</dd>
<dt>dict _word_</dt>
<dd>look _word_ up in the dictionary</dd>
<dt>quote _symbol_</dt>
<dd>get a stock quote for _symbol_</dd>
<dt>pollen _zip_</dt>
<dd>report pollen forecast for US zip code _zip_</dd>
<dt>cdecl explain _jibberish_</dt>
<dd>explain the C declaration _jibberish_ (eg. "cdecl explain struct bar *(*foo)[](int)")</dd>
<dt>cdecl declare _english_</dt>
<dd>give the C declaration for _english_ (eg. "cdecl declare foo as pointer to array of function (int) returning pointer to struct bar")</dd>
<dt>how many _x_ in _y_ _z_</dt>
<dd>determine the number of _x_ items that are contained in _y_ amount of _z_ items (eg. how many miles in 1 light year)</dd>
<dt>how much is _amt_ _source_ in _dest_</dt>
<dd>do a currency conversion from _source_ to _dest_. Both must be three-letter currency codes. (eg. "how much is 100 USD in EUR")</dd>
<dt>calc _expr_</dt>
<dd>calculate _expr_ (eg. "calc 2 * 5")</dd>
</dl>
Toys
----
<dl>
<dt>8ball, _question_</dt>
<dd>consult the magic 8-ball regarding _question_</dd>
<dt>_nickname_++</dt>
<dd>add a whuffie point for _nickname_</dd>
<dt>_nickname_--</dt>
<dd>remove a whuffie point for _nickname_</dd>
<dt>whuffie for _nickname_</dt>
<dd>check the whuffie for _nickname_</dd>
</dl>

66
acrobot.py Executable file
View File

@ -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()

29
addfacts.py Executable file
View File

@ -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()

140
arsenic.py Executable file
View File

@ -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"^(?P<command>rp) +(?P<args>.*)$"),
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"^(?P<command>finger) +(?P<args>.*)$"),
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<whom>[^: ]+):? +(?P<what>.*)"),
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()

69
async_proc.py Executable file
View File

@ -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()

114
bindingsbot.py Normal file
View File

@ -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.',)

44
convert.py Executable file
View File

@ -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()

40
daemon.py Executable file
View File

@ -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()

9
dbdump.py Executable file
View File

@ -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

63
dorkbot.py Executable file
View File

@ -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"^,(?P<dir>up|down)\s+(?P<nick>\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)

381
filedbm.py Executable file
View File

@ -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(), [])

34
finger.py Normal file
View File

@ -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()

457
firebot.py Executable file
View File

@ -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<whom>[^: ]+):? +(?P<what>.*)"),
note))
bindings.append((re.compile(r"^\008[:, ]+later tell (?P<whom>[^: ]+):? +(?P<what>.*)"),
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<whom>.*)$"),
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<code>.+)$"), 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<url>\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<jibberish>.*)$"),
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<delay>[0-9]+) ?(?P<unit>[a-z]*) say (?P<what>.*)"),
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<long>--?l(ong)? +)?(?P<jibberish>.*)$"),
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<from>.*) +in +(?P<to>.*)$"),
units))
bindings.append((re.compile(r"^how many (?P<to>.*) in (?P<from>[^?]*)[?.!]*$"),
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<expr>[0-9.]+\s*[-+*/^%]\s*[0-9.]+)$"),
calc))
bindings.append((re.compile(r"^calc (?P<expr>.+)$"),
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"^(?P<cmd>host) (?P<args>.+)$"),
generic_cmd))
bindings.append((re.compile(r"^(?P<cmd>whois) (?P<args>.+)$"),
generic_cmd))
def pollen(self, sender, forum, addl, match):
forecast_re = re.compile('fimages/std/(?P<count>[0-9]+\.[0-9])\.gif')
predom_re = re.compile('Predominant pollen: (?P<pollens>[^<]*)')
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<zip>[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<zip>[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<symbol>[.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<amt>[0-9.]+) ?(?P<from>[A-Z]{3}) in (?P<to>[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<nick>\w+)(?P<mod>\+\+|\-\-)[? ]*$"),
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<nick>\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)

114
gallium.py Executable file
View File

@ -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"^(?P<command>whois) +(?P<args>.*)$"),
runcmd))
bindings.append((re.compile(r"^(?P<command>host) +(?P<args>.*)$"),
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)

7
health.sh Executable file
View File

@ -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

301
infobot.py Normal file
View File

@ -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<key>.+)$", 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<key>.+)$", 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<key>.+)$", 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<key>.+)$", 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<me>\008[,: ]+)?(?P<question>(what|who|where|wtf).*('s|'re| is| are) )(?P<key>.+)$",
re.IGNORECASE),
lookup))
bindings.append((re.compile(r"^(?P<me>\008[,: ]+)append (?P<key>.+) <= (?P<val>.+)",
re.IGNORECASE),
append_cmd))
bindings.append((re.compile((r"^(?P<me>\008)[,: ]+(?P<no>no, *)"
r"(?P<key>.+?) (is|are) (?P<val>.+)$"),
re.IGNORECASE),
store))
bindings.append((re.compile((r"^(?P<no>no,? *)?(?P<me>\008)[,: ]+"
r"(?P<key>.+?) (is|are) (?P<val>.+)$"),
re.IGNORECASE),
store))
bindings.append((re.compile(r"^([^:, ]+[:,] *)?(?P<no>)(?P<me>)(?P<key>.+) (is|are) (?P<val>.+)$",
re.IGNORECASE),
store))
bindings.append((re.compile(r"^(?P<me>\008[,: ]+)?(?P<question>)(?P<key>.+)$",
re.IGNORECASE),
lookup))

604
irc.py Normal file
View File

@ -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)

96
linkbot.py Executable file
View File

@ -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()

38
opbot.py Executable file
View File

@ -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()

74
procbot.py Executable file
View File

@ -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()

146
seedyb.py Normal file
View File

@ -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

325
shorturl.py Executable file
View File

@ -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 = """\
<head>
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code %(code)d.
<p>Message: %(message)s.
<p>Error code explanation: %(code)s = %(explain)s.
</body>
"""
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('<a href="%s">%s</a>' % (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('<title>URLs</title><h1>URLs</h1><ol>\n')
for url in URLS:
self.send('<li><a href="%s">%s</a></li>\n' % (url, url))
self.send('</ol>\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()

22
testbot.py Normal file
View File

@ -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()

75
webretriever.py Executable file
View File

@ -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()