diff --git a/modules/Countdown.py b/modules/Countdown.py index ba727d6..c31a799 100644 --- a/modules/Countdown.py +++ b/modules/Countdown.py @@ -14,15 +14,18 @@ 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 from datetime import datetime import re from dateutil.parser import * from dateutil.relativedelta import * from dateutil.tz import * +import MySQLdb as mdb + +from extlib import irclib from Module import Module @@ -30,86 +33,332 @@ class Countdown(Module): """Track when events will happen.""" - def do(self, connection, event, nick, userhost, what, admin_unlocked): - """Add/retrieve countdown items.""" + def db_init(self): + """Set up the database tables, if they don't exist.""" - match = re.search('^!countdown\s+add\s+(\S+)\s+(.*)$', what) - if match: + version = self.db_module_registered(self.__class__.__name__) + if (version == None): + db = self.get_db() try: - item = match.group(1) - target = parse(match.group(2), default=datetime.now().replace(tzinfo=tzlocal())) + version = 1 + cur = db.cursor(mdb.cursors.DictCursor) + cur.execute(''' + CREATE TABLE countdown_item ( + name VARCHAR(255) NOT NULL, + source VARCHAR(255) NOT NULL, + time TIMESTAMP NOT NULL, + PRIMARY KEY (name, source) + ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci + ''') - if not self.config.has_section(self.__class__.__name__): - self.config.add_section(self.__class__.__name__) - - self.config.set(self.__class__.__name__, item, target.astimezone(tzutc()).isoformat()) - replystr = 'added countdown item ' + item - return self.irc.reply(event, replystr) - except ValueError as e: - self.log.error("could not parse countdown item") + db.commit() + self.db_register_module_version(self.__class__.__name__, version) + except mdb.Error as e: + db.rollback() + self.log.error("database error trying to create tables") self.log.exception(e) - return self.irc.reply(event, - "could not parse countdown item: {0:s}".format(str(e))) + raise + finally: cur.close() - match = re.search('^!countdown\s+remove\s+(\S+)$', what) - if match: - try: - item = match.group(1) - if self.config.remove_option(self.__class__.__name__, item): - replystr = 'removed countdown item ' + item - return self.irc.reply(event, replystr) - except NoSectionError: pass + def register_handlers(self): + """Hook handler functions into the IRC library.""" - match = re.search('^!countdown\s+list$', what) - if match: - try: - cdlist = self.config.options(self.__class__.__name__) - self.remove_metaoptions(cdlist) - cdlist.sort() - liststr = ', '.join(cdlist) - return self.irc.reply(event, liststr) - except NoSectionError: pass + # register IRC regex handlers + self.irc.add_global_regex_handler('pubmsg', + r'^!countdown\s+add\s+(\S+)\s+(.*)$', + self.add_item) + self.irc.add_global_regex_handler('privmsg', + r'^!countdown\s+add\s+(\S+)\s+(.*)$', + self.add_item) + self.irc.add_global_regex_handler('pubmsg', + r'^!countdown\s+remove\s+(\S+)$', + self.remove_item) + self.irc.add_global_regex_handler('privmsg', + r'^!countdown\s+remove\s+(\S+)$', + self.remove_item) + self.irc.add_global_regex_handler('pubmsg', + r'^!countdown\s+list$', + self.list_items) + self.irc.add_global_regex_handler('privmsg', + r'^!countdown\s+list$', + self.list_items) + self.irc.add_global_regex_handler('pubmsg', + r'^!countdown\s+(\S+)$', + self.item_detail) + self.irc.add_global_regex_handler('privmsg', + r'^!countdown\s+(\S+)$', + self.item_detail) - match = re.search('^!countdown\s+(\S+)$', what) - if match: - try: - item = match.group(1) - timestr = self.config.get(self.__class__.__name__, item) - time = parse(timestr) + def unregister_handlers(self): + """Unhook handler functions from the IRC library.""" + + # register IRC regex handlers + self.irc.remove_global_regex_handler('pubmsg', + r'^!countdown\s+add\s+(\S+)\s+(.*)$', + self.add_item) + self.irc.remove_global_regex_handler('privmsg', + r'^!countdown\s+add\s+(\S+)\s+(.*)$', + self.add_item) + self.irc.remove_global_regex_handler('pubmsg', + r'^!countdown\s+remove\s+(\S+)$', + self.remove_item) + self.irc.remove_global_regex_handler('privmsg', + r'^!countdown\s+remove\s+(\S+)$', + self.remove_item) + self.irc.remove_global_regex_handler('pubmsg', + r'^!countdown\s+list$', + self.list_items) + self.irc.remove_global_regex_handler('privmsg', + r'^!countdown\s+list$', + self.list_items) + self.irc.remove_global_regex_handler('pubmsg', + r'^!countdown\s+(\S+)$', + self.item_detail) + self.irc.remove_global_regex_handler('privmsg', + r'^!countdown\s+(\S+)$', + self.item_detail) + + def add_item(self, nick, userhost, event, from_admin, groups): + """Add a new item to track, and the datetime it occurs. + + Args: + nick source nickname (unused) + userhost source userhost (unused) + event IRC event, target used to associate item to a source + from_admin whether or not the event came from an admin (unused) + groups tuple length 2, the item name and the time of the item + + """ + + try: + name, time_str = groups + time = parse(time_str, default=datetime.now().replace(tzinfo=tzlocal())) + + # determine if source is a channel of a privmsg + source = event.target() + if source == self.irc.server.get_nickname(): + source = irclib.nm_to_n(event.source()) + + self._add_countdown_item_to_database(name, source, + time.astimezone(tzutc())) + self.log.debug("added countdown item '{0:s}' at " + "{1:s}".format(name, time.isoformat())) + replystr = "added countdown item '{0:s}'".format(name) + return self.irc.reply(event, replystr) + except Exception as e: + self.log.error("could not add countdown item") + self.log.exception(e) + return self.irc.reply(event, ("could not add countdown item: " + "{0:s}".format(str(e)))) + + def remove_item(self, nick, userhost, event, from_admin, groups): + """State when the provided item will occur. + + Args: + nick source nickname (unused) + userhost source userhost (unused) + event IRC event, target used to filter item names + from_admin whether or not the event came from an admin (unused) + groups tuple length 1, the item name to remove + + """ + + try: + name = groups[0] + + # determine if source is a channel of a privmsg + source = event.target() + if source == self.irc.server.get_nickname(): + source = irclib.nm_to_n(event.source()) + + self._remove_countdown_item_from_database(name, source) + replystr = "removed countdown item '{0:s}'".format(name) + return self.irc.reply(event, replystr) + except Exception: pass + + def list_items(self, nick, userhost, event, from_admin, groups): + """State when the provided item will occur. + + Args: + nick source nickname (unused) + userhost source userhost (unused) + event IRC event, target used to filter item names + from_admin whether or not the event came from an admin (unused) + groups empty tuple (unused) + + """ + + try: + # determine if source is a channel of a privmsg + source = event.target() + if source == self.irc.server.get_nickname(): + source = irclib.nm_to_n(event.source()) + + cdlist = self._list_countdown_items(source) + print(cdlist) + liststr = "countdown items: {0:s}".format(", ".join(cdlist)) + return self.irc.reply(event, liststr) + except Exception: pass + + def item_detail(self, nick, userhost, event, from_admin, groups): + """State when the provided item will occur. + + Args: + nick source nickname (unused) + userhost source userhost (unused) + event IRC event, target used to filter item names + from_admin whether or not the event came from an admin (unused) + groups tuple length 1, the item name to look up + + """ + + try: + name = groups[0] + + # determine if source is a channel of a privmsg + source = event.target() + if source == self.irc.server.get_nickname(): + source = irclib.nm_to_n(event.source()) + + time = self._get_countdown_item_time(name, source) + if time: rdelta = relativedelta(time, datetime.now().replace(tzinfo=tzlocal())) - relstr = item + ' will occur in ' + relstr = "{0:s} will occur in ".format(name) if rdelta.years != 0: - relstr += str(rdelta.years) + ' years ' - if rdelta.years > 1: - relstr += 's' - relstr += ', ' + relstr += "{0:s} year{1:s} ".format(str(rdelta.years), + "s" if rdelta.years != 1 else "") if rdelta.months != 0: - relstr += str(rdelta.months) + ' month' - if rdelta.months > 1: - relstr += 's' - relstr += ', ' + relstr += "{0:s} month{1:s}, ".format(str(rdelta.months), + "s" if rdelta.months != 1 else "") if rdelta.days != 0: - relstr += str(rdelta.days) + ' day' - if rdelta.days > 1: - relstr += 's' - relstr += ', ' + relstr += "{0:s} day{1:s}, ".format(str(rdelta.days), + "s" if rdelta.days != 1 else "") if rdelta.hours != 0: - relstr += str(rdelta.hours) + ' hour' - if rdelta.hours > 1: - relstr += 's' - relstr += ', ' + relstr += "{0:s} hour{1:s}, ".format(str(rdelta.hours), + "s" if rdelta.hours != 1 else "") if rdelta.minutes != 0: - relstr += str(rdelta.minutes) + ' minute' - if rdelta.minutes > 1: - relstr += 's' - relstr += ', ' + relstr += "{0:s} minute{1:s}, ".format(str(rdelta.minutes), + "s" if rdelta.minutes != 1 else "") if rdelta.seconds != 0: - relstr += str(rdelta.seconds) + ' second' - if rdelta.seconds > 1: - relstr += 's' - relstr += ', ' + relstr += "{0:s} second{1:s}, ".format(str(rdelta.seconds), + "s" if rdelta.seconds != 1 else "") + # remove trailing comma from output return self.irc.reply(event, relstr[0:-2]) - except NoOptionError: pass + except mdb.Error: pass + + def _add_countdown_item_to_database(self, name, source, time): + """Add the given countdown item from the given source to the database. + + Args: + name the name of the item to add + source the source (nick or channel) this item is from + time the datetime of the item to add + + """ + + db = self.get_db() + try: + cur = db.cursor(mdb.cursors.DictCursor) + statement = ''' + INSERT INTO countdown_item (name, source, time) + VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE time=%s + ''' + cur.execute(statement, (name, source, time, time)) + db.commit() + return cur.lastrowid + except mdb.Error as e: + db.rollback() + self.log.error("database error during adding countdown item") + self.log.exception(e) + raise + finally: cur.close() + + def _remove_countdown_item_from_database(self, name, source): + """Add the given countdown item from the given source to the database. + + Args: + name the name of the item to remove + source the source (nick or channel) this item is from + + """ + + db = self.get_db() + try: + cur = db.cursor(mdb.cursors.DictCursor) + statement = ''' + DELETE FROM countdown_item WHERE name = %s + AND source = %s + ''' + cur.execute(statement, (name, source)) + db.commit() + return + except mdb.Error as e: + db.rollback() + self.log.error("database error during removing countdown item") + self.log.exception(e) + raise + finally: cur.close() + + def _list_countdown_items(self, source): + """Retrieve the names of all countdown items in the database. + + Args: + source the source (nick or channel) to retrieve items for + + Returns: + The names of the items, as a list of strings. + + """ + + items = [] + db = self.get_db() + try: + cur = db.cursor(mdb.cursors.DictCursor) + query = ''' + SELECT name FROM countdown_item + WHERE source = %s + ''' + cur.execute(query, (source,)) + results = cur.fetchall() + for result in results: + items.append(result['name']) + + return items + except mdb.Error as e: + self.log.error("database error while getting item time") + self.log.exception(e) + raise + finally: cur.close() + + def _get_countdown_item_time(self, name, source): + """Add the given countdown item from the given source to the database. + + Args: + name the name of the item to add + source the source (nick or channel) this item is from + + Returns: + The datetime for the item in local time. + + """ + + db = self.get_db() + try: + cur = db.cursor(mdb.cursors.DictCursor) + query = ''' + SELECT time FROM countdown_item + WHERE name = %s AND source = %s + ''' + cur.execute(query, (name, source)) + result = cur.fetchone() + if result: + return result['time'].replace(tzinfo=tzutc()) + except mdb.Error as e: + self.log.error("database error while getting item time") + self.log.exception(e) + raise + finally: cur.close() # vi:tabstop=4:expandtab:autoindent # kate: indent-mode python;indent-width 4;replace-tabs on;