""" Achievements - gamifying IRC Copyright (C) 2011 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 . """ import re import thread import time import MySQLdb as mdb from Module import Module from extlib import irclib __author__ = "Brian S. Stephan" __copyright__ = "Copyright 2011, Brian S. Stephan" __credits__ = ["Brian S. Stephan", "#lh"] __license__ = "GPL" __version__ = "0.1" __maintainer__ = "Brian S. Stephan" __email__ = "bss@incorporeal.org" __status__ = "Development" class Achievements(Module): """Give out achievements for doing stuff on IRC, because why not.""" class AchievementsSettings(): """Track system settings.""" pass def __init__(self, irc, config): """Set up trigger regexes.""" # TODO joinpattern = '^!achievements\s+join$' leavepattern = '^!achievements\s+leave$' infopattern = '^!achievements\s+info\s+(.*)$' rankpattern = '^!achievements\s+rank\s+(.*)$' self.joinre = re.compile(joinpattern) self.leavere = re.compile(leavepattern) self.infore = re.compile(infopattern) self.rankre = re.compile(rankpattern) Module.__init__(self, irc, config) self.next_achievements_scan = 0 thread.start_new_thread(self.thread_do, ()) def db_init(self): """Set up the database tables, if they don't exist.""" version = self.db_module_registered(self.__class__.__name__) if (version == None): # have to create the database tables db = self.get_db() try: version = 1 cur = db.cursor(mdb.cursors.DictCursor) cur.execute(''' CREATE TABLE achievements_player ( id SERIAL, nick VARCHAR(64) NOT NULL UNIQUE, userhost VARCHAR(256) NOT NULL DEFAULT '', is_playing INTEGER NOT NULL DEFAULT 0, last_seen_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ''') cur.execute(''' CREATE TABLE achievements_event ( id SERIAL, player_id BIGINT(20) UNSIGNED NOT NULL, event VARCHAR(64) NOT NULL, target VARCHAR(64), msg_len INTEGER, event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(player_id) REFERENCES achievements_player(id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ''') cur.execute(''' CREATE TABLE achievements_achievement ( id SERIAL, name VARCHAR(256) NOT NULL, description VARCHAR(256) NOT NULL, query VARCHAR(1024) NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ''') cur.execute(''' CREATE TABLE achievements_log ( id SERIAL, player_id BIGINT(20) UNSIGNED NOT NULL, achievement_id BIGINT(20) UNSIGNED NOT NULL, event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(player_id) REFERENCES achievements_player(id), FOREIGN KEY(achievement_id) REFERENCES achievements_achievement(id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ''') cur.execute(''' CREATE TABLE achievements_config ( channel TEXT NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ''') cur.execute(''' CREATE TABLE achievements_filter ( id SERIAL, filter VARCHAR(256) NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ''') cur.execute(''' CREATE TABLE achievements_filter_log ( id SERIAL, filter_id BIGINT(20) UNSIGNED NOT NULL, event_id BIGINT(20) UNSIGNED NOT NULL, FOREIGN KEY(filter_id) REFERENCES achievements_filter(id), FOREIGN KEY(event_id) REFERENCES achievements_event(id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ''') 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) raise finally: cur.close() def register_handlers(self): """Handle all sorts of things to track.""" self.irc.server.add_global_handler('pubmsg', self.track_irc_event) self.irc.server.add_global_handler('pubnotice', self.track_irc_event) self.irc.server.add_global_handler('privmsg', self.track_irc_event) self.irc.server.add_global_handler('privnotice', self.track_irc_event) self.irc.server.add_global_handler('join', self.track_irc_event) self.irc.server.add_global_handler('kick', self.track_irc_event) self.irc.server.add_global_handler('mode', self.track_irc_event) self.irc.server.add_global_handler('part', self.track_irc_event) self.irc.server.add_global_handler('quit', self.track_irc_event) self.irc.server.add_global_handler('invite', self.track_irc_event) self.irc.server.add_global_handler('action', self.track_irc_event) self.irc.server.add_global_handler('topic', self.track_irc_event) self.irc.server.add_global_handler('pubmsg', self.on_pub_or_privmsg) self.irc.server.add_global_handler('privmsg', self.on_pub_or_privmsg) def unregister_handlers(self): self.irc.server.remove_global_handler('pubmsg', self.track_irc_event) self.irc.server.remove_global_handler('pubnotice', self.track_irc_event) self.irc.server.remove_global_handler('privmsg', self.track_irc_event) self.irc.server.remove_global_handler('privnotice', self.track_irc_event) self.irc.server.remove_global_handler('join', self.track_irc_event) self.irc.server.remove_global_handler('kick', self.track_irc_event) self.irc.server.remove_global_handler('mode', self.track_irc_event) self.irc.server.remove_global_handler('part', self.track_irc_event) self.irc.server.remove_global_handler('quit', self.track_irc_event) self.irc.server.remove_global_handler('invite', self.track_irc_event) self.irc.server.remove_global_handler('action', self.track_irc_event) self.irc.server.remove_global_handler('topic', self.track_irc_event) self.irc.server.remove_global_handler('pubmsg', self.on_pub_or_privmsg) self.irc.server.remove_global_handler('privmsg', self.on_pub_or_privmsg) def track_irc_event(self, connection, event): """Put events in the database.""" if event.source(): if event.source().find('!') >= 0: nick = irclib.nm_to_n(event.source()) userhost = irclib.nm_to_uh(event.source()) self.log.debug('good: ' + nick + ' ' + userhost + ' ' + event.eventtype() + ' ' + str(event.target())) if event.arguments(): msg = event.arguments()[0] msg_len = len(event.arguments()[0]) else: msg = '' msg_len = 0 player_id = self._get_or_add_player(nick, userhost) self._add_event(player_id, event.eventtype(), event.target(), msg, msg_len) else: self.log.debug('bad: ' + event.source() + ' ' + event.eventtype()) else: self.log.debug('really bad: ' + event.eventtype()) def do(self, connection, event, nick, userhost, what, admin_unlocked): """Do stuff when commanded.""" if self.joinre.search(what): return self.irc.reply(event, self._join_system(nick)) elif self.leavere.search(what): return self.irc.reply(event, self._leave_system(nick)) elif self.infore.search(what): match = self.infore.search(what) achievement = match.group(1) desc = self._get_achievement_info(achievement) if desc: return self.irc.reply(event, achievement + ': ' + desc) elif self.rankre.search(what): match = self.rankre.search(what) player = match.group(1) achievements = self._get_player_achievements(player) if len(achievements): return self.irc.reply(event, player + ' has obtained ' + ', '.join(achievements)) def thread_do(self): """Do the scan for achievements and other miscellaneous tasks.""" while not self.is_shutdown: self._do_achievement_scan() time.sleep(1) def _get_or_add_player(self, nick, userhost): """Add a player to the database, or update the existing one, and return the id.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) statement = ''' INSERT IGNORE INTO achievements_player (nick) VALUES (%s)''' cur.execute(statement, (nick,)) statement = ''' UPDATE achievements_player SET userhost = %s, last_seen_time = CURRENT_TIMESTAMP WHERE nick = %s''' cur.execute(statement, (userhost, nick)) db.commit() statement = '''SELECT id FROM achievements_player WHERE nick = %s''' cur.execute(statement, (nick,)) result = cur.fetchone() return result['id'] except mdb.Error as e: self.log.error("database error getting or adding player") self.log.exception(e) raise finally: cur.close() def _add_event(self, player_id, event, target, msg, msg_len): """Add an event to the log.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) statement = ''' INSERT INTO achievements_event ( player_id, event, target, msg_len ) VALUES (%s, %s, %s, %s) ''' cur.execute(statement, (player_id, event, target, msg_len)) db.commit() event_id = cur.lastrowid # now see if the event matched any filters query = ''' SELECT id FROM achievements_filter WHERE %s REGEXP filter ''' cur.execute(query, (msg,)) results = cur.fetchall() for result in results: cur = db.cursor(mdb.cursors.DictCursor) statement = ''' INSERT INTO achievements_filter_log (filter_id, event_id) VALUES (%s, %s) ''' cur.execute(statement, (result['id'], event_id)) db.commit() return event_id except mdb.Error as e: self.log.error("database error adding event") self.log.exception(e) raise finally: cur.close() def _get_achievements_settings(self): """Get the report settings.""" db = self.get_db() try: # get the settings cur = db.cursor(mdb.cursors.DictCursor) query = 'SELECT channel FROM achievements_config' cur.execute(query) result = cur.fetchone() if result: settings = self.AchievementsSettings() settings.channel = result['channel'] return settings else: return None except mdb.Error as e: self.log.error("database error getting settings") self.log.exception(e) raise except AttributeError as e: self.log.error("could not get channel settings, probably unset") self.log.exception(e) return None finally: cur.close() def _join_system(self, nick): """Add the appropriate nick to the system.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) statement = 'UPDATE achievements_player SET is_playing = 1 WHERE nick = %s' cur.execute(statement, (nick,)) db.commit() return nick + ' joined.' except mdb.Error as e: self.log.error("database error joining the system") self.log.exception(e) raise finally: cur.close() def _leave_system(self, nick): """Remove the appropriate nick from the system.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) statement = 'UPDATE achievements_player SET is_playing = 0 WHERE nick = %s' cur.execute(statement, (nick,)) db.commit() return nick + ' left.' except mdb.Error as e: self.log.error("database error leaving the system") self.log.exception(e) raise finally: cur.close() def _add_player_to_achievement_log(self, player_id, achievement_id): """Log the success of a player.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) statement = 'INSERT INTO achievements_log (player_id, achievement_id) VALUES (%s, %s)' cur.execute(statement, (player_id, achievement_id)) db.commit() return except mdb.Error as e: self.log.error("database error adding player to achievement log") self.log.exception(e) raise finally: cur.close() def _get_achievement_info(self, achievement): """Return the description of a given achievement.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) query = 'SELECT description FROM achievements_achievement WHERE name = %s' cur.execute(query, (achievement,)) result = cur.fetchone() if result: return result['description'] except mdb.Error as e: self.log.error("database error getting achievement info") self.log.exception(e) raise finally: cur.close() def _get_player_achievements(self, nick): """Return the achievements the nick has.""" achievements = [] db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) query = ''' SELECT a.name FROM achievements_achievement a INNER JOIN achievements_log l ON l.achievement_id = a.id INNER JOIN achievements_player p ON p.id = l.player_id WHERE p.nick = %s ''' cur.execute(query, (nick,)) results = cur.fetchall() for result in results: achievements.append(result['name']) return achievements except mdb.Error as e: self.log.error("database error getting player achievements") self.log.exception(e) raise finally: cur.close() def _do_achievement_scan(self): """Run the queries in the database, seeing if anyone new has an achievement.""" # don't do anything the first time if self.next_achievements_scan == 0: self.next_achievements_scan = time.time() + 300 if self.next_achievements_scan < time.time(): self.next_achievements_scan = time.time() + 300 self.log.debug('in achievement scan') settings = self._get_achievements_settings() if settings is not None: channel = settings.channel achievers = self._query_for_new_achievers() for achiever in achievers: self.sendmsg(channel, achiever[0] + ' achieved ' + achiever[1] + '!') def _query_for_new_achievers(self): """Get new achievement earners for each achievement.""" achievers = [] self.log.debug('checking achievements') db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) query = 'SELECT id, name, query FROM achievements_achievement' cur.execute(query) achievements = cur.fetchall() for achievement in achievements: self.log.debug('checking achievement:[' + achievement['name'] + ']') query = ''' SELECT p.id, p.nick FROM achievements_player p WHERE p.nick IN ( ''' query = query + achievement['query'] query = query + ''' ) AND p.is_playing = 1 AND p.id NOT IN ( SELECT player_id FROM achievements_log l INNER JOIN achievements_achievement a ON a.id = l.achievement_id WHERE a.name = %s ) ''' cur.execute(query, (achievement['name'],)) ach_achievers = cur.fetchall() for ach_achiever in ach_achievers: self.log.debug('name:[' + ach_achiever['nick'] + '] achievement:[' + achievement['name'] + ']') self._add_player_to_achievement_log(ach_achiever['id'], achievement['id']) achievers.append((ach_achiever['nick'], achievement['name'])) return achievers except mdb.Error as e: self.log.error("database error scanning new achievers") self.log.exception(e) raise finally: cur.close() # vi:tabstop=4:expandtab:autoindent