diff --git a/dr.botzo.py b/dr.botzo.py index 8a52403..e643b73 100755 --- a/dr.botzo.py +++ b/dr.botzo.py @@ -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()