support for recursion. took a lot of time, probably a bit brittle, and a bunch of other changes got caught in the wake as i made it work. there are a couple candidates for making things generic in Module somewhere (somehow?), as a lot of stuff around replypath is reused for each class

This commit is contained in:
Brian S. Stephan 2010-07-26 00:49:15 -05:00
parent 565aff193c
commit c8e3a4354b
1 changed files with 148 additions and 30 deletions

View File

@ -18,13 +18,19 @@ class Module(object):
"""Base class used for creating classes that have real functionality.
"""
def __init__(self, config, server):
def __init__(self, config, server, modlist):
"""Constructor for a feature module. Inheritors should not do anything special
here, instead they should implement register_handlers and do, or else this will
be a very uneventful affair.
Classes that are interested in allowing an indirect call to their do routine
should add themselves to modlist inside their __init__. This will allow other
modules to call do and see if anything can handle text they may have seen (such
as in recursive commands).
"""
self.config = config
self.modlist = modlist
self.register_handlers(server)
def register_handlers(self, server):
@ -89,6 +95,50 @@ class Module(object):
self.do(connection, event, nick, userhost, replypath, what, admin_unlocked)
def try_recursion(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
"""Upon seeing a line intended for this module, see if there are subcommands
that we should do what is basically a text replacement on. The intent is to
allow things like the following:
command arg1 [anothercommand arg1 arg2]
where the output of anothercommand is command's arg2..n. It's mostly for
amusement purposes, but maybe there are legitimate uses. This is intended to
be attempted after you've determined the line should be handled by your module.
"""
start_idx = what.find('[')
subcmd = what[start_idx+1:]
end_idx = subcmd.rfind(']')
subcmd = subcmd[:end_idx]
attempt = what
if start_idx == -1 or end_idx == -1:
# no nested commands at all if replypath is a real value, so don't do a damn thing
if replypath is not None:
return attempt
# no more replacements found, see if what we had is workable
else:
for module in self.modlist:
ret = module.do(connection, event, nick, userhost, None, attempt, admin_unlocked)
if ret is not None:
return ret
# if we got here, it's not workable. just return what we got
return attempt
else:
# we have a subcmd, see if there's another one nested
ret = self.try_recursion(connection, event, nick, userhost, None, subcmd, admin_unlocked)
if ret is not None:
blarg = attempt.replace('['+subcmd+']', ret)
if replypath is not None:
return blarg
else:
return self.try_recursion(connection, event, nick, userhost, None, blarg, admin_unlocked)
else:
return attempt
def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
"""Implement this method in your subclass to have a fairly-automatic hook into
IRC functionality. This is called by the default on_pubmsg and on_privmsg
@ -102,8 +152,9 @@ class GoogleTranslate(Module):
http://code.google.com/apis/ajaxlanguage/documentation/
"""
def __init__(self, config, server):
super(GoogleTranslate, self).__init__(config, server)
def __init__(self, config, server, modlist):
super(GoogleTranslate, self).__init__(config, server, modlist)
modlist.append(self)
def register_handlers(self, server):
server.add_global_handler('pubmsg', self.on_pubmsg)
@ -112,6 +163,10 @@ class GoogleTranslate(Module):
def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
whats = what.split(' ')
if whats[0] == 'translate' and len(whats) >= 4:
if replypath is not None:
what = self.try_recursion(connection, event, nick, userhost, replypath, what, admin_unlocked)
whats = what.split(' ')
fromlang = whats[1]
tolang = whats[2]
text = ' '.join(whats[3:])
@ -125,14 +180,18 @@ class GoogleTranslate(Module):
translation = content[start_idx:]
end_idx = translation.find('"}, "')
translation = translation[:end_idx]
connection.privmsg(replypath, translation)
if replypath is None:
return translation
else:
connection.privmsg(replypath, translation)
class Countdown(Module):
"""Class that adds a countdown item to the bot
"""
def __init__(self, config, server):
super(Countdown, self).__init__(config, server)
def __init__(self, config, server, modlist):
super(Countdown, self).__init__(config, server, modlist)
modlist.append(self)
def register_handlers(self, server):
server.add_global_handler('pubmsg', self.on_pubmsg)
@ -141,6 +200,10 @@ class Countdown(Module):
def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
whats = what.split(' ')
if whats[0] == 'countdown' and len(whats) >= 2:
if replypath is not None:
what = self.try_recursion(connection, event, nick, userhost, replypath, what, admin_unlocked)
whats = what.split(' ')
if whats[1] == 'add' and len(whats) >= 4:
item = whats[2]
target = parse(' '.join(whats[3:]), default=datetime.now().replace(tzinfo=tzlocal()))
@ -148,11 +211,19 @@ class Countdown(Module):
self.config.add_section('countdown')
self.config.set('countdown', item, target.astimezone(tzutc()).isoformat())
connection.privmsg(replypath, 'added countdown item ' + whats[2])
replystr = 'added countdown item ' + whats[2]
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
elif whats[1] == 'remove':
try:
if self.config.remove_option('countdown', whats[2]):
connection.privmsg(replypath, 'removed countdown item ' + whats[2])
replystr = 'removed countdown item ' + whats[2]
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
except NoSectionError: pass
elif whats[1] == 'list':
try:
@ -160,7 +231,10 @@ class Countdown(Module):
cdlist.remove('debug')
cdlist.sort()
liststr = ', '.join(cdlist)
connection.privmsg(replypath, liststr)
if replypath is None:
return liststr
else:
connection.privmsg(replypath, liststr)
except NoSectionError: pass
else:
try:
@ -181,15 +255,19 @@ class Countdown(Module):
if rdelta.seconds != 0:
relstr += str(rdelta.seconds) + ' seconds'
#relstr += ' (' + timestr + ')'
connection.privmsg(replypath, relstr)
if replypath is None:
return relstr
else:
connection.privmsg(replypath, relstr)
except NoOptionError: pass
class Dice(Module):
"""Rolls dice, for RPGs mostly
"""
def __init__(self, config, server):
super(Dice, self).__init__(config, server)
def __init__(self, config, server, modlist):
super(Dice, self).__init__(config, server, modlist)
modlist.append(self)
def register_handlers(self, server):
server.add_global_handler('pubmsg', self.on_pubmsg)
@ -259,15 +337,19 @@ class Dice(Module):
if t != times-1:
result += ', '
connection.privmsg(replypath, result)
if replypath is None:
return result
else:
connection.privmsg(replypath, result)
class Seen(Module):
"""Keeps track of when people say things in public channels, and reports on when
they last said something.
"""
def __init__(self, config, server):
super(Seen, self).__init__(config, server)
def __init__(self, config, server, modlist):
super(Seen, self).__init__(config, server, modlist)
modlist.append(self)
def register_handlers(self, server):
server.add_global_handler('pubmsg', self.on_pubmsg)
@ -283,20 +365,29 @@ class Seen(Module):
# also see if it's a query
whats = what.split(' ')
if whats[0] == 'seen' and len(whats) >= 2:
if replypath is not None:
what = self.try_recursion(connection, event, nick, userhost, replypath, what, admin_unlocked)
whats = what.split(' ')
query = whats[1]
if query != 'debug':
try:
seendata = self.config.get('seen', query).split('|:|')
converted = datetime.strptime(seendata[1], "%Y-%m-%dT%H:%M:%S.%f").replace(tzinfo=tzutc())
connection.privmsg(replypath, 'last saw ' + query + ' at ' + converted.astimezone(tzlocal()).strftime("%Y/%m/%d %H:%M:%S %Z") + ' saying \'' + seendata[2] + '\'')
replystr = 'last saw ' + query + ' at ' + converted.astimezone(tzlocal()).strftime("%Y/%m/%d %H:%M:%S %Z") + ' saying \'' + seendata[2] + '\''
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
except NoOptionError: pass
class IrcAdmin(Module):
"""all kinds of miscellaneous irc stuff
"""
def __init__(self, config, server):
super(IrcAdmin, self).__init__(config, server)
def __init__(self, config, server, modlist):
super(IrcAdmin, self).__init__(config, server, modlist)
modlist.append(self)
def register_handlers(self, server):
server.add_global_handler('welcome', self.on_connect)
@ -338,7 +429,11 @@ class IrcAdmin(Module):
channel = whats[1]
if irclib.is_channel(channel):
connection.join(channel)
connection.privmsg(replypath, 'joined ' + channel)
replystr = 'joined ' + channel
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
def sub_part_channel(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
whats = what.split(' ')
@ -346,12 +441,17 @@ class IrcAdmin(Module):
channel = whats[1]
if irclib.is_channel(channel):
connection.part(channel, ' '.join(whats[2:]))
connection.privmsg(replypath, 'parted ' + channel)
replystr = 'parted ' + channel
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
def sub_quit_channel(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
whats = what.split(' ')
if whats[0] == 'quit' and admin_unlocked:
connection.privmsg(replypath, 'quitting')
if replypath is not None:
connection.privmsg(replypath, 'quitting')
connection.quit(' '.join(whats[1:]))
with open('dr.botzo.cfg', 'w') as cfg:
self.config.write(cfg)
@ -367,7 +467,11 @@ class IrcAdmin(Module):
channelset = set(self.config.get('channels', 'autojoin').split(','))
channelset.add(channel)
self.config.set('channels', 'autojoin', ','.join(channelset))
connection.privmsg(replypath, 'added ' + channel + ' to autojoin')
replystr = 'added ' + channel + ' to autojoin'
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
except NoOptionError: pass
elif whats[1] == 'remove':
try:
@ -377,7 +481,11 @@ class IrcAdmin(Module):
channelset = set(self.config.get('channels', 'autojoin').split(','))
channelset.discard(channel)
self.config.set('channels', 'autojoin', ','.join(channelset))
connection.privmsg(replypath, 'removed ' + channel + ' from autojoin')
replystr = 'removed ' + channel + ' from autojoin'
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
except NoOptionError: pass
def sub_save_config(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
@ -385,7 +493,11 @@ class IrcAdmin(Module):
if whats[0] == 'save' and admin_unlocked:
with open('dr.botzo.cfg', 'w') as cfg:
self.config.write(cfg)
connection.privmsg(replypath, 'saved config file')
replystr = 'saved config file'
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
def sub_change_nick(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
whats = what.split(' ')
@ -393,7 +505,11 @@ class IrcAdmin(Module):
newnick = whats[1]
connection.nick(newnick)
self.config.set('IRC', 'nick', newnick)
connection.privmsg(replypath, 'changed nickname')
replystr = 'changed nickname'
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
#####
# init
@ -425,11 +541,13 @@ irclib.DEBUG = config.getboolean('IRC', 'debug')
irc = irclib.IRC()
server = irc.server().connect(botserver, botport, botnick, botircname)
count = Countdown(config, server)
dice = Dice(config, server)
admin = IrcAdmin(config, server)
gt = GoogleTranslate(config, server)
seen = Seen(config, server)
modlist = []
count = Countdown(config, server, modlist)
dice = Dice(config, server, modlist)
admin = IrcAdmin(config, server, modlist)
gt = GoogleTranslate(config, server, modlist)
seen = Seen(config, server, modlist)
# loop forever
irc.process_forever()