From ef66c855f34ece6dd359e727f8e9b4010636ac4f Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Fri, 21 Oct 2011 17:01:49 -0500 Subject: [PATCH] Achievements: new dumb feature, add irc achievements stats on users are tracked, and achievements are defined by writing sql queries against those stats. silly fun --- modules/Achievements.py | 479 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 479 insertions(+) create mode 100644 modules/Achievements.py diff --git a/modules/Achievements.py b/modules/Achievements.py new file mode 100644 index 0000000..1e09913 --- /dev/null +++ b/modules/Achievements.py @@ -0,0 +1,479 @@ +""" +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() + print("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() + print("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() + print("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()) + print('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: + print('bad: ' + event.source() + ' ' + event.eventtype()) + else: + print('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() + print('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() + print('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() + print('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() + print('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() + print('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() + print('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() + print('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() + print('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: + print('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: + print('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() + print('sqlite error: ' + str(e)) + raise + +# vi:tabstop=4:expandtab:autoindent