migrate some code that became pivotal to the bot into DrBotIRC.

this is a big change. DrBotIrc is now in charge of module loading
and unloading, aliases, and recursion. the Alias module is no more,
and a bunch of functionality was moved out of IrcAdmin, including
also config file saving, the sigint handler, and quitting the bot.

additionally, a lot of stuff got caught in the wake. dr.botzo.py
is simpler now, and lets DrBotIRC do the dynamic loading stuff.
Module.__init__ changed, modules no longer get modlist and instead
get a reference to the DrBotIRC object. IrcAdmin still has the same
exposed methods, but now calls out to DrBotIRC to achieve some of
them.

naturally, a recursion/alias rewrite was included with this change.
it is clearer now (i think), but probably brittle somewhere.
additionally, currently any module that has registered a pubmsg
handler can potentially fire more than once on one input (without
recursion). this may be the next thing to fix. do() may need to
be split, or maybe it's time to stop having modules deal with
pubmsg/privmsg entirely. need to decide.

WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
This commit is contained in:
Brian S. Stephan 2011-01-07 17:38:26 -06:00
parent 973dbae90e
commit 01d3c7c80c
9 changed files with 264 additions and 258 deletions

View File

@ -16,7 +16,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from ConfigParser import NoOptionError, NoSectionError
import re
import signal
import socket
import sys
from extlib import irclib
@ -55,12 +59,250 @@ class DrBotServerConnection(irclib.ServerConnection):
class DrBotIRC(irclib.IRC):
"""Subclass irclib's IRC, in order to create a DrBotServerConnection."""
"""Implement a customized irclib IRC."""
modlist = []
config = None
server = None
def __init__(self, config):
irclib.IRC.__init__(self)
self.config = config
# handle SIGINT
signal.signal(signal.SIGINT, self.sigint_handler)
def server(self):
c = DrBotServerConnection(self)
self.connections.append(c)
return c
"""Create a DrBotServerConnection."""
self.server = DrBotServerConnection(self)
self.connections.append(self.server)
self.server.add_global_handler('pubmsg', self.on_pubmsg, 1)
self.server.add_global_handler('privmsg', self.on_pubmsg, 1)
return self.server
def on_pubmsg(self, connection, event):
"""See if there is an alias ("!command") in the text, and if so, translate it into
an internal bot command and run it.
"""
nick = irclib.nm_to_n(event.source())
userhost = irclib.nm_to_uh(event.source())
replypath = event.target()
what = event.arguments()[0]
admin_unlocked = False
try:
if userhost == self.config.get('dr.botzo', 'admin_userhost'):
admin_unlocked = True
except NoOptionError: pass
return self.try_recursion(connection, event, nick, userhost, replypath, what, admin_unlocked)
def try_alias(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
# first see if the aliases are being directly manipulated via add/remove
whats = what.split(' ')
try:
if whats[0] == '!alias' and whats[1] == 'add' and len(whats) >= 4:
if not self.config.has_section('Alias'):
self.config.add_section('Alias')
self.config.set('Alias', whats[2], ' '.join(whats[3:]))
replystr = 'Added alias ' + whats[2] + '.'
return self.reply(connection, replypath, replystr)
if whats[0] == '!alias' and whats[1] == 'remove' and len(whats) >= 3:
if not self.config.has_section('Alias'):
self.config.add_section('Alias')
if self.config.remove_option('Alias', whats[2]):
replystr = 'Removed alias ' + whats[2] + '.'
return self.reply(connection, replypath, replystr)
elif whats[0] == '!alias' and whats[1] == 'list':
try:
if len(whats) > 2:
alias = self.config.get('Alias', whats[2])
return self.reply(connection, replypath, alias)
else:
alist = self.config.options('Alias')
alist.sort()
liststr = ', '.join(alist)
return self.reply(connection, replypath, liststr)
except NoSectionError: pass
except NoOptionError: pass
except NoSectionError: pass
# done searching for recursions in this level. we will now operate on
# whatever recursion-satisfied string we have, checking for alias and
# running module commands
# now that that's over, try doing alias work
try:
alias_list = self.config.options('Alias')
for alias in alias_list:
alias_re = re.compile(alias)
if alias_re.search(what):
# we found an alias for our given string, doing a replace
command = re.sub(alias, self.config.get('Alias', alias), what)
# need to do another recursion scan
ret = self.try_recursion(connection, event, nick, userhost, None, command, admin_unlocked)
return self.reply(connection, replypath, ret)
except NoOptionError: pass
except NoSectionError: pass
# if we got here, there are no matching aliases, so return what we got
return self.reply(connection, replypath, what)
def reply(self, connection, replypath, replystr, stop_responding=False):
"""
Reply over IRC to replypath or return a string with the reply.
Utility method to do the proper type of reply (either to IRC, or as a return
to caller) depending on the target. Pretty simple, and included in the base
class for convenience. It should be the last step for callers:
return self.reply(connection, replypath, 'hello')
"""
if replystr is not None:
if replypath is None:
return replystr
else:
connection.privmsg(replypath, replystr)
if stop_responding:
return "NO MORE"
def try_recursion(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
"""Scan message for subcommands to execute and use as part of this command.
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.
"""
attempt = what
start_idx = attempt.find('[')
subcmd = attempt[start_idx+1:]
end_idx = subcmd.rfind(']')
subcmd = subcmd[:end_idx]
if start_idx == -1 or end_idx == -1 or len(subcmd) == 0:
# no recursion, now let's look for aliases
command = self.try_alias(connection, event, nick, userhost, None, attempt, admin_unlocked)
# aliases resolved. run result against each module
for module in self.modlist:
ret = module.do(connection, event, nick, userhost, None, command, admin_unlocked)
if ret is not None:
# a module had a result for us, post-alias, so return it
# TODO: scan all modules with compounding results
return self.reply(connection, replypath, ret)
# if we got here, text isn't a command, so pass it back, but ONLY if the output
# is different from the input
if replypath is None or what != command:
return self.reply(connection, replypath, command)
else:
ret = self.try_recursion(connection, event, nick, userhost, None, subcmd, admin_unlocked)
if ret is not None:
return self.try_alias(connection, event, nick, userhost, replypath, attempt.replace('['+subcmd+']', ret), admin_unlocked)
else:
return self.try_alias(connection, event, nick, userhost, replypath, attempt, admin_unlocked)
def quit_irc(self, connection, msg):
for module in self.modlist:
module.shutdown()
connection.quit(msg)
self.save_config()
sys.exit()
def save_config(self):
with open('dr.botzo.cfg', 'w') as cfg:
self.config.write(cfg)
for module in self.modlist:
module.save()
return 'Saved.'
def load_module(self, modname):
"""Load a module (in both the python and dr.botzo sense) if not
already loaded.
"""
for module in self.modlist:
if modname == module.__class__.__name__:
return 'Module ' + modname + ' is already loaded.'
# not loaded, let's get to work
try:
modstr = 'modules.'+modname
__import__(modstr)
module = sys.modules[modstr]
botmod = eval('module.' + modname + '(self, self.config, self.server)')
self.modlist.append(botmod)
botmod.register_handlers(self.server)
# might as well add it to the list
modset = set(self.config.get('dr.botzo', 'module_list').split(','))
modset.add(modname)
self.config.set('dr.botzo', 'module_list', ','.join(modset))
return 'Module ' + modname + ' loaded.'
except ImportError:
return 'Module ' + modname + ' not found.'
def unload_module(self, modname):
"""Attempt to unload and del a module if it's loaded."""
modstr = 'modules.'+modname
for module in self.modlist:
if modname == module.__class__.__name__:
# do anything the module needs to do to clean up
module.shutdown()
# remove module references
self.modlist.remove(module)
module.unregister_handlers()
# del it
del(sys.modules[modstr])
del(module)
# might as well remove it from the list
modset = set(self.config.get('dr.botzo', 'module_list').split(','))
modset.remove(modname)
self.config.set('dr.botzo', 'module_list', ','.join(modset))
return 'Module ' + modname + ' unloaded.'
# guess it was never loaded
return 'Module ' + modname + ' is not loaded.'
def reload_module(self, modname):
"""Attempt to reload a module, by removing it from memory and then
re-initializing it.
"""
ret = self.unload_module(modname)
if ret == 'Module ' + modname + ' unloaded.':
ret = self.load_module(modname)
if ret == 'Module ' + modname + ' loaded.':
return 'Module ' + modname + ' reloaded.'
return 'Module ' + modname + ' reload failed. Check the console.'
# SIGINT signal handler
def sigint_handler(self, signal, frame):
print(self.save_config())
sys.exit()
# vi:tabstop=4:expandtab:autoindent
# kate: indent-mode python;indent-width 4;replace-tabs on;

View File

@ -31,21 +31,16 @@ class Module(object):
def priority(self):
return 50
def __init__(self, config, server, modlist):
def __init__(self, irc, config, server):
"""
Construct 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.irc = irc
self.config = config
self.server = server
self.modlist = modlist
# open database connection
dbfile = self.config.get('dr.botzo', 'database')
@ -61,9 +56,6 @@ class Module(object):
# set up database for this module
self.db_init()
# add self to the object list
modlist.append(self)
# print what was loaded, for debugging
print("loaded " + self.__class__.__name__)

View File

@ -26,7 +26,6 @@ import sqlite3
import DrBotIRC
from extlib import irclib
modlist = []
moduleList = [ "Countdown", "Dice", "IrcAdmin", "GoogleTranslate", "Seen", "FactFile" ]
modObjs = []
@ -86,7 +85,7 @@ except NoSectionError: pass # the database doesn't need to exist
# start up the IRC bot
# create IRC and server objects and connect
irc = DrBotIRC.DrBotIRC()
irc = DrBotIRC.DrBotIRC(config)
server = irc.server().connect(botserver, botport, botnick, botpass, botuser, botircname)
# load features
@ -95,14 +94,7 @@ try:
mods = cfgmodlist.split(',')
for mod in mods:
# try to load each module
mod = mod.strip()
modstr = 'modules.'+mod
print "DEBUG: attempting to load module %s" % (modstr)
__import__(modstr)
module = sys.modules[modstr]
botmod = eval('module.' + mod + '(config, server, modlist)')
botmod.register_handlers(server)
irc.load_module(mod)
except NoSectionError as e:
print("You seem to be missing a modules config section, which you probably wanted.")
except NoOptionError as e:

View File

@ -1,137 +0,0 @@
"""
Alias - have internal shortcuts to commands
Copyright (C) 2010 Brian S. Stephan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from ConfigParser import NoOptionError, NoSectionError
import re
from extlib import irclib
from Module import Module
class Alias(Module):
"""Alias commands as !command, circumventing bot addressing stuff."""
def priority(self):
return 1
def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
"""See if there is an alias ("!command") in the text, and if so, translate it into
an internal bot command and run it.
"""
# first see if the aliases are being directly manipulated via add/remove
whats = what.split(' ')
try:
if whats[0] == '!alias' and whats[1] == 'add' and len(whats) >= 4:
if not self.config.has_section(self.__class__.__name__):
self.config.add_section(self.__class__.__name__)
self.config.set(self.__class__.__name__, whats[2], ' '.join(whats[3:]))
replystr = 'Added alias ' + whats[2] + '.'
return self.reply(connection, replypath, replystr)
if whats[0] == '!alias' and whats[1] == 'remove' and len(whats) >= 3:
if not self.config.has_section(self.__class__.__name__):
self.config.add_section(self.__class__.__name__)
if self.config.remove_option(self.__class__.__name__, whats[2]):
replystr = 'Removed alias ' + whats[2] + '.'
return self.reply(connection, replypath, replystr)
elif whats[0] == '!alias' and whats[1] == 'list':
try:
if len(whats) > 2:
alias = self.config.get(self.__class__.__name__, whats[2])
return self.reply(connection, replypath, alias)
else:
alist = self.config.options(self.__class__.__name__)
self.remove_metaoptions(alist)
alist.sort()
liststr = ', '.join(alist)
return self.reply(connection, replypath, liststr)
except NoSectionError: pass
except NoOptionError: pass
except NoSectionError: pass
# search for recursions, which will search for recursions, which ...
what = self.try_recursion(connection, event, nick, userhost, None, what, admin_unlocked)
# done searching for recursions in this level. we will now operate on
# whatever recursion-satisfied string we have, checking for alias and
# running module commands
# now that that's over, try doing alias work
try:
alias_list = self.config.options(self.__class__.__name__)
self.remove_metaoptions(alias_list)
for alias in alias_list:
alias_re = re.compile(alias)
if alias_re.search(what):
command = re.sub(alias, self.config.get(self.__class__.__name__, alias), what)
# we found an alias for our given string, doing a replace
# need to do another recursion scan
command = self.try_recursion(connection, event, nick, userhost, None, command, admin_unlocked)
# running it against each module
for module in self.modlist:
ret = module.do(connection, event, nick, userhost, None, command, admin_unlocked)
if ret is not None:
# a module had a result for us, post-alias, so return it
# TODO: scan all modules with compounding results
return self.reply(connection, replypath, ret)
except NoOptionError: pass
except NoSectionError: pass
def try_recursion(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
"""Scan message for subcommands to execute and use as part of this command.
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.
"""
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 or len(subcmd) == 0:
# no alias, just returning what we got
return attempt
else:
# we have a subcmd, see if there's another one nested
# this will include more calls to Alias, which will try recursing again
for module in self.modlist:
ret = module.do(connection, event, nick, userhost, None, subcmd, admin_unlocked)
if ret is not None:
# some module had a change, so we replace [subcmd] with ret and return it
return attempt.replace('['+subcmd+']', ret)
# we got here, no one had a replacement. return what we had
return attempt
# vi:tabstop=4:expandtab:autoindent
# kate: indent-mode python;indent-width 4;replace-tabs on;

View File

@ -26,10 +26,10 @@ class EightBall(Module):
"""Return a random answer when asked a question."""
def __init__(self, config, server, modlist):
def __init__(self, irc, config, server):
"""Initialize the list of self.responses."""
Module.__init__(self, config, server, modlist)
Module.__init__(self, irc, config, server)
self.responses = []

View File

@ -33,9 +33,6 @@ class IrcAdmin(Module):
server.add_global_handler('pubmsg', self.on_pubmsg, self.priority())
server.add_global_handler('privmsg', self.on_privmsg, self.priority())
# we define save, so we'll bite the bullet and take SIGINT
signal.signal(signal.SIGINT, self.sigint_handler)
def unregister_handlers(self):
self.server.remove_global_handler('welcome', self.on_connect)
self.server.remove_global_handler('pubmsg', self.on_pubmsg)
@ -75,7 +72,7 @@ class IrcAdmin(Module):
elif whats[0] == 'autojoin' and admin_unlocked and len(whats) >= 3:
return self.sub_autojoin_manipulate(connection, event, nick, userhost, replypath, what, admin_unlocked)
elif whats[0] == 'config' and whats[1] == 'save' and admin_unlocked:
return self.sub_save_config(connection, event, nick, userhost, replypath, what, admin_unlocked)
return self.reply(connection, replypath, self.irc.save_config())
elif whats[0] == 'nick' and admin_unlocked and len(whats) >= 2:
return self.sub_change_nick(connection, event, nick, userhost, replypath, what, admin_unlocked)
elif whats[0] == 'load' and admin_unlocked and len(whats) >= 2:
@ -106,14 +103,10 @@ class IrcAdmin(Module):
def sub_quit_irc(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
whats = what.split(' ')
for module in self.modlist:
module.shutdown()
if replypath is not None:
connection.privmsg(replypath, 'Quitting...')
connection.quit(' '.join(whats[1:]))
self.save_config()
sys.exit()
self.irc.quit_irc(connection, ' '.join(whats[1:]))
def sub_autojoin_manipulate(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
whats = what.split(' ')
@ -141,16 +134,6 @@ class IrcAdmin(Module):
return self.reply(connection, replypath, replystr)
except NoOptionError: pass
def sub_save_config(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
with open('dr.botzo.cfg', 'w') as cfg:
self.config.write(cfg)
for module in self.modlist:
module.save()
replystr = 'Saved.'
return self.reply(connection, replypath, replystr)
def sub_change_nick(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
whats = what.split(' ')
@ -166,58 +149,13 @@ class IrcAdmin(Module):
"""
whats = what.split(' ')
modname = whats[1]
for module in self.modlist:
if modname == module.__class__.__name__:
return self.reply(connection, replypath, 'Module ' + modname + ' is already loaded.')
# not loaded, let's get to work
try:
modstr = 'modules.'+modname
__import__(modstr)
module = sys.modules[modstr]
botmod = eval('module.' + modname + '(self.config, self.server, self.modlist)')
botmod.register_handlers(self.server)
# might as well add it to the list
modset = set(self.config.get('dr.botzo', 'module_list').split(','))
modset.add(modname)
self.config.set('dr.botzo', 'module_list', ','.join(modset))
return self.reply(connection, replypath, 'Module ' + modname + ' loaded.')
except ImportError:
return self.reply(connection, replypath, 'Module ' + modname + ' not found.')
return self.reply(connection, replypath, self.irc.load_module(whats[1]))
def sub_unload_module(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
"""Attempt to unload and del a module if it's loaded."""
whats = what.split(' ')
modname = whats[1]
modstr = 'modules.'+modname
for module in self.modlist:
if modname == module.__class__.__name__:
# do anything the module needs to do to clean up
module.shutdown()
# remove module references
self.modlist.remove(module)
module.unregister_handlers()
# del it
del(sys.modules[modstr])
del(module)
# might as well remove it from the list
modset = set(self.config.get('dr.botzo', 'module_list').split(','))
modset.remove(modname)
self.config.set('dr.botzo', 'module_list', ','.join(modset))
return self.reply(connection, replypath, 'Module ' + modname + ' unloaded.')
# guess it was never loaded
return self.reply(connection, replypath, 'Module ' + modname + ' is not loaded.')
return self.reply(connection, replypath, self.irc.unload_module(whats[1]))
def sub_reload_module(self, connection, event, nick, userhost, replypath, what, admin_unlocked):
"""Attempt to reload a module, by removing it from memory and then
@ -225,28 +163,7 @@ class IrcAdmin(Module):
"""
whats = what.split(' ')
modname = whats[1]
ret = self.sub_unload_module(connection, event, nick, userhost, None, what, admin_unlocked)
if ret == 'Module ' + modname + ' unloaded.':
ret = self.sub_load_module(connection, event, nick, userhost, None, what, admin_unlocked)
if ret == 'Module ' + modname + ' loaded.':
return self.reply(connection, replypath, 'Module ' + modname + ' reloaded.')
return self.reply(connection, replypath, 'Module ' + modname + ' reload failed. Check the console.')
# Save the config file.
def save_config(self):
with open('dr.botzo.cfg', 'w') as cfg:
self.config.write(cfg)
# SIGINT signal handler
def sigint_handler(self, signal, frame):
for module in self.modlist:
module.shutdown()
self.save_config()
print('saved config')
sys.exit()
return self.reply(connection, replypath, self.irc.unload_module(whats[1]))
# vi:tabstop=4:expandtab:autoindent
# kate: indent-mode python;indent-width 4;replace-tabs on;

View File

@ -26,12 +26,12 @@ __date__ = "$Oct 23, 2010 11:12:33 AM$"
class Karma(Module):
def __init__(self, config, server, modlist):
def __init__(self, irc, config, server):
"""
Upon creation, determine the save file location
"""
Module.__init__(self, config, server, modlist)
Module.__init__(self, irc, config, server)
pattern = "(?:(\S+)|\((.+)\))"
karmapattern = pattern + '(\+\+|--|\+-|-\+)' + '(\s+|$)'

View File

@ -32,10 +32,10 @@ class MegaHAL(Module):
def priority(self):
return 95
def __init__(self, config, server, modlist):
def __init__(self, irc, config, server):
"""Upon creation, open the MegaHAL brain and get ready for doing actual stuff."""
Module.__init__(self, config, server, modlist)
Module.__init__(self, irc, config, server)
mh_python.initbrain()

View File

@ -29,14 +29,14 @@ class Twitter(Module):
Access Twitter via the bot as an authenticated client.
"""
def __init__(self, config, server, modlist):
def __init__(self, irc, config, server):
"""
Prompt the user for oauth stuff when starting up.
TODO: make this optional, and have API calls log if they need auth.
"""
Module.__init__(self, config, server, modlist)
Module.__init__(self, irc, config, server)
# begin oauth magic
self.consumer_key = 'N2aSGxBP8t3cCgWyF1B2Aw'