diff --git a/.gitignore b/.gitignore index 1fea45c..f2ea467 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.facts *.pyc *.swp +*.urls *~ dr.botzo.cfg diff --git a/Module.py b/Module.py index 1cd39c0..51ddaaf 100644 --- a/Module.py +++ b/Module.py @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from ConfigParser import NoSectionError, NoOptionError import inspect import re import sys @@ -93,7 +94,7 @@ class Module(object): admin_unlocked = False try: - if userhost == self.config.get('admin', 'userhost'): + if userhost == self.config.get('dr.botzo', 'admin_userhost'): admin_unlocked = True except NoOptionError: pass @@ -101,7 +102,13 @@ class Module(object): addressed_pattern = '^' + connection.get_nickname() + '[:,]?\s+' addressed_re = re.compile(addressed_pattern) - if not addressed_re.match(what): + need_prefix = True + try: + need_prefix = self.config.getboolean(self.__class__.__name__, 'meta.pubmsg_needs_bot_prefix') + except NoOptionError: pass + except NoSectionError: pass + + if not addressed_re.match(what) and need_prefix: return else: what = addressed_re.sub('', what) @@ -131,7 +138,7 @@ class Module(object): admin_unlocked = False try: - if userhost == self.config.get('admin', 'userhost'): + if userhost == self.config.get('dr.botzo', 'admin_userhost'): admin_unlocked = True except NoOptionError: pass @@ -219,6 +226,16 @@ class Module(object): else: return attempt + def remove_metaoptions(self, list): + """Remove metaoptions from provided list, which was probably from a config file.""" + + list.remove('debug') + # if this option has been provided, don't print it + try: + self.config.get(self.__class__.__name__, 'meta.pubmsg_needs_bot_prefix') + list.remove('meta.pubmsg_needs_bot_prefix') + except NoOptionError: pass + def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked): """ Do the primary thing this module was intended to do. diff --git a/TODO b/TODO index 38ba3e6..ba6afb2 100644 --- a/TODO +++ b/TODO @@ -10,8 +10,7 @@ dr.botzo --- TODO * some sort of cron interface (periodic events) * named pipe to send commands to the bot outside of IRC * url module - * direct adds implied. also do historical urls? - * last 20 or something? + * do historical urls? - last 20 or something? * more text modification nonsense * any interesting web service stuff? * obligatory info command diff --git a/dr.botzo.cfg.example b/dr.botzo.cfg.example index 19ff127..5731f97 100644 --- a/dr.botzo.cfg.example +++ b/dr.botzo.cfg.example @@ -1,16 +1,12 @@ -[IRC] +[dr.botzo] server = irc.foonetic.net port = 6667 nick = dr_devzo name = dr. devzo usermode = -x debug = true +admin_userhost = bss@ayu.incorporeal.org +module_list = IrcAdmin -[admin] -userhost = bss@ayu.incorporeal.org - -[channels] +[IrcAdmin] autojoin = #bss - -[modules] -modlist = IrcAdmin diff --git a/dr.botzo.py b/dr.botzo.py index 00a9952..f98e392 100644 --- a/dr.botzo.py +++ b/dr.botzo.py @@ -72,17 +72,17 @@ config.read([os.path.expanduser('~/.dr.botzo.cfg'), 'dr.botzo.cfg']) # load necessary options try: # load connection info - botserver = config.get('IRC', 'server') - botport = config.getint('IRC', 'port') - botnick = config.get('IRC', 'nick') - botircname = config.get('IRC', 'name') + botserver = config.get('dr.botzo', 'server') + botport = config.getint('dr.botzo', 'port') + botnick = config.get('dr.botzo', 'nick') + botircname = config.get('dr.botzo', 'name') except NoSectionError as e: sys.exit("Aborted due to error with necessary configuration: " + str(e)) except NoOptionError as e: sys.exit("Aborted due to error with necessary configuration: " + str(e)) # load additional options -irclib.DEBUG = config.getboolean('IRC', 'debug') +irclib.DEBUG = config.getboolean('dr.botzo', 'debug') # start up the IRC bot @@ -92,7 +92,7 @@ server = irc.server().connect(botserver, botport, botnick, botircname) # load features try: - cfgmodlist = config.get('modules', 'modlist') + cfgmodlist = config.get('dr.botzo', 'module_list') mods = cfgmodlist.split(',') for mod in mods: diff --git a/modules/Countdown.py b/modules/Countdown.py index f976732..d4747cb 100644 --- a/modules/Countdown.py +++ b/modules/Countdown.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from ConfigParser import NoOptionError +from ConfigParser import NoOptionError, NoSectionError from datetime import datetime from dateutil.parser import * @@ -34,29 +34,29 @@ class Countdown(Module): if whats[1] == 'add' and len(whats) >= 4: item = whats[2] target = parse(' '.join(whats[3:]), default=datetime.now().replace(tzinfo=tzlocal())) - if not self.config.has_section('countdown'): - self.config.add_section('countdown') + if not self.config.has_section(self.__class__.__name__): + self.config.add_section(self.__class__.__name__) - self.config.set('countdown', item, target.astimezone(tzutc()).isoformat()) + self.config.set(self.__class__.__name__, item, target.astimezone(tzutc()).isoformat()) replystr = 'added countdown item ' + whats[2] return self.reply(connection, replypath, replystr) elif whats[1] == 'remove': try: - if self.config.remove_option('countdown', whats[2]): + if self.config.remove_option(self.__class__.__name__, whats[2]): replystr = 'removed countdown item ' + whats[2] return self.reply(connection, replypath, replystr) except NoSectionError: pass elif whats[1] == 'list': try: - cdlist = self.config.options('countdown') - cdlist.remove('debug') + cdlist = self.config.options(self.__class__.__name__) + self.remove_metaoptions(cdlist) cdlist.sort() liststr = ', '.join(cdlist) return self.reply(connection, replypath, liststr) except NoSectionError: pass else: try: - timestr = self.config.get('countdown', whats[1]) + timestr = self.config.get(self.__class__.__name__, whats[1]) time = parse(timestr) rdelta = relativedelta(time, datetime.now().replace(tzinfo=tzlocal())) relstr = whats[1] + ' will occur in ' diff --git a/modules/FactFile.py b/modules/FactFile.py index 312519f..15147d6 100644 --- a/modules/FactFile.py +++ b/modules/FactFile.py @@ -30,7 +30,7 @@ class FactFile(Module): def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked): whats = what.split(' ') try: - filename = self.config.get('fact', whats[0]) + filename = self.config.get(self.__class__.__name__, whats[0]) # open file if os.path.isfile(filename): diff --git a/modules/IrcAdmin.py b/modules/IrcAdmin.py index acfcdf0..fc1ac64 100644 --- a/modules/IrcAdmin.py +++ b/modules/IrcAdmin.py @@ -45,14 +45,14 @@ class IrcAdmin(Module): # user modes try: - nick = self.config.get('IRC', 'nick') - usermode = self.config.get('IRC', 'usermode') + nick = self.config.get('dr.botzo', 'nick') + usermode = self.config.get('dr.botzo', 'usermode') connection.mode(nick, usermode) except NoOptionError: pass # join the specified channels try: - autojoins = self.config.get('channels', 'autojoin').split(',') + autojoins = self.config.get(self.__class__.__name__, 'autojoin').split(',') for channel in autojoins: if irclib.is_channel(channel): connection.join(channel) @@ -104,9 +104,9 @@ class IrcAdmin(Module): # get existing list channel = whats[2] if irclib.is_channel(channel): - channelset = set(self.config.get('channels', 'autojoin').split(',')) + channelset = set(self.config.get(self.__class__.__name__, 'autojoin').split(',')) channelset.add(channel) - self.config.set('channels', 'autojoin', ','.join(channelset)) + self.config.set(self.__class__.__name__, 'autojoin', ','.join(channelset)) replystr = 'added ' + channel + ' to autojoin' return self.reply(connection, replypath, replystr) except NoOptionError: pass @@ -115,9 +115,9 @@ class IrcAdmin(Module): # get existing list channel = whats[2] if irclib.is_channel(channel): - channelset = set(self.config.get('channels', 'autojoin').split(',')) + channelset = set(self.config.get(self.__class__.__name__, 'autojoin').split(',')) channelset.discard(channel) - self.config.set('channels', 'autojoin', ','.join(channelset)) + self.config.set(self.__class__.__name__, 'autojoin', ','.join(channelset)) replystr = 'removed ' + channel + ' from autojoin' return self.reply(connection, replypath, replystr) except NoOptionError: pass @@ -135,7 +135,7 @@ class IrcAdmin(Module): if whats[0] == 'nick' and admin_unlocked and len(whats) >= 2: newnick = whats[1] connection.nick(newnick) - self.config.set('IRC', 'nick', newnick) + self.config.set('dr.botzo', 'nick', newnick) replystr = 'changed nickname' return self.reply(connection, replypath, replystr) diff --git a/modules/Seen.py b/modules/Seen.py index 13a4c3b..9ca98fd 100644 --- a/modules/Seen.py +++ b/modules/Seen.py @@ -40,13 +40,13 @@ class Seen(Module): admin_unlocked = False # whatever it is, store it - if not self.config.has_section('seen'): - self.config.add_section('seen') + if not self.config.has_section(self.__class__.__name__): + self.config.add_section(self.__class__.__name__) - self.config.set('seen', nick, userhost + '|:|' + datetime.utcnow().isoformat() + '|:|' + what) + self.config.set(self.__class__.__name__, nick, userhost + '|:|' + datetime.utcnow().isoformat() + '|:|' + what) try: - if userhost == self.config.get('admin', 'userhost'): + if userhost == self.config.get('dr.botzo', 'admin_userhost'): admin_unlocked = True except NoOptionError: pass @@ -73,7 +73,7 @@ class Seen(Module): query = whats[1] if query != 'debug': try: - seendata = self.config.get('seen', query).split('|:|') + seendata = self.config.get(self.__class__.__name__, query).split('|:|') converted = datetime.strptime(seendata[1], "%Y-%m-%dT%H:%M:%S.%f").replace(tzinfo=tzutc()) replystr = 'last saw ' + query + ' at ' + converted.astimezone(tzlocal()).strftime("%Y/%m/%d %H:%M:%S %Z") + ' saying \'' + seendata[2] + '\'' return self.reply(connection, replypath, replystr) diff --git a/modules/Trigger.py b/modules/Trigger.py new file mode 100644 index 0000000..5c8a9d8 --- /dev/null +++ b/modules/Trigger.py @@ -0,0 +1,44 @@ +""" +Trigger - simple input -> output lookup based on config +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 Trigger(Module): + """Return text based on trigger input.""" + + def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked): + """Look up input text in config, and respond with result if found.""" + + try: + trigger_list = self.config.options(self.__class__.__name__) + self.remove_metaoptions(trigger_list) + + for trigger in trigger_list: + if re.search(trigger, what) is not None: + output = self.config.get(self.__class__.__name__, trigger) + return self.reply(connection, replypath, output) + except NoOptionError: pass + except NoSectionError: pass + +# vi:tabstop=4:expandtab:autoindent +# kate: indent-mode python;indent-width 4;replace-tabs on; diff --git a/modules/Urls.py b/modules/Urls.py new file mode 100644 index 0000000..e459e56 --- /dev/null +++ b/modules/Urls.py @@ -0,0 +1,73 @@ +""" +Urls - store urls, retrieve urls, even track some maybe +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 +import os +import random +import re + +from extlib import irclib + +from Module import Module + +class Urls(Module): + """Track URLs and allow one to be returned randomly or searched for.""" + + def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked): + """Search for a URL from the store, or return a random one, or add one.""" + + whats = what.split(' ') + if whats[0] == "url": + try: + filename = self.config.get(self.__class__.__name__, 'urlfile') + + # check file + if os.path.isfile(filename): + # try special commands like add + if len(whats) > 1: + if whats[1] == "add" and len(whats) > 2: + with open(filename, 'a') as file: + urlstr = ' '.join(whats[2:]) + "\n" + file.write(urlstr) + + return self.reply(connection, replypath, 'added url') + + # default: get a random url + + # http://www.regexprn.com/2008/11/read-random-line-in-large-file-in.html + with open(filename, 'r') as file: + urls = [] + if len(whats) == 1: + urls = file.readlines() + else: + lines = file.readlines() + for line in lines: + # check if line matches provided regex + if re.search(' '.join(whats[1:]), line, re.IGNORECASE) is not None: + urls.append(line) + if len(urls) > 0: + to_print = urls[random.randint(1, len(urls))-1] + + # return text + return self.reply(connection, replypath, to_print.rstrip()) + else: + print('filename in config file for urls is wrong') + except NoOptionError: pass + +# vi:tabstop=4:expandtab:autoindent +# kate: indent-mode python;indent-width 4;replace-tabs on;