""" 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 sqlite3 import thread import time 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, server): """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, server) self.connection = None 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 db.execute(''' CREATE TABLE achievements_player ( id INTEGER PRIMARY KEY AUTOINCREMENT, nick TEXT NOT NULL UNIQUE, userhost TEXT NOT NULL DEFAULT '', is_playing INTEGER NOT NULL DEFAULT 0, last_seen_time TEXT DEFAULT CURRENT_TIMESTAMP )''') db.execute(''' CREATE TABLE achievements_event ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER NOT NULL, event TEXT NOT NULL, target TEXT, msg_len INTEGER, event_time TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(player_id) REFERENCES achievements_player(id) )''') db.execute(''' CREATE TABLE achievements_achievement ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, desc TEXT NOT NULL, query TEXT NOT NULL )''') db.execute(''' CREATE TABLE achievements_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER NOT NULL, achievement_id INTEGER NOT NULL, event_time TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(player_id) REFERENCES achievements_player(id), FOREIGN KEY(achievement_id) REFERENCES achievements_achievement(id) )''') db.commit() db.close() self.db_register_module_version(self.__class__.__name__, version) except sqlite3.Error as e: db.rollback() db.close() self.log.error("sqlite error: " + str(e)) raise if (version < 2): db = self.get_db() try: version = 2 db.execute(''' CREATE TABLE achievements_config ( channel TEXT NOT NULL )''') db.commit() db.close() self.db_register_module_version(self.__class__.__name__, version) except sqlite3.Error as e: db.rollback() db.close() self.log.error("sqlite error: " + str(e)) raise if (version < 3): db = self.get_db() try: version = 3 db.execute(''' CREATE TABLE achievements_filter ( id INTEGER PRIMARY KEY AUTOINCREMENT, filter TEXT NOT NULL )''') db.execute(''' CREATE TABLE achievements_filter_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, filter_id INTEGER NOT NULL, event_id INTEGER NOT NULL, FOREIGN KEY(filter_id) REFERENCES achievements_filter(id), FOREIGN KEY(event_id) REFERENCES achievements_event(id) )''') db.commit() db.close() self.db_register_module_version(self.__class__.__name__, version) except sqlite3.Error as e: db.rollback() db.close() self.log.error("sqlite error: " + str(e)) raise def register_handlers(self): """Handle all sorts of things to track.""" self.server.add_global_handler('pubmsg', self.track_irc_event) self.server.add_global_handler('pubnotice', self.track_irc_event) self.server.add_global_handler('privmsg', self.track_irc_event) self.server.add_global_handler('privnotice', self.track_irc_event) self.server.add_global_handler('join', self.track_irc_event) self.server.add_global_handler('kick', self.track_irc_event) self.server.add_global_handler('mode', self.track_irc_event) self.server.add_global_handler('part', self.track_irc_event) self.server.add_global_handler('quit', self.track_irc_event) self.server.add_global_handler('invite', self.track_irc_event) self.server.add_global_handler('action', self.track_irc_event) self.server.add_global_handler('topic', self.track_irc_event) self.server.add_global_handler('pubmsg', self.on_pub_or_privmsg) self.server.add_global_handler('privmsg', self.on_pub_or_privmsg) def unregister_handlers(self): self.server.remove_global_handler('pubmsg', self.track_irc_event) self.server.remove_global_handler('pubnotice', self.track_irc_event) self.server.remove_global_handler('privmsg', self.track_irc_event) self.server.remove_global_handler('privnotice', self.track_irc_event) self.server.remove_global_handler('join', self.track_irc_event) self.server.remove_global_handler('kick', self.track_irc_event) self.server.remove_global_handler('mode', self.track_irc_event) self.server.remove_global_handler('part', self.track_irc_event) self.server.remove_global_handler('quit', self.track_irc_event) self.server.remove_global_handler('invite', self.track_irc_event) self.server.remove_global_handler('action', self.track_irc_event) self.server.remove_global_handler('topic', self.track_irc_event) self.server.remove_global_handler('pubmsg', self.on_pub_or_privmsg) self.server.remove_global_handler('privmsg', self.on_pub_or_privmsg) def track_irc_event(self, connection, event): """Put events in the database.""" self.connection = connection 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.reply(connection, event, self._join_system(nick)) elif self.leavere.search(what): return self.reply(connection, 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.reply(connection, 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.reply(connection, 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.""" try: db = self.get_db() cur = db.cursor() statement = ''' INSERT OR IGNORE INTO achievements_player (nick) VALUES (?)''' cur.execute(statement, (nick,)) statement = ''' UPDATE achievements_player SET userhost = ?, last_seen_time = CURRENT_TIMESTAMP WHERE nick = ?''' cur.execute(statement, (userhost, nick)) db.commit() statement = '''SELECT id FROM achievements_player WHERE nick = ?''' cur = db.execute(statement, (nick,)) result = cur.fetchone() db.close() return result['id'] except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise def _add_event(self, player_id, event, target, msg, msg_len): """Add an event to the log.""" try: db = self.get_db() cur = db.cursor() statement = ''' INSERT INTO achievements_event ( player_id, event, target, msg_len ) VALUES (?, ?, ?, ?) ''' 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 ? REGEXP filter ''' cursor = db.execute(query, (msg.decode('utf-8', 'replace'),)) results = cursor.fetchall() for result in results: cur = db.cursor() statement = ''' INSERT INTO achievements_filter_log (filter_id, event_id) VALUES (?, ?) ''' cur.execute(statement, (result['id'], event_id)) db.commit() db.close() return event_id except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise def _get_achievements_settings(self): """Get the report settings.""" try: # get the settings db = self.get_db() query = 'SELECT channel FROM achievements_config' cursor = db.execute(query) result = cursor.fetchone() db.close() if result: settings = self.AchievementsSettings() settings.channel = result['channel'] return settings except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise def _join_system(self, nick): """Add the appropriate nick to the game.""" try: db = self.get_db() cur = db.cursor() statement = 'UPDATE achievements_player SET is_playing = 1 WHERE nick = ?' cur.execute(statement, (nick,)) db.commit() db.close() return nick + ' joined.' except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise def _leave_system(self, nick): """Remove the appropriate nick from the game.""" try: db = self.get_db() cur = db.cursor() statement = 'UPDATE achievements_player SET is_playing = 0 WHERE nick = ?' cur.execute(statement, (nick,)) db.commit() db.close() return nick + ' left.' except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise def _add_player_to_achievement_log(self, player_id, achievement_id): """Log the success of a player.""" try: db = self.get_db() cur = db.cursor() statement = 'INSERT INTO achievements_log (player_id, achievement_id) VALUES (?, ?)' cur.execute(statement, (player_id, achievement_id)) db.commit() db.close() return except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise def _get_achievement_info(self, achievement): """Return the description of a given achievement.""" try: db = self.get_db() query = 'SELECT desc FROM achievements_achievement WHERE name = ?' cursor = db.execute(query, (achievement,)) result = cursor.fetchone() db.close() if result: return result['desc'] except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise def _get_player_achievements(self, nick): """Return the achievements the nick has.""" achievements = [] try: db = self.get_db() 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 = ? ''' cursor = db.execute(query, (nick,)) results = cursor.fetchall() db.close() for result in results: achievements.append(result['name']) return achievements except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise 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 if self.connection is None: return settings = self._get_achievements_settings() channel = settings.channel achievers = self._query_for_new_achievers() for achiever in achievers: self.sendmsg(self.connection, channel, achiever[0] + ' achieved ' + achiever[1] + '!') def _query_for_new_achievers(self): """Get new achievement earners for each achievement.""" achievers = [] try: db = self.get_db() query = 'SELECT id, name, query FROM achievements_achievement' cursor = db.execute(query) achievements = cursor.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 = ? ) ''' cursor = db.execute(query, (achievement['name'],)) ach_achievers = cursor.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'])) db.close() return achievers except sqlite3.Error as e: db.close() self.log.error('sqlite error: ' + str(e)) raise # vi:tabstop=4:expandtab:autoindent