diff --git a/DrBotIRC.py b/DrBotIRC.py
index 5b4523c..af7e44d 100644
--- a/DrBotIRC.py
+++ b/DrBotIRC.py
@@ -16,7 +16,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+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;
diff --git a/Module.py b/Module.py
index 26599d9..82f1a7d 100644
--- a/Module.py
+++ b/Module.py
@@ -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__)
diff --git a/dr.botzo.py b/dr.botzo.py
index 8582db7..9ede43e 100644
--- a/dr.botzo.py
+++ b/dr.botzo.py
@@ -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:
diff --git a/modules/Alias.py b/modules/Alias.py
deleted file mode 100644
index 02f0185..0000000
--- a/modules/Alias.py
+++ /dev/null
@@ -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 .
-"""
-
-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;
diff --git a/modules/EightBall.py b/modules/EightBall.py
index 151dc64..43e5c34 100644
--- a/modules/EightBall.py
+++ b/modules/EightBall.py
@@ -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 = []
diff --git a/modules/IrcAdmin.py b/modules/IrcAdmin.py
index f0d471e..a3f9153 100644
--- a/modules/IrcAdmin.py
+++ b/modules/IrcAdmin.py
@@ -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;
diff --git a/modules/Karma.py b/modules/Karma.py
index 13d1647..122f10d 100644
--- a/modules/Karma.py
+++ b/modules/Karma.py
@@ -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+|$)'
diff --git a/modules/MegaHAL.py b/modules/MegaHAL.py
index c96d012..e8e55b3 100644
--- a/modules/MegaHAL.py
+++ b/modules/MegaHAL.py
@@ -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()
diff --git a/modules/Twitter.py b/modules/Twitter.py
index 25977bd..7490f6d 100644
--- a/modules/Twitter.py
+++ b/modules/Twitter.py
@@ -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'