Brian S. Stephan
ef66c855f3
stats on users are tracked, and achievements are defined by writing sql queries against those stats. silly fun
480 lines
18 KiB
Python
480 lines
18 KiB
Python
"""
|
|
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 <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
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
|