# -*- coding: utf-8 -*- ### # Copyright (c) 2013, tann # All rights reserved. # # ### import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks import supybot.ircdb as ircdb import supybot.ircmsgs as ircmsgs import supybot.schedule as schedule import supybot.log as log import supybot.conf as conf import os import re import sqlite3 import random import time import datetime import unicodedata import hashlib #A list with items that are removed when timeout is reached, values must be unique class TimeoutList: def __init__(self, timeout): self.timeout = timeout self.dict = {} def setTimeout(self, timeout): self.timeout = timeout def clearTimeout(self): for k, t in self.dict.items(): if t < (time.time() - self.timeout): del self.dict[k] def append(self, key): self.clearTimeout() self.dict[key] = time.time() def has(self, key): self.clearTimeout() if key in self.dict: return True return False def getTimeLeft(self, key): return self.timeout - (time.time() - self.dict[key]) class TriviaTime(callbacks.Plugin): """ TriviaTime - An enhanced multiplayer and multichannel trivia game for Supybot. Includes KAOS: work together to get all the answers before time runs out. """ threaded = True # enables threading for supybot plugin currentDBVersion = 1.2 def __init__(self, irc): log.info('** TriviaTime loaded! **') self.__parent = super(TriviaTime, self) self.__parent.__init__(irc) # games info self.games = {} # separate game for each channel self.voiceTimeouts = TimeoutList(self.registryValue('voice.timeoutVoice')) #Database amend statements for outdated versions self.dbamends = {} #Formatted like this: : "; ;" (This IS valid SQL as long as we include the semicolons) #logger self.logger = self.Logger(self) # connections dbLocation = self.registryValue('admin.sqlitedb') # tuple head, tail ('example/example/', 'filename.txt') dbFolder = os.path.split(dbLocation) # take folder from split dbFolder = dbFolder[0] # create the folder if not os.path.exists(dbFolder): log.info('The database location did not exist, creating folder structure') os.makedirs(dbFolder) self.storage = self.Storage(dbLocation) #self.storage.dropActivityTable() self.storage.makeActivityTable() #self.storage.dropUserLogTable() self.storage.makeUserLogTable() #self.storage.dropGameTable() self.storage.makeGameTable() #self.storage.dropGameLogTable() self.storage.makeGameLogTable() #self.storage.dropUserTable() self.storage.makeUserTable() #self.storage.dropLoginTable() self.storage.makeLoginTable() #self.storage.dropReportTable() self.storage.makeReportTable() #self.storage.dropQuestionTable() self.storage.makeQuestionTable() #self.storage.dropTemporaryQuestionTable() self.storage.makeTemporaryQuestionTable() #self.storage.dropEditTable() self.storage.makeEditTable() #self.storage.dropDeleteTable() self.storage.makeDeleteTable() self.storage.makeInfoTable() #triviainfo table check #if self.storage.isTriviaVersionSet(): if self.storage.getVersion() != None and self.storage.getVersion() != self.currentDBVersion: return def _games(self): for (network, games) in self.games.items(): for (channel, game) in games.items(): yield game def die(self): for game in self._games(): game.stop() def reset(self): for game in self._games(): game.stop() def doPrivmsg(self, irc, msg): """ Catches all PRIVMSG, including channels communication """ username = self.getUsername(msg.nick, msg.prefix) channel = msg.args[0] # Make sure that it is starting inside of a channel, not in pm if not irc.isChannel(channel): return if callbacks.addressed(irc.nick, msg): return channelCanonical = ircutils.toLower(channel) kaosRemainingCommand= self.registryValue('commands.showHintCommandKAOS', channel) otherHintCommand = self.registryValue('commands.extraHint', channel) game = self.getGame(irc, channel) if game is not None: # Look for command to list remaining KAOS if msg.args[1] == kaosRemainingCommand and game.question.find("KAOS:") == 0: irc.sendMsg(ircmsgs.notice(msg.nick, "'{0}' now also works for KAOS hints, check it out!".format(otherHintCommand))) game.getRemainingKAOS() elif msg.args[1] == otherHintCommand: if game.question.find("KAOS:") == 0: game.getRemainingKAOS() else: game.getOtherHint() else: # check the answer game.checkAnswer(msg) def doJoin(self, irc, msg): username = self.getUsername(msg.nick, msg.prefix) channel = msg.args[0] self.handleVoice(irc, msg.nick, username, channel) def doNotice(self, irc, msg): username = msg.nick if msg.args[1][1:5] == "PING": pingMsg = msg.args[1][6:] pingMsg = pingMsg[:-1] pingMsg = pingMsg.split('*', 1) if len(pingMsg) == 2: pingTime = time.time()-float(pingMsg[0])-1300000000 channelHash = pingMsg[1] channel = '' for name in irc.state.channels: if channelHash == self.shortHash(ircutils.toLower(name)): if username in irc.state.channels[name].users: channel = name break if channel == '': irc.sendMsg(ircmsgs.notice(username, 'Ping reply: %0.2f seconds' % (pingTime))) else: irc.sendMsg(ircmsgs.privmsg(channel, '%s: Ping reply: %0.2f seconds' % (username, pingTime))) def voiceUser(self, irc, nick, username, channel): usernameCanonical = ircutils.toLower(username) irc.queueMsg(ircmsgs.voice(channel, nick)) self.voiceTimeouts.append(usernameCanonical) #The following functions are not ready and still in testing. Actually, they haven't even been tested yet. Use at your own risk. """ def checkLevel(self, irc, nick, username, channel): usernameCanonical = ircutils.toLower(username) #RETRIEVE # questions answers if USERLEVEL == NONE: changeLevel(noob) elif QUESTIONS_ANSWERED > 6000000: changeLevel(Trivia God) elif QUESTIONS_ANSWERED > 4500000: changeLevel(All Is KAOS) elif QUESTIONS_ANSWERED > 4000000: changeLevel(Elite) elif QUESTIONS_ANSWERED > 3000000: changelevel(Addicted) elif QUESTIONS_ANSWERED > 2500000: changelevel(Distinguished) elif QUESTIONS_ANSWERED > 2000000: changelevel(Genius) elif QUESTIONS_ANSWERED > 1500000: changelevel(Master) elif QUESTIONS_ANSWERED > 1000000: changelevel(Player) elif QUESTIONS_ANSWERED > 500000: changelevel(Student) elif QUESTIONS_ANSWERED > 100000: changelevel(Guesser) elif QUESTIONS_ANSWERED > 1: changelLevel(noob) def changeLevel(self, irc, nick, username, channel): usernameCanonical - ircutils.toLower(username) USERLEVEL = NEWUSERLEVEL irc.sendMsg(ircmsgs.privmsg(channel, 'Congratulations %s, you\'ve answered %q questions and leveled up to %l!' % (username, questions, newlevel))) """ def handleVoice(self, irc, nick, username, channel): if not self.registryValue('voice.enableVoice'): return timeoutVoice = self.registryValue('voice.timeoutVoice') self.voiceTimeouts.setTimeout(timeoutVoice) usernameCanonical = ircutils.toLower(username) dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalStats'): stat = threadStorage.getUserStat(username, None) rank = threadStorage.getUserRank(username, None) else: stat = threadStorage.getUserStat(username, channel) rank = threadStorage.getUserRank(username, channel) if stat and not self.voiceTimeouts.has(usernameCanonical): numTopToVoice = self.registryValue('voice.numTopToVoice') minPointsVoiceYear = self.registryValue('voice.minPointsVoiceYear') minPointsVoiceMonth = self.registryValue('voice.minPointsVoiceMonth') minPointsVoiceWeek = self.registryValue('voice.minPointsVoiceWeek') if rank['year'] <= numTopToVoice and stat['points_year'] >= minPointsVoiceYear: self.voiceUser(irc, nick, username, channel) irc.sendMsg(ircmsgs.privmsg(channel, 'Giving voice to %s for being MVP this YEAR (#%d)' % (nick, rank['year']))) elif rank['month'] <= numTopToVoice and stat['points_month'] >= minPointsVoiceMonth: self.voiceUser(irc, nick, username, channel) irc.sendMsg(ircmsgs.privmsg(channel, 'Giving voice to %s for being MVP this MONTH (#%d)' % (nick, rank['month']))) elif rank['week'] <= numTopToVoice and stat['points_week'] >= minPointsVoiceWeek: self.voiceUser(irc, nick, username, channel) irc.sendMsg(ircmsgs.privmsg(channel, 'Giving voice to %s for being MVP this WEEK (#%d)' % (nick, rank['week']))) def addZeroWidthSpace(self, text): if len(text) <= 1: return text s = u'%s\u200b%s' % (text[:1], text[1:]) return s.encode('utf-8') def shortHash(self, text): hashText = hashlib.sha1(text).hexdigest() hashText = self.numToBase94(int(hashText, 16), 8) return hashText def numToBase94(self, n, maxChars): chars = '!"#$%&\'() +,-./0123456789:;<=>?@ABCDEFGHUJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' L = [] for i in range(maxChars): L.append(chars[n % len(chars)]) n = int(n / len(chars)) return ''.join(L) def addActivity(self, activityType, activityText, channel, irc, storage=None): if storage is None: dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) else: threadStorage = storage threadStorage.removeOldActivity() threadStorage.insertActivity(activityType, activityText, channel, irc.network) def deleteGame(self, irc, channel): channelCanonical = ircutils.toLower(channel) if irc.network in self.games: if channelCanonical in self.games[irc.network]: del self.games[irc.network][channelCanonical] if len(self.games[irc.network]) < 1: del self.games[irc.network] def createGame(self, irc, channel): if irc.network not in self.games: self.games[irc.network] = {} channelCanonical = ircutils.toLower(channel) newGame = self.Game(irc, channel, self) if newGame.active == True: self.games[irc.network][channelCanonical] = newGame def getGame(self, irc, channel): channelCanonical = ircutils.toLower(channel) if irc.network in self.games: if channelCanonical in self.games[irc.network]: return self.games[irc.network][channelCanonical] return None def isTriviaMod(self, hostmask, channel): channel = ircutils.toLower(channel) cap = self.getTriviaCapability(hostmask, channel) return cap in ['{0},{1}'.format(channel,'triviamod'), '{0},{1}'.format(channel,'triviaadmin'), 'owner'] def isTriviaAdmin(self, hostmask, channel): channel = ircutils.toLower(channel) cap = self.getTriviaCapability(hostmask, channel) return cap in ['{0},{1}'.format(channel,'triviaadmin'), 'owner'] def getTriviaCapability(self, hostmask, channel): if ircdb.users.hasUser(hostmask): channel = ircutils.toLower(channel) caps = list(ircdb.users.getUser(hostmask).capabilities) triviamod = '{0},{1}'.format(channel,'triviamod') triviaadmin = '{0},{1}'.format(channel,'triviaadmin') # If multiple capabilities exist, pick the most important if 'owner' in caps: return 'owner' elif triviaadmin in caps: return triviaadmin elif triviamod in caps: return triviamod else: return 'user' return None def getUsername(self, nick, hostmask): username = nick try: #rootcoma!~rootcomaa@unaffiliated/rootcoma user = ircdb.users.getUser(hostmask) username = user.name except KeyError: pass return username def reply(self, irc, msg, outstr, prefixNick=True): if ircutils.isChannel(msg.args[0]): target = msg.args[0] else: target = msg.nick if prefixNick == False or ircutils.isNick(target): irc.sendMsg(ircmsgs.privmsg(target, outstr)) else: irc.sendMsg(ircmsgs.privmsg(target, '%s: %s' % (msg.nick, outstr))) irc.noReply() def acceptdelete(self, irc, msg, arg, channel, num): """[] Accept a question deletion. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix username = self.getUsername(msg.nick, hostmask) if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be a TriviaMod in {0} to use this command.'.format(channel)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): delete = self.storage.getDeleteById(num) else: delete = self.storage.getDeleteById(num, channel) if delete: if username == delete['username']: irc.reply('You cannot accept your own deletion request.') else: questionNumber = delete['line_num'] irc.reply('Question #%d deleted!' % questionNumber) threadStorage.removeReportByQuestionNumber(questionNumber) threadStorage.removeEditByQuestionNumber(questionNumber) threadStorage.deleteQuestion(questionNumber) threadStorage.removeDelete(num) threadStorage.removeDeleteByQuestionNumber(questionNumber) self.logger.doLog(irc, channel, "%s accepted delete# %i, question #%i deleted" % (msg.nick, num, questionNumber)) activityText = '%s deleted a question, approved by %s' % (delete['username'], msg.nick) self.addActivity('delete', activityText, channel, irc, threadStorage) else: if self.registryValue('general.globalstats'): irc.error('Unable to find delete #{0}.'.format(num)) else: irc.error('Unable to find delete #{0} in {1}.'.format(num, channel)) acceptdelete = wrap(acceptdelete, ['channel', 'int']) def acceptedit(self, irc, msg, arg, channel, num): """[] Accept a question edit, and remove edit. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix username = self.getUsername(msg.nick, hostmask) if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be a TriviaMod in {0} to use this command.'.format(channel)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): edit = self.storage.getEditById(num) else: edit = self.storage.getEditById(num, channel) if edit: if username == edit['username']: irc.reply('You cannot accept your own edit.') else: question = threadStorage.getQuestionById(edit['question_id']) questionOld = question['question'] if question else '' threadStorage.updateQuestion(edit['question_id'], edit['question']) threadStorage.updateUser(edit['username'], 0, 1) threadStorage.removeEdit(edit['id']) threadStorage.removeReportByQuestionNumber(edit['question_id']) irc.reply('Question #%d updated!' % edit['question_id']) self.logger.doLog(irc, channel, "%s accepted edit# %i, question #%i edited NEW: '%s' OLD '%s'" % (msg.nick, num, edit['question_id'], edit['question'], questionOld)) activityText = '%s edited a question, approved by %s' % (edit['username'], msg.nick) self.addActivity('edit', activityText, channel, irc, threadStorage) irc.sendMsg(ircmsgs.notice(msg.nick, 'NEW: %s' % (edit['question']))) if questionOld != '': irc.sendMsg(ircmsgs.notice(msg.nick, 'OLD: %s' % (questionOld))) else: irc.error('Question could not be found for this edit') else: if self.registryValue('general.globalstats'): irc.error('Unable to find edit #{0}.'.format(num)) else: irc.error('Unable to find edit #{0} in {1}.'.format(num, channel)) acceptedit = wrap(acceptedit, ['channel', 'int']) def acceptnew(self, irc, msg, arg, channel, num): """[] Accept a new question, and add it to the database. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix username = self.getUsername(msg.nick, hostmask) if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be a TriviaMod in {0} to use this command.'.format(channel)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): q = threadStorage.getTemporaryQuestionById(num) else: q = threadStorage.getTemporaryQuestionById(num, channel) if q: if username == q['username']: irc.reply('You cannot accept your own new question.') else: threadStorage.updateUser(q['username'], 0, 0, 0, 0, 1) threadStorage.insertQuestionsBulk([(q['question'], q['question'])]) threadStorage.removeTemporaryQuestion(q['id']) irc.reply('Question accepted!') self.logger.doLog(irc, channel, "%s accepted new question #%i, '%s'" % (msg.nick, num, q['question'])) activityText = '%s added a new question, approved by %s' % (q['username'], msg.nick) self.addActivity('new', activityText, channel, irc, threadStorage) else: if self.registryValue('general.globalstats'): irc.error('Unable to find new question #{0}.'.format(num)) else: irc.error('Unable to find new question #{0} in {1}.'.format(num, channel)) acceptnew = wrap(acceptnew, ['channel', 'int']) def add(self, irc, msg, arg, user, channel, question): """[] Adds a question to the database. Channel is only required when using the command outside of a channel. """ username = msg.nick charMask = self.registryValue('hints.charMask', channel) if charMask not in question: irc.error('The question must include the separating character %s ' % (charMask)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) threadStorage.updateUser(username, 0, 0, 0, 1) threadStorage.insertTemporaryQuestion(username, channel, question) irc.reply('Thank you for adding your question to the question database, it is awaiting approval. ') self.logger.doLog(irc, channel, "%s added new question: '%s'" % (username, question)) add = wrap(add, ['user', 'channel', 'text']) def addfile(self, irc, msg, arg, filename): """[] Add a file of questions to the question database, filename defaults to configured question file. """ if filename is None: filename = self.registryValue('admin.quizfile') try: filesLines = open(filename).readlines() except: irc.error('Could not open file to add to database. Make sure it exists on the server.') return irc.reply('Adding questions from %s to database.. This may take a few minutes' % filename) insertList = [] channel = msg.args[0] for line in filesLines: insertList.append((str(line).strip(),str(line).strip())) dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) info = threadStorage.insertQuestionsBulk(insertList) irc.reply('Successfully added %d questions, skipped %d' % (info[0], info[1])) self.logger.doLog(irc, channel, "%s added question file: '%s', added: %i, skipped: %i" % (msg.nick, filename, info[0], info[1])) addfile = wrap(addfile, ['owner', optional('text')]) def authweb(self, irc, msg, arg, channel): """[] This registers triviamods and triviaadmins on the website. Use this command again if the account password has changed. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix capability = self.getTriviaCapability(hostmask, channel) if capability is None or capability == 'user': irc.reply('You must be a TriviaMod in {0} to use this command.'.format(channel)) return user = ircdb.users.getUser(hostmask) salt = '' password = '' isHashed = user.hashed if isHashed: (salt, password) = user.password.split('|') else: password = user.password dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) info = threadStorage.insertLogin(user.name, salt, isHashed, password, capability) irc.reply('Success, updated your web access login.') self.logger.doLog(irc, channel, "%s authed for web access" % (user.name)) authweb = wrap(authweb, ['channel']) def clearpoints(self, irc, msg, arg, channel, username): """[] Deletes all of a users points, and removes all their records. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaAdmin(hostmask, channel) == False: irc.reply('You must be a TriviaAdmin in {0} to use this command.'.format(channel)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) threadStorage.removeUserLogs(username, channel) irc.reply('Removed all points from {0} in {1}.'.format(username, channel)) self.logger.doLog(irc, channel, '{0} cleared points for {1} in {2}.'.format(msg.nick, username, channel)) clearpoints = wrap(clearpoints, ['channel', 'nick']) def day(self, irc, msg, arg, channel, num): """[] [] Displays the top scores of the day. Parameter is optional, display up to that number. (eg 20 - display 11-20) Channel is only required when using the command outside of a channel. """ num = max(num, 10) offset = num-9 dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalStats'): tops = threadStorage.viewDayTop10(None, num) else: tops = threadStorage.viewDayTop10(channel, num) topsList = ['Today\'s Top {0}-{1} Players: '.format(offset, num)] if tops: for i in range(len(tops)): topsList.append('\x02 #%d:\x02 %s %d ' % ((i+offset) , self.addZeroWidthSpace(tops[i]['username']), tops[i]['points'])) else: topsList.append('No players') topsText = ''.join(topsList) self.reply(irc, msg, topsText, prefixNick=False) day = wrap(day, ['channel', optional('int')]) def delete(self, irc, msg, arg, user, channel, t, id, reason): """[] [] [] Deletes a question from the database. Type decides whether to delete by round number (r), or question number (q) (default round). Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix username = self.getUsername(msg.nick, hostmask) dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) # Search for question ID if deletion is by 'round' if t is None or str.lower(t) == "round": q = threadStorage.getQuestionByRound(id, channel) if q: id = q['id'] else: irc.error('Could not find that round #{0}.'.format(id)) return if not threadStorage.questionIdExists(id): irc.error('That question does not exist.') elif threadStorage.isQuestionDeleted(id): irc.error('That question is already deleted.') elif threadStorage.isQuestionPendingDeletion(id): irc.error('That question is already pending deletion.') else: threadStorage.insertDelete(username, channel, id, reason) irc.reply('Question %d marked for deletion and pending review.' % id) self.logger.doLog(irc, channel, "%s marked question #%i for deletion" % (username, id)) delete = wrap(delete, ['user', 'channel', optional(('literal',("question", "QUESTION", "ROUND", "round"))),'int', optional('text')]) def edit(self, irc, msg, arg, user, channel, num, question): """[] Correct a question by providing the question number and the corrected text. Channel is only required when using the command outside of a channel. """ username = self.getUsername(msg.nick, msg.prefix) dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) q = threadStorage.getQuestionById(num) if q: questionParts = question.split('*') if len(questionParts) < 2: oldQuestionParts = q['question'].split('*') questionParts.extend(oldQuestionParts[1:]) question = questionParts[0] for part in questionParts[1:]: question += '*' question += part if question == q['question']: irc.error('Your edit does not change the original question.') else: threadStorage.insertEdit(num, question, username, channel) threadStorage.updateUser(username, 1, 0) irc.reply('Success! Submitted edit for further review.') irc.sendMsg(ircmsgs.notice(msg.nick, 'NEW: %s' % (question))) irc.sendMsg(ircmsgs.notice(msg.nick, 'OLD: %s' % (q['question']))) self.logger.doLog(irc, channel, "%s edited question #%i: OLD: '%s' NEW: '%s'" % (username, num, q['question'], question)) else: irc.error('Question does not exist') edit = wrap(edit, ['user', 'channel', 'int', 'text']) def givepoints(self, irc, msg, arg, channel, username, points, days): """[] [] Give a user points, last argument is optional amount of days in past to add records. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaAdmin(hostmask, channel) == False: irc.reply('You must be a TriviaAdmin in {0} to use this command.'.format(channel)) return elif points < 1: irc.error("You cannot give less than 1 point.") return username = self.getUsername(username, username) day=None month=None year=None if days is not None: d = datetime.date.today() d -= datetime.timedelta(days) day = d.day month = d.month year = d.year dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) threadStorage.updateUserLog(username, channel, points, 0, 0, day, month, year) irc.reply('Added {0} points to {1} in {2}.'.format(points, username, channel)) self.logger.doLog(irc, channel, '{0} gave {1} points to {2} in {3}.'.format(msg.nick, points, username, channel)) givepoints = wrap(givepoints, ['channel', 'nick', 'int', optional('int')]) def listdeletes(self, irc, msg, arg, channel, page): """[] [] List questions pending deletion. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return # Grab list from the database dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): count = threadStorage.countDeletes() else: count = threadStorage.countDeletes(channel) pages = int(count / 3) + int(count % 3 > 0) page = max(1, min(page, pages)) if self.registryValue('general.globalstats'): deletes = threadStorage.getDeleteTop3(page) else: deletes = threadStorage.getDeleteTop3(page, channel=channel) # Output list if count < 1: if self.registryValue('general.globalstats'): irc.reply('No deletes found.') else: irc.reply('No deletes found in {0}.'.format(channel)) else: irc.reply('Showing page %i of %i' % (page, pages)) for delete in deletes: question = threadStorage.getQuestionById(delete['line_num']) questionText = question['question'] if question else 'Question not found' irc.reply('Delete #%d, by %s Question #%d: %s, Reason:%s'%(delete['id'], delete['username'], delete['line_num'], questionText, delete['reason'])) irc.reply('Use the showdelete command to see more information') listdeletes = wrap(listdeletes, ['channel', optional('int')]) def listedits(self, irc, msg, arg, channel, page): """[] [] List edits pending approval. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return # Grab list from the database dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): count = threadStorage.countEdits() else: count = threadStorage.countEdits(channel) pages = int(count / 3) + int(count % 3 > 0) page = max(1, min(page, pages)) if self.registryValue('general.globalstats'): edits = threadStorage.getEditTop3(page) else: edits = threadStorage.getEditTop3(page, channel=channel) # Output list if count < 1: if self.registryValue('general.globalstats'): irc.reply('No edits found.') else: irc.reply('No edits found in {0}.'.format(channel)) else: irc.reply('Showing page %i of %i' % (page, pages)) for edit in edits: irc.reply('Edit #%d, Question #%d, NEW:%s'%(edit['id'], edit['question_id'], edit['question'])) irc.reply('Use the showedit command to see more information') listedits = wrap(listedits, ['channel', optional('int')]) def listreports(self, irc, msg, arg, user, channel, page): """[] [] List reports pending edit. Channel is only required when using the command outside of a channel. """ # Grab list from the database dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): count = threadStorage.countReports() else: count = threadStorage.countReports(channel) pages = int(count / 3) + int(count % 3 > 0) page = max(1, min(page, pages)) if self.registryValue('general.globalstats'): reports = threadStorage.getReportTop3(page) else: reports = threadStorage.getReportTop3(page, channel=channel) # Output list if count < 1: if self.registryValue('general.globalstats'): irc.reply('No reports found.') else: irc.reply('No reports found in {0}.'.format(channel)) else: irc.reply('Showing page %i of %i' % (page, pages)) for report in reports: irc.reply('Report #%d \'%s\' by %s on %s Q#%d '%(report['id'], report['report_text'], report['username'], report['channel'], report['question_num'])) irc.reply('Use the showreport command to see more information') listreports = wrap(listreports, ['user', 'channel', optional('int')]) def listnew(self, irc, msg, arg, channel, page): """[] [] List questions awaiting approval. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return # Grab list from the database dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): count = threadStorage.countTemporaryQuestions() else: count = threadStorage.countTemporaryQuestions(channel) pages = int(count / 3) + int(count % 3 > 0) page = max(1, min(page, pages)) if self.registryValue('general.globalstats'): q = threadStorage.getTemporaryQuestionTop3(page) else: q = threadStorage.getTemporaryQuestionTop3(page, channel=channel) # Output list if count < 1: if self.registryValue('general.globalstats'): irc.reply('No new questions found.') else: irc.reply('No new questions found in {0}.'.format(channel)) else: irc.reply('Showing page %i of %i' % (page, pages)) for ques in q: irc.reply('Temp Q #%d: %s'%(ques['id'], ques['question'])) irc.reply('Use the shownew to see more information') listnew = wrap(listnew, ['channel', optional('int')]) def info(self, irc, msg, arg, channel): """[] Get TriviaTime information, how many questions/users in database, time, etc. Channel is only required when using the command outside of a channel. """ dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) totalUsersEver = threadStorage.getNumUser(channel) numActiveThisWeek = threadStorage.getNumActiveThisWeek(channel) infoText = ' TriviaTime v1.1 by Trivialand on Freenode: https://github.com/tannn/TriviaTime ' self.reply(irc, msg, infoText, prefixNick=False) infoText = ' Time is %s ' % (time.asctime(time.localtime(),)) self.reply(irc, msg, infoText, prefixNick=False) infoText = '\x02 %d Users\x02 on scoreboard with \x02%d Active This Week\x02' % (totalUsersEver, numActiveThisWeek) self.reply(irc, msg, infoText, prefixNick=False) numKaos = threadStorage.getNumKAOS() numQuestionTotal = threadStorage.getNumQuestions() infoText = '\x02 %d Questions\x02 and \x02%d KAOS\x02 (\x02%d Total\x02) in the database ' % ((numQuestionTotal-numKaos), numKaos, numQuestionTotal) self.reply(irc, msg, infoText, prefixNick=False) info = wrap(info, ['channel']) def ping(self, irc, msg, arg): """ Check your ping time to the bot. The client must respond correctly to pings. """ channel = msg.args[0] channelHash = self.shortHash(ircutils.toLower(channel)) username = msg.nick irc.sendMsg(ircmsgs.privmsg(username, '\x01PING %0.2f*%s\x01' % (time.time()-1300000000, channelHash))) ping = wrap(ping) def me(self, irc, msg, arg, channel): """[] Get your rank, score & questions asked for day, month, year. Channel is only required when using the command outside of a channel. """ username = self.getUsername(msg.nick, msg.prefix) identified = ircdb.users.hasUser(msg.prefix) dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalStats'): stat = threadStorage.getUserStat(username, None) rank = threadStorage.getUserRank(username, None) else: stat = threadStorage.getUserStat(username, channel) rank = threadStorage.getUserRank(username, channel) if not stat: errorMessage = 'You do not have any points.' identifyMessage = '' if not identified: identifyMessage = ' You should identify to keep track of your score more accurately.' irc.reply('%s%s' % (errorMessage, identifyMessage)) else: hasPoints = False infoList = ['%s\'s Stats: Points (answers)' % (self.addZeroWidthSpace(stat['username']))] if rank['day'] > 0 or stat['points_day'] > 0 or stat['answer_day'] > 0: hasPoints = True infoList.append(' \x02Today:\x02 #%d %d (%d)' % (rank['day'], stat['points_day'], stat['answer_day'])) if rank['week'] > 0 or stat['points_week'] > 0 or stat['answer_week'] > 0: hasPoints = True infoList.append(' \x02This Week:\x02 #%d %d (%d)' % (rank['week'], stat['points_week'], stat['answer_week'])) if rank['month'] > 0 or stat['points_month'] > 0 or stat['answer_week'] > 0: hasPoints = True infoList.append(' \x02This Month:\x02 #%d %d (%d)' % (rank['month'], stat['points_month'], stat['answer_month'])) if rank['year'] > 0 or stat['points_year'] > 0 or stat['answer_year'] > 0: hasPoints = True infoList.append(' \x02This Year:\x02 #%d %d (%d)' % (rank['year'], stat['points_year'], stat['answer_year'])) if not hasPoints: infoList = ['%s: You do not have any points.' % (username)] if not identified: infoList.append(' You should identify to keep track of your score more accurately.') infoText = ''.join(infoList) self.reply(irc, msg, infoText, prefixNick=False) me = wrap(me, ['channel']) def month(self, irc, msg, arg, channel, num): """[] [] Displays the top ten scores of the month. Parameter is optional, display up to that number. (eg 20 - display 11-20) Channel is only required when using the command outside of a channel. """ num = max(num, 10) offset = num-9 dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalStats'): tops = threadStorage.viewMonthTop10(None, num) else: tops = threadStorage.viewMonthTop10(channel, num) topsList = ['This Month\'s Top {0}-{1} Players: '.format(offset, num)] if tops: for i in range(len(tops)): topsList.append('\x02 #%d:\x02 %s %d ' % ((i+offset) , self.addZeroWidthSpace(tops[i]['username']), tops[i]['points'])) else: topsList.append('No players') topsText = ''.join(topsList) self.reply(irc, msg, topsText, prefixNick=False) month = wrap(month, ['channel', optional('int')]) def next(self, irc, msg, arg, channel): """ Skip to the next question immediately. This can only be used by a user with a certain streak, set in the config. """ username = self.getUsername(msg.nick, msg.prefix) minStreak = self.registryValue('general.nextMinStreak', channel) game = self.getGame(irc, channel) # Sanity checks # 1. Is trivia running? # 2. Is question is still being asked? # 3. Is caller the streak holder? # 4. Is streak high enough? if game is None or game.active == False: self.reply(irc, msg, 'Trivia is not currently running.') elif game.questionOver == False: self.reply(irc, msg, 'You must wait until the current question is over.') elif game.lastWinner != ircutils.toLower(username): self.reply(irc, msg, 'You are not currently the streak holder.') elif game.streak < minStreak: self.reply(irc, msg, 'You do not have a large enough streak yet (%i of %i).' % (game.streak, minStreak)) else: self.reply(irc, msg, 'Onto the next question!', prefixNick=False) game.removeEvent() game.nextQuestion() next = wrap(next, ['onlyInChannel']) def rmedit(self, irc, msg, arg, channel, num): """[] Deny a question edit. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): edit = threadStorage.getEditById(num) else: edit = threadStorage.getEditById(num, channel) if edit: threadStorage.removeEdit(edit['id']) irc.reply('Edit %d removed!' % edit['id']) self.logger.doLog(irc, channel, "%s removed edit# %i, for question #%i, text: %s" % (msg.nick, edit['id'], edit['question_id'], edit['question'])) else: if self.registryValue('general.globalstats'): irc.error('Unable to find edit #{0}.'.format(num)) else: irc.error('Unable to find edit #{0} in {1}.'.format(num, channel)) rmedit = wrap(rmedit, ['channel', 'int']) def rmdelete(self, irc, msg, arg, channel, num): """[] Deny a deletion request. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): delete = threadStorage.getDeleteById(num) else: delete = threadStorage.getDeleteById(num, channel) if delete: threadStorage.removeDelete(num) irc.reply('Delete %d removed!' % num) self.logger.doLog(irc, channel, "%s removed delete# %i, for question #%i, reason was '%s'" % (msg.nick, num, delete['line_num'], delete['reason'])) else: if self.registryValue('general.globalstats'): irc.error('Unable to find delete #{0}.'.format(num)) else: irc.error('Unable to find delete #{0} in {1}.'.format(num, channel)) rmdelete = wrap(rmdelete, ['channel', 'int']) def rmreport(self, irc, msg, arg, channel, num): """[] Delete a report. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): report = threadStorage.getReportById(num) else: report = threadStorage.getReportById(num, channel) if report: threadStorage.removeReport(report['id']) irc.reply('Report %d removed!' % report['id']) self.logger.doLog(irc, channel, "%s removed report# %i, for question #%i text was %s" % (msg.nick, report['id'], report['question_num'], report['report_text'])) else: if self.registryValue('general.globalstats'): irc.error('Unable to find report #{0}.'.format(num)) else: irc.error('Unable to find report #{0} in {1}.'.format(num, channel)) rmreport = wrap(rmreport, ['channel', 'int']) def rmnew(self, irc, msg, arg, channel, num): """[] Deny a new question. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): q = threadStorage.getTemporaryQuestionById(num) else: q = threadStorage.getTemporaryQuestionById(num, channel) if q: threadStorage.removeTemporaryQuestion(q['id']) irc.reply('Temp question #%d removed!' % q['id']) self.logger.doLog(irc, channel, "%s removed new question #%i, '%s'" % (msg.nick, q['id'], q['question'])) else: if self.registryValue('general.globalstats'): irc.error('Unable to find new question #{0}.'.format(num)) else: irc.error('Unable to find new question #{0} in {1}.'.format(num, channel)) rmnew = wrap(rmnew, ['channel', 'int']) def repeat(self, irc, msg, arg, channel): """ Repeat the current question. """ game = self.getGame(irc, channel) # Sanity checks # 1. Is trivia running? # 2. Is a question being asked? # 3. Has the question already been repeated? # 4. Is the question currently blank? if game is None or game.active == False: self.reply(irc, msg, 'Trivia is not currently running.') elif game.questionOver == True: self.reply(irc, msg, 'No question is currently being asked.') elif game.questionRepeated == False and game.question != '': game.repeatQuestion() irc.noReply() else: irc.noReply() repeat = wrap(repeat, ['onlyInChannel']) def report(self, irc, msg, arg, user, channel, roundNum, text): """[channel] Provide a report for a bad question. Be sure to include the round number and the problem(s). Channel is a optional parameter which is only needed when reporting outside of the channel """ username = self.getUsername(msg.nick, msg.prefix) channelCanonical = ircutils.toLower(channel) game = self.getGame(irc, channel) if game is not None: numAsked = game.numAsked questionOver = game.questionOver if text[:2] == 's/': if numAsked == roundNum and questionOver == False: irc.reply('Sorry, you must wait until the current question is over to report it.') return dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) question = threadStorage.getQuestionByRound(roundNum, channel) if question: if text[:2] == 's/': # Regex substitution regex = text[2:].split('/') if len(regex) > 1: threadStorage.updateUser(username, 1, 0) pattern = regex[0] repl = regex[1] try: newQuestionText = re.sub(pattern, repl, question['question']) except: irc.error('Unable to process this regex substitution.') return if newQuestionText == question['question']: # Ignore if no substitutions made irc.error('This regex substitution expression does not change the original question.') else: threadStorage.insertEdit(question['id'], newQuestionText, username, channel) irc.reply('Regex substitution detected: Question edited!') irc.sendMsg(ircmsgs.notice(username, 'NEW: %s' % (newQuestionText))) irc.sendMsg(ircmsgs.notice(username, 'OLD: %s' % (question['question']))) self.logger.doLog(irc, channel, "%s edited question #%i, NEW: '%s', OLD: '%s'" % (msg.nick, question['id'], newQuestionText, question['question'])) else: # Incomplete expression irc.error('Incomplete regex substitution expression.') elif str.lower(utils.str.normalizeWhitespace(text))[:6] == 'delete': # Delete if not threadStorage.questionIdExists(question['id']): irc.error('That question does not exist.') elif threadStorage.isQuestionDeleted(question['id']): irc.error('That question is already deleted.') elif threadStorage.isQuestionPendingDeletion(question['id']): irc.error('That question is already pending deletion.') else: reason = utils.str.normalizeWhitespace(text)[6:] reason = utils.str.normalizeWhitespace(reason) irc.reply('Marked question for deletion.') self.logger.doLog(irc, channel, "%s marked question #%i for deletion" % (msg.nick, question['id'])) threadStorage.insertDelete(username, channel, question['id'], reason) else: # Regular report threadStorage.updateUser(username, 0, 0, 1) threadStorage.insertReport(channel, username, text, question['id']) self.logger.doLog(irc, channel, "%s reported question #%i, Text: '%s'" % (msg.nick, question['id'], text)) irc.reply('Your report has been submitted!') else: irc.error('Sorry, round %d could not be found in the database' % (roundNum)) report = wrap(report, ['user', 'channel', 'int', 'text']) def restorequestion(self, irc, msg, arg, channel, questionNum): """[] Restore a deleted question. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return username = msg.nick dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if not threadStorage.questionIdExists(questionNum): irc.error('That question does not exist.') return if not threadStorage.isQuestionDeleted(questionNum): irc.error('That question was not deleted.') return threadStorage.restoreQuestion(questionNum) irc.reply('Question %d restored!' % questionNum) self.logger.doLog(irc, channel, "%s restored question #%i" % (username, questionNum)) restorequestion = wrap(restorequestion, ['channel', 'int']) def skip(self, irc, msg, arg, channel): """ Skip the current question and start the next. Rate-limited. Requires a certain percentage of active players to skip. """ username = self.getUsername(msg.nick, msg.prefix) usernameCanonical = ircutils.toLower(username) dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) timeSeconds = self.registryValue('skip.skipActiveTime', channel) totalActive = threadStorage.getNumUserActiveIn(channel, timeSeconds) channelCanonical = ircutils.toLower(channel) game = self.getGame(irc, channel) # Sanity checks if game is None or game.active == False: self.reply(irc, msg, 'Trivia is not currently running.') return elif game.questionOver == True: self.reply(irc, msg, 'No question is currently being asked.') return elif not threadStorage.wasUserActiveIn(username, channel, timeSeconds): self.reply(irc, msg, 'Only users who have answered a question in the last %s seconds can vote to skip.' % (timeSeconds)) return elif usernameCanonical in game.skipVoteCount: self.reply(irc, msg, 'You can only vote to skip once.') return elif totalActive < 1: return # Ensure the game's skip timeout is set? and then check the user skipSeconds = self.registryValue('skip.skipTime', channel) game.skips.setTimeout(skipSeconds) if game.skips.has(usernameCanonical): self.reply(irc, msg, 'You must wait %d seconds to be able to skip again.' % (game.skips.getTimeLeft(usernameCanonical))) return # Update skip count game.skipVoteCount[usernameCanonical] = 1 game.skips.append(usernameCanonical) self.reply(irc, msg, '%s voted to skip this question.' % username, prefixNick=False) percentAnswered = ((1.0*len(game.skipVoteCount))/(totalActive*1.0)) # Check if skip threshold has been reached if percentAnswered >= self.registryValue('skip.skipThreshold', channel): self.reply(irc, msg, 'Skipped question! (%d of %d voted)' % (len(game.skipVoteCount), totalActive), prefixNick=False) game.removeEvent() game.nextQuestion() skip = wrap(skip, ['onlyInChannel']) def stats(self, irc, msg, arg, channel, username): """ [] Show a player's rank, score & questions asked for day, month, and year. Channel is only required when using the command outside of a channel. """ dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalStats'): stat = threadStorage.getUserStat(username, None) rank = threadStorage.getUserRank(username, None) else: stat = threadStorage.getUserStat(username, channel) rank = threadStorage.getUserRank(username, channel) if not stat: irc.reply("I couldn't find that user in the database.") else: hasPoints = False infoList = ['%s\'s Stats: Points (answers)' % (self.addZeroWidthSpace(stat['username']))] if rank['day'] > 0 or stat['points_day'] > 0 or stat['answer_day'] > 0: hasPoints = True infoList.append(' \x02Today:\x02 #%d %d (%d)' % (rank['day'], stat['points_day'], stat['answer_day'])) if rank['week'] > 0 or stat['points_week'] > 0 or stat['answer_week'] > 0: hasPoints = True infoList.append(' \x02This Week:\x02 #%d %d (%d)' % (rank['week'], stat['points_week'], stat['answer_week'])) if rank['month'] > 0 or stat['points_month'] > 0 or stat['answer_month'] > 0: hasPoints = True infoList.append(' \x02This Month:\x02 #%d %d (%d)' % (rank['month'], stat['points_month'], stat['answer_month'])) if rank['year'] > 0 or stat['points_year'] > 0 or stat['answer_year'] > 0: hasPoints = True infoList.append(' \x02This Year:\x02 #%d %d (%d)' % (rank['year'], stat['points_year'], stat['answer_year'])) if not hasPoints: infoList = ['%s: %s does not have any points.' % (msg.nick, username)] infoText = ''.join(infoList) self.reply(irc, msg, infoText, prefixNick=False) stats = wrap(stats, ['channel', 'nick']) def showdelete(self, irc, msg, arg, channel, num): """[] [] Show a deleteion request pending approval. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return if num is not None: dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): delete = threadStorage.getDeleteById(num) else: delete = threadStorage.getDeleteById(num, channel) if delete: question = threadStorage.getQuestionById(delete['line_num']) questionText = question['question'] if question else 'Question not found' irc.reply('Delete #%d, by %s Question #%d: %s, Reason: %s'%(delete['id'], delete['username'], delete['line_num'], questionText, delete['reason'])) else: if self.registryValue('general.globalstats'): irc.error('Unable to find delete #{0}.'.format(num)) else: irc.error('Unable to find delete #{0} in {1}.'.format(num, channel)) else: self.listdeletes(irc, msg, [channel]) showdelete = wrap(showdelete, ['channel', optional('int')]) def showquestion(self, irc, msg, arg, user, channel, num): """[] Search question database for question at line number. Channel is only necessary when editing from outside of the channel. """ dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) question = threadStorage.getQuestionById(num) if question: if question['deleted'] == 1: irc.reply('Info: This question is currently deleted.') irc.reply('Question #%d: %s' % (num, question['question'])) else: irc.error('Question not found') showquestion = wrap(showquestion, ['user', 'channel', 'int']) def showround(self, irc, msg, arg, user, channel, num): """[] Show what question was asked during gameplay. Channel is only necessary when editing from outside of the channel. """ game = self.getGame(irc, channel) if game is not None and num == game.numAsked and not game.questionOver: irc.error('The current question can\'t be displayed until it is over.') else: dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) question = threadStorage.getQuestionByRound(num, channel) if question: irc.reply('Round %d: Question #%d: %s' % (num, question['id'], question['question'])) else: irc.error('Round not found') showround = wrap(showround, ['user', 'channel', 'int']) def showreport(self, irc, msg, arg, user, channel, num): """[] [] Shows report information, if number is provided one record is shown, otherwise the last 3 are. Channel is only necessary when editing from outside of the channel. """ if num is not None: dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): report = threadStorage.getReportById(num) else: report = threadStorage.getReportById(num, channel) if report: irc.reply('Report #%d \'%s\' by %s on %s Q#%d '%(report['id'], report['report_text'], report['username'], report['channel'], report['question_num'])) question = threadStorage.getQuestionById(report['question_num']) if question: irc.reply('Question #%d: %s' % (question['id'], question['question'])) else: irc.error('Question could not be found.') else: if self.registryValue('general.globalstats'): irc.reply('Unable to find report #{0}.'.format(num)) else: irc.reply('Unable to find report #{0} in {1}.'.format(num, channel)) else: self.listreports(irc, msg, [channel]) showreport = wrap(showreport, ['user', 'channel', optional('int')]) def showedit(self, irc, msg, arg, channel, num): """[] [] Show top 3 edits, or provide edit num to view one. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return if num is not None: dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): edit = threadStorage.getEditById(num) else: edit = threadStorage.getEditById(num, channel) if edit: question = threadStorage.getQuestionById(edit['question_id']) irc.reply('Edit #%d by %s, Question #%d'%(edit['id'], edit['username'], edit['question_id'])) irc.reply('NEW:%s' %(edit['question'])) if question: irc.reply('OLD:%s' % (question['question'])) else: irc.error('Question could not be found for this edit') else: if self.registryValue('general.globalstats'): irc.error('Unable to find edit #{0}.'.format(num)) else: irc.error('Unable to find edit #{0} in {1}.'.format(num, channel)) else: self.listedits(irc, msg, [channel]) showedit = wrap(showedit, ['channel', optional('int')]) def shownew(self, irc, msg, arg, channel, num): """[] [] Show questions awaiting approval Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaMod(hostmask, channel) == False: irc.reply('You must be at least a TriviaMod in {0} to use this command.'.format(channel)) return if num is not None: dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalstats'): q = threadStorage.getTemporaryQuestionById(num) else: q = threadStorage.getTemporaryQuestionById(num, channel) if q: irc.reply('Temp Q #%d by %s: %s'%(q['id'], q['username'], q['question'])) else: if self.registryValue('general.globalstats'): irc.error('Unable to find new question #{0}.'.format(num)) else: irc.error('Unable to find new question #{0} in {1}.'.format(num, channel)) else: self.listnew(irc, msg, [channel]) shownew = wrap(shownew, ['channel', optional('int')]) def start(self, irc, msg, args, channel): """ Begins a round of Trivia inside the current channel. """ game = self.getGame(irc, channel) # Sanity checks # 1. Is trivia running? # 2. Is the previous trivia session still in the shutdown phase? # 3. Is there a stop pending? if game is None: # create a new game self.reply(irc, msg, 'Another epic round of trivia is about to begin.', prefixNick=False) self.createGame(irc, channel) elif game.active == False: self.reply(irc, msg, 'Please wait for the previous game instance to stop.') elif game.stopPending == True: game.stopPending = False self.reply(irc, msg, 'Pending stop aborted', prefixNick=False) else: self.reply(irc, msg, 'Trivia has already been started.') start = wrap(start, ['onlyInChannel']) def stop(self, irc, msg, args, user, channel): """ Stops the current Trivia round. """ game = self.getGame(irc, channel) # Sanity checks # 1. Is trivia running? # 2. Is a question being asked? # 2.1 Is a stop pending? if game is None or game.active == False: self.reply(irc, msg, 'Game is already stopped.') elif game.questionOver == False: if game.stopPending == True: self.reply(irc, msg, 'Trivia is already pending stop.') else: game.stopPending = True self.reply(irc, msg, 'Trivia will now stop after this question.', prefixNick=False) else: game.stop() irc.noReply() stop = wrap(stop, ['user', 'onlyInChannel']) def time(self, irc, msg, arg): """ Figure out what time/day it is on the server. """ timeStr = time.asctime(time.localtime()) self.reply(irc, msg, 'The current server time appears to be {0}'.format(timeStr), prefixNick=False) time = wrap(time) def transferpoints(self, irc, msg, arg, channel, userfrom, userto): """[] Transfers all points and records from one user to another. Channel is only required when using the command outside of a channel. """ hostmask = msg.prefix if self.isTriviaAdmin(hostmask, channel) == False: irc.reply('You must be a TriviaAdmin in {0} to use this command.'.format(channel)) return userfrom = userfrom userto = userto dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) threadStorage.transferUserLogs(userfrom, userto, channel) irc.reply('Transferred all records from {0} to {1} in {2}.'.format(userfrom, userto, channel)) self.logger.doLog(irc, channel, '{0} transferred records from {1} to {2} in {3}.'.format(msg.nick, userfrom, userto, channel)) transferpoints = wrap(transferpoints, ['channel', 'nick', 'nick']) def week(self, irc, msg, arg, channel, num): """[] [] Displays the top scores of the week. Parameter is optional, display up to that number. (eg 20 - display 11-20) Channel is only required when using the command outside of a channel. """ num = max(num, 10) offset = num-9 dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalStats'): tops = threadStorage.viewWeekTop10(None, num) else: tops = threadStorage.viewWeekTop10(channel, num) topsList = ['This Week\'s Top {0}-{1} Players: '.format(offset, num)] if tops: for i in range(len(tops)): topsList.append('\x02 #%d:\x02 %s %d ' % ((i+offset) , self.addZeroWidthSpace(tops[i]['username']), tops[i]['points'])) else: topsList.append('No players') topsText = ''.join(topsList) self.reply(irc, msg, topsText, prefixNick=False) week = wrap(week, ['channel', optional('int')]) def year(self, irc, msg, arg, channel, num): """[] [] Displays the top scores of the year. Parameter is optional, display up to that number. (eg 20 - display 11-20) Channel is only required when using the command outside of a channel. """ num = max(num, 10) offset = num-9 dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) if self.registryValue('general.globalStats'): tops = threadStorage.viewYearTop10(None, num) else: tops = threadStorage.viewYearTop10(channel, num) topsList = ['This Year\'s Top {0}-{1} Players: '.format(offset, num)] if tops: for i in range(len(tops)): topsList.append('\x02 #%d:\x02 %s %d ' % ((i+offset) , self.addZeroWidthSpace(tops[i]['username']), tops[i]['points'])) else: topsList.append('No players') topsText = ''.join(topsList) self.reply(irc, msg, topsText, prefixNick=False) year = wrap(year, ['channel', optional('int')]) #Game instance class Game: """ Main game logic, single game instance for each channel. """ def __init__(self, irc, channel, base): # constants self.unmaskedChars = " -'\"_=+&%$#@!~`[]{}?.,<>|\\/:;" # get utilities from base plugin self.base = base self.games = base.games self.storage = base.storage self.Storage = base.Storage self.registryValue = base.registryValue self.channel = channel self.irc = irc self.network = irc.network # reset stats self.skips = TimeoutList(self.registryValue('skip.skipTime', channel)) self.stopPending = False self.shownHint = False self.questionRepeated = False self.skipVoteCount = {} self.streak = 0 self.lastWinner = '' self.hintsCounter = 0 self.numAsked = 0 self.lastAnswer = time.time() self.roundStartedAt = time.mktime(time.localtime()) self.loadGameState() # activate self.questionOver = True self.active = True # stop any old game and start a new one self.removeEvent() self.nextQuestion() def checkAnswer(self, msg): """ Check users input to see if answer was given. """ # Already done? get out of here if self.questionOver: return channel = msg.args[0] # is it a user? username = self.base.getUsername(msg.nick, msg.prefix) correctAnswerFound = False correctAnswer = '' attempt = str.lower(msg.args[1]) attempt = self.removeAccents(attempt) attempt = self.removeExtraSpaces(attempt) # was a correct answer guessed? for ans in self.alternativeAnswers: normalizedAns = self.removeAccents(ans) normalizedAns = self.removeExtraSpaces(normalizedAns) normalizedAns = str.lower(normalizedAns) if normalizedAns == attempt and normalizedAns not in self.guessedAnswers: correctAnswerFound = True correctAnswer = ans for ans in self.answers: normalizedAns = self.removeAccents(ans) normalizedAns = self.removeExtraSpaces(normalizedAns) normalizedAns = str.lower(normalizedAns) if normalizedAns == attempt and normalizedAns not in self.guessedAnswers: correctAnswerFound = True correctAnswer = ans if correctAnswerFound: dbLocation = self.registryValue('admin.sqlitedb') threadStorage = self.Storage(dbLocation) # time stats timeElapsed = float(time.time() - self.askedAt) pointsAdded = self.points # Past first hint? deduct points if self.hintsCounter > 1: pointsAdded /= 2 * (self.hintsCounter - 1) if len(self.answers) > 1: if ircutils.toLower(username) not in self.correctPlayers: self.correctPlayers[ircutils.toLower(username)] = 1 self.correctPlayers[ircutils.toLower(username)] += 1 # KAOS? divide points pointsAdded /= (len(self.answers) + 1) # Convert score to int pointsAdded = int(pointsAdded) self.totalAmountWon += pointsAdded # report the correct guess for kaos item threadStorage.updateUserLog(username, self.channel, pointsAdded,0, 0) self.lastAnswer = time.time() self.sendMessage('\x02%s\x02 gets \x02%d\x02 points for: \x02%s\x02' % (username, pointsAdded, correctAnswer)) else: # Normal question solved streakBonus = 0 # update streak info if ircutils.toLower(self.lastWinner) != ircutils.toLower(username): self.lastWinner = ircutils.toLower(username) self.streak = 1 else: self.streak += 1 streakBonus = pointsAdded * .01 * (self.streak-1) if streakBonus > pointsAdded * .5: streakBonus = pointsAdded * .5 threadStorage.updateGameStreak(self.channel, self.lastWinner, self.streak) threadStorage.updateUserHighestStreak(self.lastWinner, self.streak) threadStorage.updateGameLongestStreak(self.channel, username, self.streak) # Convert score to int pointsAdded = int(pointsAdded) # report correct guess, and show players streak threadStorage.updateUserLog(username, self.channel, (pointsAdded+streakBonus), 1, timeElapsed) self.lastAnswer = time.time() self.sendMessage('DING DING DING, \x02%s\x02 got the correct answer, \x02%s\x02, in \x02%0.4f\x02 seconds for \x02%d(+%d)\x02 points!' % (username, correctAnswer, timeElapsed, pointsAdded, streakBonus)) if self.registryValue('general.showStats', self.channel): if self.registryValue('general.globalStats'): stat = threadStorage.getUserStat(username, None) else: stat = threadStorage.getUserStat(username, self.channel) if stat: todaysScore = stat['points_day'] weekScore = stat['points_week'] monthScore = stat['points_month'] recapMessageList = ['\x02%s\x02 has won \x02%d\x02 in a row!' % (username, self.streak)] recapMessageList.append(' Total Points') recapMessageList.append(' TODAY: \x02%d\x02' % (todaysScore)) if weekScore > pointsAdded: recapMessageList.append(' this WEEK \x02%d\x02' % (weekScore)) if weekScore > pointsAdded or todaysScore > pointsAdded: if monthScore > pointsAdded: recapMessageList.append(' &') if monthScore > pointsAdded: recapMessageList.append(' this MONTH: \x02%d\x02' % (monthScore)) recapMessage = ''.join(recapMessageList) self.sendMessage(recapMessage) # add guessed word to list so we can cross it out if self.guessedAnswers.count(attempt) == 0: self.guessedAnswers.append(attempt) # can show more hints now self.shownHint = False # Have all of the answers been found? if len(self.guessedAnswers) == len(self.answers): # question is over self.questionOver = True if len(self.guessedAnswers) > 1: bonusPoints = 0 if len(self.correctPlayers) >= 2: if len(self.answers) >= 9: bonusPoints = self.registryValue('kaos.payoutKAOS', self.channel) bonusPointsText = '' if bonusPoints > 0: for nick in self.correctPlayers: threadStorage.updateUserLog(nick, self.channel, bonusPoints, 0, 0) bonusPointsText = 'Everyone gets a %d Point Bonus!!' % int(bonusPoints) # give a special message if it was KAOS self.sendMessage('All KAOS answered! %s' % bonusPointsText) self.sendMessage('Total Awarded: \x02%d Points to %d Players\x02' % (int(self.totalAmountWon), len(self.correctPlayers))) self.removeEvent() threadStorage.updateQuestionStats(self.lineNumber, 1, 0) if self.stopPending == True: self.stop() return waitTime = self.registryValue('general.waitTime',self.channel) if waitTime < 2: waitTime = 2 log.error('waitTime was set too low (<2 seconds). Setting to 2 seconds') waitTime = time.time() + waitTime self.queueEvent(waitTime, self.nextQuestion) #self.base.handleVoice(self.irc, username, channel) def getHintString(self, hintNum=None): if hintNum == None: hintNum = self.hintsCounter hintRatio = self.registryValue('hints.hintRatio') # % to show each hint hints = '' ratio = float(hintRatio * .01) charMask = self.registryValue('hints.charMask', self.channel) # create a string with hints for all of the answers for ans in self.answers: if ircutils.toLower(ans) in self.guessedAnswers: continue ans = unicode(ans.decode('utf-8')) if hints != '': hints += ' ' if len(self.answers) > 1: hints += '[' if hintNum == 0: masked = ans for i in range(len(masked)): if masked[i] in self.unmaskedChars: hints+= masked[i] else: hints += charMask elif hintNum == 1: divider = int(len(ans) * ratio) if divider > 3: divider = 3 if divider >= len(ans): divider = len(ans)-1 hints += ans[:divider] masked = ans[divider:] for i in range(len(masked)): if masked[i] in self.unmaskedChars: hints+= masked[i] else: hints += charMask elif hintNum == 2: divider = int(len(ans) * ratio) if divider > 3: divider = 3 if divider >= len(ans): divider = len(ans)-1 lettersInARow=divider-1 maskedInARow=0 hints += ans[:divider] ansend = ans[divider:] hintsend = '' unmasked = 0 if self.registryValue('hints.vowelsHint', self.channel): hints+= self.getMaskedVowels(ansend, divider-1) else: hints+= self.getMaskedRandom(ansend, divider-1) if len(self.answers) > 1: hints += ']' return hints.encode('utf-8') def getMaskedVowels(self, letters, sizeOfUnmasked): charMask = self.registryValue('hints.charMask', self.channel) hintsList = [''] unmasked = 0 lettersInARow = sizeOfUnmasked for i in range(len(letters)): masked = letters[i] if masked in self.unmaskedChars: hintsList.append(masked) elif str.lower(self.removeAccents(masked.encode('utf-8'))) in 'aeiou' and unmasked < (len(letters)-1) and lettersInARow < 3: hintsList.append(masked) lettersInARow += 1 unmasked += 1 else: hintsList.append(charMask) lettersInARow = 0 hints = ''.join(hintsList) return hints def getMaskedRandom(self, letters, sizeOfUnmasked): charMask = self.registryValue('hints.charMask', self.channel) hintRatio = self.registryValue('hints.hintRatio') # % to show each hint hints = '' unmasked = 0 maskedInARow=0 lettersInARow=sizeOfUnmasked for i in range(len(letters)): masked = letters[i] if masked in self.unmaskedChars: hints += masked unmasked += 1 elif maskedInARow > 2 and unmasked < (len(letters)-1): lettersInARow += 1 unmasked += 1 maskedInARow = 0 hints += letters[i] elif lettersInARow < 3 and unmasked < (len(letters)-1) and random.randint(0,100) < hintRatio: lettersInARow += 1 unmasked += 1 maskedInARow = 0 hints += letters[i] else: maskedInARow += 1 lettersInARow=0 hints += charMask return hints def getOtherHintString(self): charMask = self.registryValue('hints.charMask', self.channel) if len(self.answers) > 1 or len(self.answers) < 1: return ans = self.answers[0] hints = 'Hint: \x02\x0312' divider = 0 if len(ans) < 2: divider = 0 elif self.hintsCounter == 1: divider = 1 elif self.hintsCounter == 2: divider = int((len(ans) * .25) + 1) if divider > 4: divider = 4 elif self.hintsCounter == 3: divider = int((len(ans) * .5) + 1) if divider > 6: divider = 6 if divider == len(ans): divider -= 1 if divider > 0: hints += ans[:divider] return hints def getOtherHint(self): if self.questionOver: return if self.shownHint == False: self.shownHint = True if len(self.answers) == 1: self.sendMessage(self.getOtherHintString()) def getRemainingKAOS(self): if self.questionOver: return if len(self.answers) > 1: if self.shownHint == False: self.shownHint = True self.sendMessage('\x02\x0312%s' % (self.getHintString(self.hintsCounter-1))) def loadGameState(self): gameInfo = self.storage.getGame(self.channel) if gameInfo is not None: self.numAsked = gameInfo['num_asked'] self.roundStartedAt = gameInfo['round_started'] self.lastWinner = gameInfo['last_winner'] self.streak = int(gameInfo['streak']) def loopEvent(self): """ Main game/question/hint loop called by event. Decides whether question or hint is needed. """ # out of hints to give? if self.hintsCounter >= 3: answer = '' # create a string to show answers missed for ans in self.answers: # dont show guessed values at loss if ircutils.toLower(ans) in self.guessedAnswers: continue if answer != '': answer += ' ' if len(self.answers) > 1: answer += '[' answer += ans if len(self.answers) > 1: answer += ']' # Give failure message if len(self.answers) > 1: self.sendMessage("""Time's up! No one got \x02%s\x02""" % answer) self.sendMessage("""Correctly Answered: \x02%d of %d\x02 Total Awarded: \x02%d Points to %d Players\x02""" % (len(self.guessedAnswers), len(self.answers), int(self.totalAmountWon), len(self.correctPlayers)) ) else: self.sendMessage("""Time's up! The answer was \x02%s\x02.""" % answer) self.storage.updateQuestionStats(self.lineNumber, 0, 1) #reset stuff self.answers = [] self.alternativeAnswers = [] self.question = '' self.questionOver = True if self.stopPending == True: self.stop() return # provide next question waitTime = self.registryValue('general.waitTime',self.channel) if waitTime < 2: waitTime = 2 log.error('waitTime was set too low (<2 seconds). Setting to 2 seconds') waitTime = time.time() + waitTime self.queueEvent(waitTime, self.nextQuestion) else: # give out more hints self.nextHint() def nextHint(self): """ Max hints have not been reached, and no answer is found, need more hints """ hints = self.getHintString(self.hintsCounter) #increment hints counter self.hintsCounter += 1 self.sendMessage('Hint %s: \x02\x0312%s' % (self.hintsCounter, hints), 1, 9) #reset hint shown self.shownHint = False hintTime = 2 if len(self.answers) > 1: hintTime = self.registryValue('kaos.hintKAOS', self.channel) else: hintTime = self.registryValue('questions.hintTime', self.channel) if hintTime < 2: timout = 2 log.error('hintTime was set too low(<2 seconds). setting to 2 seconds') hintTime += time.time() self.queueEvent(hintTime, self.loopEvent) def nextQuestion(self): """ Time for a new question """ inactivityTime = self.registryValue('general.timeout') if self.lastAnswer < time.time() - inactivityTime: self.stop() self.sendMessage('Stopping due to inactivity') return elif self.stopPending == True: self.stop() return # reset and increment self.questionOver = False self.questionRepeated = False self.shownHint = False self.skipVoteCount = {} self.question = '' self.answers = [] self.alternativeAnswers = [] self.guessedAnswers = [] self.totalAmountWon = 0 self.lineNumber = -1 self.correctPlayers = {} self.hintsCounter = 0 self.numAsked += 1 # grab the next q numQuestion = self.storage.getNumQuestions() if numQuestion == 0: self.sendMessage('There are no questions. Stopping. If you are an admin, use the addfile command to add questions to the database.') self.stop() return numQuestionsLeftInRound = self.storage.getNumQuestionsNotAsked(self.channel, self.roundStartedAt) if numQuestionsLeftInRound == 0: self.numAsked = 1 self.roundStartedAt = time.mktime(time.localtime()) self.storage.updateGameRoundStarted(self.channel, self.roundStartedAt) self.sendMessage('All of the questions have been asked, shuffling and starting over') self.storage.updateGame(self.channel, self.numAsked) #increment q's asked retrievedQuestion = self.retrieveQuestion() self.points = self.registryValue('questions.defaultPoints', self.channel) for x in retrievedQuestion: if 'q' == x: self.question = retrievedQuestion['q'] if 'a' == x: self.answers = retrievedQuestion['a'] if 'aa' == x: self.alternativeAnswers = retrievedQuestion['aa'] if 'p' == x: self.points = retrievedQuestion['p'] if '#' == x: self.lineNumber = retrievedQuestion['#'] # store the question number so it can be reported self.storage.insertGameLog(self.channel, self.numAsked, self.lineNumber, self.question) self.sendQuestion() self.queueEvent(0, self.loopEvent) self.askedAt = time.time() def queueEvent(self, hintTime, func): """ Create a new timer event for loopEvent call """ # create a new thread for event next step to happen for [hintTime] seconds def event(): func() if self.active: schedule.addEvent(event, hintTime, '%s.trivia' % self.channel) def removeAccents(self, text): text = unicode(text.decode('utf-8')) normalized = unicodedata.normalize('NFKD', text) normalized = u''.join([c for c in normalized if not unicodedata.combining(c)]) return normalized.encode('utf-8') def removeExtraSpaces(self, text): return utils.str.normalizeWhitespace(text) def repeatQuestion(self): self.questionRepeated = True try: self.sendQuestion() except AttributeError: pass def removeEvent(self): """ Remove/cancel timer event """ # try and remove the current timer and thread, if we fail don't just carry on try: schedule.removeEvent('%s.trivia' % self.channel) except KeyError: pass def retrieveQuestion(self): # temporary function to get data lineNumber, question, timesAnswered, timesMissed = self.retrieveQuestionFromSql() answer = question.split('*', 1) if len(answer) > 1: question = answer[0].strip() answers = answer[1].split('*') answer = [] alternativeAnswers = [] if ircutils.toLower(question[:4]) == 'kaos': for ans in answers: if answer.count(ans) == 0: answer.append(str(ans).strip()) elif ircutils.toLower(question[:5]) == 'uword': for ans in answers: answer.append(str(ans).strip()) question = 'Unscramble the letters: ' shuffledLetters = list(unicode(ans.decode('utf-8'))) random.shuffle(shuffledLetters) for letter in shuffledLetters: question += letter question += ' ' question = question.encode('utf-8') break else: for ans in answers: if answer == []: answer.append(str(ans).strip()) else: alternativeAnswers.append(str(ans).strip()) points = self.registryValue('questions.defaultPoints', self.channel) if len(answer) > 1: points = self.registryValue('kaos.defaultKAOS', self.channel) * len(answers) additionalPoints = 0 additionalPoints += timesAnswered * -5 additionalPoints += timesMissed * 5 if additionalPoints > 200: additionalPoints = 200 if additionalPoints < -200: additionalPoints = -200 points += additionalPoints return {'p':points, 'q':question, 'a':answer, 'aa':alternativeAnswers, '#':lineNumber } else: log.info('Bad question found on line#%d' % lineNumber) # TODO report bad question # default question, everything went wrong with grabbing question return {'#':lineNumber, 'p':10050, 'q':'KAOS: The 10 Worst U.S. Presidents (Last Name Only)? (This is a panic question, if you see this report this question. it is malformed.)', 'a':['Bush', 'Nixon', 'Hoover', 'Grant', 'Johnson', 'Ford', 'Reagan', 'Coolidge', 'Pierce'], 'aa':['Obama'] } def retrieveQuestionFromSql(self): question = self.storage.getRandomQuestionNotAsked(self.channel, self.roundStartedAt) return (question['id'], question['question'], question['num_answered'], question['num_missed']) def sendMessage(self, msg, color=None, bgcolor=None): """ , [], [] helper for game instance to send messages to channel """ # no color self.irc.sendMsg(ircmsgs.privmsg(self.channel, ' %s ' % msg)) def sendQuestion(self): question = self.question.rstrip() if question[-1:] != '?': question += '?' # bold the q, add color questionText = '\x02\x0303%s' % (question) # KAOS? report # of answers if len(self.answers) > 1: questionText += ' %d possible answers' % (len(self.answers)) questionMessageString = '%s: %s' % (self.numAsked, questionText) maxLength = 400 questionMesagePieces = [questionMessageString[i:i+maxLength] for i in range(0, len(questionMessageString), maxLength)] multipleMessages=False for msgPiece in questionMesagePieces: if multipleMessages: msgPiece = '\x02\x0303%s' % (msgPiece) multipleMessages = True self.sendMessage(msgPiece, 1, 9) def stop(self): """ Stop a game in progress """ # responsible for stopping a timer/thread after being told to stop self.active = False self.stopPending = False self.removeEvent() self.sendMessage('Trivia stopped. :\'(') channelCanonical = ircutils.toLower(self.channel) if self.network in self.games: if channelCanonical in self.games[self.network]: del self.games[self.network][channelCanonical] #Storage for users and points using sqlite3 class Storage: """ Storage class """ def __init__(self,loc): self.loc = loc self.conn = sqlite3.connect(loc, check_same_thread=False) # dont check threads # otherwise errors self.conn.text_factory = str self.conn.row_factory = sqlite3.Row def chunk(self, qs, rows=10000): """ Divides the data into 10000 rows each """ for i in xrange(0, len(qs), rows): yield qs[i:i+rows] def countTemporaryQuestions(self, channel=None): c = self.conn.cursor() if channel is None: c.execute('''SELECT COUNT(*) FROM triviatemporaryquestion''') else: c.execute('''SELECT COUNT(*) FROM triviatemporaryquestion WHERE channel_canonical = ?''', (ircutils.toLower(channel),)) row = c.fetchone() c.close() return row[0] def countDeletes(self, channel=None): c = self.conn.cursor() if channel is None: c.execute('''SELECT COUNT(*) FROM triviadelete''') else: c.execute('''SELECT COUNT(*) FROM triviadelete WHERE channel_canonical = ?''', (ircutils.toLower(channel),)) row = c.fetchone() c.close() return row[0] def countEdits(self, channel=None): c = self.conn.cursor() if channel is None: c.execute('''SELECT COUNT(*) FROM triviaedit''') else: c.execute('''SELECT COUNT(*) FROM triviaedit WHERE channel_canonical = ?''', (ircutils.toLower(channel),)) row = c.fetchone() c.close() return row[0] def countReports(self, channel=None): c = self.conn.cursor() if channel is None: c.execute('''SELECT COUNT(*) FROM triviareport''') else: c.execute('''SELECT COUNT(*) FROM triviareport WHERE channel_canonical = ?''', (ircutils.toLower(channel),)) row = c.fetchone() c.close() return row[0] def deleteQuestion(self, questionId): c = self.conn.cursor() test = c.execute('''UPDATE triviaquestion set deleted=1 WHERE id=?''', (questionId,)) self.conn.commit() c.close() def dropActivityTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviaactivity''') except: pass c.close() def dropDeleteTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviadelete''') except: pass c.close() def dropUserTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviausers''') except: pass c.close() def dropLoginTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE trivialogin''') except: pass c.close() def dropUserLogTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviauserlog''') except: pass c.close() def dropGameTable(self): c = self.conn.cursor() try: c.execute('''DROP table triviagames''') except: pass c.close() def dropGameLogTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviagameslog''') c.execute('''DROP INDEX gamelograndomindex''') except: pass c.close() def dropReportTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviareport''') except: pass c.close() def dropQuestionTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviaquestion''') c.execute('''DROP INDEX questionrandomindex''') except: pass c.close() def dropTemporaryQuestionTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviatemporaryquestion''') except: pass c.close() def dropEditTable(self): c = self.conn.cursor() try: c.execute('''DROP TABLE triviaedit''') except: pass c.close() def getRandomQuestionNotAsked(self, channel, roundStart): c = self.conn.cursor() c.execute('''SELECT * FROM triviaquestion WHERE deleted=0 AND id NOT IN (SELECT tl.line_num FROM triviagameslog tl WHERE tl.channel_canonical=? AND tl.asked_at>=?) ORDER BY random() LIMIT 1''', (ircutils.toLower(channel),roundStart)) row = c.fetchone() c.close() return row def getQuestionById(self, id): c = self.conn.cursor() c.execute('''SELECT * FROM triviaquestion WHERE id=? LIMIT 1''', (id,)) row = c.fetchone() c.close() return row def getQuestionByRound(self, roundNumber, channel): channel=ircutils.toLower(channel) c = self.conn.cursor() c.execute('''SELECT * FROM triviaquestion WHERE id=(SELECT tgl.line_num FROM triviagameslog tgl WHERE tgl.round_num=? AND tgl.channel_canonical=? ORDER BY id DESC LIMIT 1)''', (roundNumber,channel)) row = c.fetchone() c.close() return row def getNumQuestionsNotAsked(self, channel, roundStart): c = self.conn.cursor() c.execute('''SELECT count(id) FROM triviaquestion WHERE deleted=0 AND id NOT IN (SELECT tl.line_num FROM triviagameslog tl WHERE tl.channel=? AND tl.asked_at>=?)''', (channel,roundStart)) row = c.fetchone() c.close() return row[0] def getUserRank(self, username, channel): usernameCanonical = ircutils.toLower(username) channelCanonical = None if channel is not None: channelCanonical = ircutils.toLower(channel) dateObject = datetime.date.today() day = dateObject.day month = dateObject.month year = dateObject.year data = {} # Retrieve total rank query = '''select tr.rank from ( select count(tu2.id)+1 as rank from ( select id, username, sum(points_made) as totalscore from triviauserlog''' arguments = [] if channel is not None: query = '''%s where channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s group by username_canonical ) as tu2 where tu2.totalscore > ( select sum(points_made) from triviauserlog where username_canonical=?''' % (query) arguments.append(usernameCanonical) if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s ) ) as tr where exists( select * from triviauserlog where username_canonical=?''' % (query) arguments.append(usernameCanonical) if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s ) limit 1''' % (query) c = self.conn.cursor() c.execute(query, tuple(arguments)) row = c.fetchone() data['total'] = row[0] if row else 0 # Retrieve year rank query = '''select tr.rank from ( select count(tu2.id)+1 as rank from ( select id, username, sum(points_made) as totalscore from triviauserlog where year=?''' arguments = [year] if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s group by username_canonical ) as tu2 where tu2.totalscore > ( select sum(points_made) from triviauserlog where year=? and username_canonical=?''' % (query) arguments.append(year) arguments.append(usernameCanonical) if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s ) ) as tr where exists( select * from triviauserlog where year=? and username_canonical=?''' % (query) arguments.append(year) arguments.append(usernameCanonical) if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s ) limit 1''' % (query) c.execute(query, tuple(arguments)) row = c.fetchone() data['year'] = row[0] if row else 0 # Retrieve month rank query = '''select tr.rank from ( select count(tu2.id)+1 as rank from ( select id, username, sum(points_made) as totalscore from triviauserlog where month=? and year=?''' arguments = [month, year] if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s group by username_canonical ) as tu2 where tu2.totalscore > ( select sum(points_made) from triviauserlog where month=? and year=? and username_canonical=?''' % (query) arguments.append(month) arguments.append(year) arguments.append(usernameCanonical) if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s ) ) as tr where exists( select * from triviauserlog where month=? and year=? and username_canonical=?''' % (query) arguments.append(month) arguments.append(year) arguments.append(usernameCanonical) if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s ) limit 1''' % (query) c.execute(query, tuple(arguments)) row = c.fetchone() data['month'] = row[0] if row else 0 # Retrieve week rank weekSqlClause = '' d = datetime.date.today() weekday=d.weekday() d -= datetime.timedelta(weekday) for i in range(7): if i > 0: weekSqlClause += ' or ' weekSqlClause += '''( year=%d and month=%d and day=%d)''' % (d.year, d.month, d.day) d += datetime.timedelta(1) weekSql = '''select tr.rank from ( select count(tu2.id)+1 as rank from ( select id, username, sum(points_made) as totalscore from triviauserlog where (''' arguments = [] weekSql += weekSqlClause weekSql +=''' )''' if channel is not None: weekSql = '''%s and channel_canonical=?''' % (weekSql) arguments.append(channelCanonical) weekSql += '''group by username_canonical ) as tu2 where tu2.totalscore > ( select sum(points_made) from triviauserlog where username_canonical=?''' arguments.append(usernameCanonical) if channel is not None: weekSql = '''%s and channel_canonical=?''' % (weekSql) arguments.append(channelCanonical) weekSql += ''' and (''' weekSql += weekSqlClause weekSql += ''' ) ) ) as tr where exists( select * from triviauserlog where username_canonical=?''' arguments.append(usernameCanonical) if channel is not None: weekSql = '''%s and channel_canonical=?''' % (weekSql) arguments.append(channelCanonical) weekSql += ''' and (''' weekSql += weekSqlClause weekSql += ''' ) ) limit 1''' c.execute(weekSql, tuple(arguments)) row = c.fetchone() data['week'] = row[0] if row else 0 # Retrieve day rank query = '''select tr.rank from ( select count(tu2.id)+1 as rank from ( select id, username, sum(points_made) as totalscore from triviauserlog where day=? and month=? and year=?''' arguments = [day, month, year] if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s group by username_canonical ) as tu2 where tu2.totalscore > ( select sum(points_made) from triviauserlog where day=? and month=? and year=? and username_canonical=?''' % (query) arguments.append(day) arguments.append(month) arguments.append(year) arguments.append(usernameCanonical) if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s ) ) as tr where exists( select * from triviauserlog where day=? and month=? and year=? and username_canonical=?''' % (query) arguments.append(day) arguments.append(month) arguments.append(year) arguments.append(usernameCanonical) if channel is not None: query = '''%s and channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s ) limit 1''' % (query) c.execute(query, tuple(arguments)) row = c.fetchone() data['day'] = row[0] if row else 0 c.close() return data def getUserStat(self, username, channel): usernameCanonical = ircutils.toLower(username) channelCanonical = None if channel is not None: channelCanonical = ircutils.toLower(channel) dateObject = datetime.date.today() day = dateObject.day month = dateObject.month year = dateObject.year c = self.conn.cursor() data = {} data['username'] = username data['username_canonical'] = usernameCanonical # Retrieve total points and answered query = '''select sum(tl.points_made) as points, sum(tl.num_answered) as answered from triviauserlog tl where tl.username_canonical=?''' arguments = [usernameCanonical] if channel is not None: query = '''%s and tl.channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s limit 1''' % (query) c.execute(query, tuple(arguments)) row = c.fetchone() if row: data['points_total'] = row[0] data['answer_total'] = row[1] # Retrieve year points and answered query = '''select sum(tl.points_made) as yearPoints, sum(tl.num_answered) as yearAnswered from triviauserlog tl where tl.username_canonical=? and tl.year=?''' arguments = [usernameCanonical, year] if channel is not None: query = '''%s and tl.channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s limit 1''' % (query) c.execute(query, tuple(arguments)) row = c.fetchone() if row: data['points_year'] = row[0] data['answer_year'] = row[1] # Retrieve month points and answered query = '''select sum(tl.points_made) as yearPoints, sum(tl.num_answered) as yearAnswered from triviauserlog tl where tl.username_canonical=? and tl.year=? and tl.month=?''' arguments = [usernameCanonical, year, month] if channel is not None: query = '''%s and tl.channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s limit 1''' % (query) c.execute(query, tuple(arguments)) row = c.fetchone() if row: data['points_month'] = row[0] data['answer_month'] = row[1] # Retrieve week points and answered query = '''select sum(tl.points_made) as yearPoints, sum(tl.num_answered) as yearAnswered from triviauserlog tl where tl.username_canonical=? and (''' d = datetime.date.today() weekday=d.weekday() d -= datetime.timedelta(weekday) for i in range(7): if i > 0: query += ' or ' query += ''' (tl.year=%d and tl.month=%d and tl.day=%d)''' % (d.year, d.month, d.day) d += datetime.timedelta(1) query += ')' arguments = [usernameCanonical] if channel is not None: query = '''%s and tl.channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s limit 1''' % (query) c.execute(query, tuple(arguments)) row = c.fetchone() if row: data['points_week'] = row[0] data['answer_week'] = row[1] # Retrieve day points and answered query = '''select sum(tl.points_made) as yearPoints, sum(tl.num_answered) as yearAnswered from triviauserlog tl where tl.username_canonical=? and tl.year=? and tl.month=? and tl.day=?''' arguments = [usernameCanonical, year, month, day] if channel is not None: query = '''%s and tl.channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s limit 1''' % (query) c.execute(query, tuple(arguments)) row = c.fetchone() if row: data['points_day'] = row[0] data['answer_day'] = row[1] c.close() return data def getGame(self, channel): channel = ircutils.toLower(channel) c = self.conn.cursor() c.execute('''SELECT * FROM triviagames WHERE channel_canonical=? LIMIT 1''', (channel,)) row = c.fetchone() c.close() return row def getNumUser(self, channel): channelCanonical = ircutils.toLower(channel) c = self.conn.cursor() c.execute('''SELECT COUNT(DISTINCT(username_canonical)) FROM triviauserlog WHERE channel_canonical=?''', (channelCanonical,)) row = c.fetchone() c.close() return row[0] def getNumQuestions(self): c = self.conn.cursor() c.execute('''SELECT COUNT(*) FROM triviaquestion WHERE deleted=0''') row = c.fetchone() c.close() return row[0] def getNumKAOS(self): c = self.conn.cursor() c.execute('''SELECT COUNT(*) FROM triviaquestion WHERE lower(substr(question,1,4))=?''',('kaos',)) row = c.fetchone() c.close() return row[0] def getNumActiveThisWeek(self, channel): channelCanonical = ircutils.toLower(channel) d = datetime.date.today() weekday=d.weekday() d -= datetime.timedelta(weekday) weekSqlString = '' for i in range(7): if i > 0: weekSqlString += ' or ' weekSqlString += ''' (tl.year=%d and tl.month=%d and tl.day=%d)''' % (d.year, d.month, d.day) d += datetime.timedelta(1) c = self.conn.cursor() weekSql = '''select count(distinct(tl.username_canonical)) from triviauserlog tl where channel_canonical=? and (''' weekSql += weekSqlString weekSql += ''')''' c.execute(weekSql, (channelCanonical,)) row = c.fetchone() c.close() return row[0] def getDeleteById(self, id, channel=None): c = self.conn.cursor() if channel is None: c.execute('''SELECT * FROM triviadelete WHERE id=? LIMIT 1''', (id,)) else: c.execute('''SELECT * FROM triviadelete WHERE id=? AND channel_canonical=? LIMIT 1''', (id, ircutils.toLower(channel))) row = c.fetchone() c.close() return row def getDeleteTop3(self, page=1, amount=3, channel=None): if page < 1: page=1 if amount < 1: amount=3 page -= 1 start = page * amount c = self.conn.cursor() if channel is None: c.execute('''SELECT * FROM triviadelete ORDER BY id DESC LIMIT ?, ?''', (start, amount)) else: c.execute('''SELECT * FROM triviadelete WHERE channel_canonical = ? ORDER BY id DESC LIMIT ?, ?''', (ircutils.toLower(channel), start, amount)) rows = c.fetchall() c.close() return rows def getReportById(self, id, channel=None): c = self.conn.cursor() if channel is None: c.execute('''SELECT * FROM triviareport WHERE id=? LIMIT 1''', (id,)) else: c.execute('''SELECT * FROM triviareport WHERE id=? AND channel_canonical=? LIMIT 1''', (id, ircutils.toLower(channel))) row = c.fetchone() c.close() return row def getReportTop3(self, page=1, amount=3, channel=None): if page < 1: page=1 if amount < 1: amount=3 page -= 1 start = page * amount c = self.conn.cursor() if channel is None: c.execute('''SELECT * FROM triviareport ORDER BY id DESC LIMIT ?, ?''', (start, amount)) else: c.execute('''SELECT * FROM triviareport WHERE channel_canonical = ? ORDER BY id DESC LIMIT ?, ?''', (ircutils.toLower(channel), start, amount)) rows = c.fetchall() c.close() return rows def getTemporaryQuestionTop3(self, page=1, amount=3, channel=None): if page < 1: page=1 if amount < 1: amount=3 page -= 1 start = page * amount c = self.conn.cursor() if channel is None: c.execute('''SELECT * FROM triviatemporaryquestion ORDER BY id DESC LIMIT ?, ?''', (start, amount)) else: c.execute('''SELECT * FROM triviatemporaryquestion WHERE channel_canonical = ? ORDER BY id DESC LIMIT ?, ?''', (ircutils.toLower(channel), start, amount)) rows = c.fetchall() c.close() return rows def getTemporaryQuestionById(self, id, channel=None): c = self.conn.cursor() if channel is None: c.execute('''SELECT * FROM triviatemporaryquestion WHERE id=? LIMIT 1''', (id,)) else: c.execute('''SELECT * FROM triviatemporaryquestion WHERE id=? AND channel_canonical=? LIMIT 1''', (id, ircutils.toLower(channel))) row = c.fetchone() c.close() return row def getEditTop3(self, page=1, amount=3, channel=None): if page < 1: page = 1 if amount < 1: amount = 3 page -= 1 start = page * amount c = self.conn.cursor() if channel is None: c.execute('''SELECT * FROM triviaedit ORDER BY id DESC LIMIT ?, ?''', (start, amount)) else: c.execute('''SELECT * FROM triviaedit WHERE channel_canonical = ? ORDER BY id DESC LIMIT ?, ?''', (ircutils.toLower(channel), start, amount)) rows = c.fetchall() c.close() return rows def getEditById(self, id, channel=None): c = self.conn.cursor() if channel is None: c.execute('''SELECT * FROM triviaedit WHERE id=? LIMIT 1''', (id,)) else: c.execute('''SELECT * FROM triviaedit WHERE id=? AND channel_canonical=? LIMIT 1''', (id, ircutils.toLower(channel))) row = c.fetchone() c.close() return row def getNumUserActiveIn(self, channel, timeSeconds): channelCanonical = ircutils.toLower(channel) epoch = int(time.mktime(time.localtime())) dateObject = datetime.date.today() day = dateObject.day month = dateObject.month year = dateObject.year c = self.conn.cursor() c.execute('''SELECT COUNT(*) FROM triviauserlog WHERE day=? AND month=? AND year=? AND channel_canonical=? AND last_updated>?''', (day, month, year, channelCanonical, (epoch-timeSeconds))) row = c.fetchone() c.close() return row[0] def gameExists(self, channel): channel = ircutils.toLower(channel) c = self.conn.cursor() c.execute('''SELECT COUNT(id) FROM triviagames WHERE channel_canonical=?''', (channel,)) row = c.fetchone() c.close() return row[0] > 0 def loginExists(self, username): usernameCanonical = ircutils.toLower(username) c = self.conn.cursor() c.execute('''SELECT COUNT(id) FROM trivialogin WHERE username_canonical=?''', (usernameCanonical,)) row = c.fetchone() c.close() return row[0] > 0 def insertActivity(self, aType, activity, channel, network, timestamp=None): if timestamp is None: timestamp = int(time.mktime(time.localtime())) channelCanonical = ircutils.toLower(channel) c = self.conn.cursor() c.execute('insert into triviaactivity values (NULL, ?, ?, ?, ?, ?, ?)', (aType, activity, channel, channelCanonical, network, timestamp)) self.conn.commit() def insertDelete(self, username, channel, lineNumber, reason): usernameCanonical = ircutils.toLower(username) channelCanonical = ircutils.toLower(channel) c = self.conn.cursor() c.execute('insert into triviadelete values (NULL, ?, ?, ?, ?, ?, ?)', (username, usernameCanonical, lineNumber, channel, channelCanonical, reason)) self.conn.commit() def insertLogin(self, username, salt, isHashed, password, capability): usernameCanonical = ircutils.toLower(username) if self.loginExists(username): return self.updateLogin(username, salt, isHashed, password, capability) if not isHashed: isHashed = 0 else: isHashed = 1 c = self.conn.cursor() c.execute('insert into trivialogin values (NULL, ?, ?, ?, ?, ?, ?)', (username, usernameCanonical, salt, isHashed, password, capability)) self.conn.commit() def insertUserLog(self, username, channel, score, numAnswered, timeTaken, day=None, month=None, year=None, epoch=None): if day == None and month == None and year == None: dateObject = datetime.date.today() day = dateObject.day month = dateObject.month year = dateObject.year score = int(score) if epoch is None: epoch = int(time.mktime(time.localtime())) if self.userLogExists(username, channel, day, month, year): return self.updateUserLog(username, channel, score, numAnswered, timeTaken, day, month, year, epoch) c = self.conn.cursor() usernameCanonical = ircutils.toLower(username) channelCanonical = ircutils.toLower(channel) scoreAvg = 'NULL' if numAnswered >= 1: scoreAvg = score / numAnswered c.execute('insert into triviauserlog values (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (username, score, numAnswered, day, month, year, epoch, timeTaken, scoreAvg, usernameCanonical, channel, channelCanonical)) self.conn.commit() c.close() def insertUser(self, username, numEditted=0, numEdittedAccepted=0, numReported=0, numQuestionsAdded=0, numQuestionsAccepted=0): usernameCanonical = ircutils.toLower(username) if self.userExists(username): return self.updateUser(username, numEditted, numEdittedAccepted, numReported, numQuestionsAdded, numQuestionsAccepted) c = self.conn.cursor() c.execute('insert into triviausers values (NULL, ?, ?, ?, ?, ?, ?, ?, 0)', ( username, numEditted, numEdittedAccepted, usernameCanonical, numReported, numQuestionsAdded, numQuestionsAccepted ) ) self.conn.commit() c.close() def insertGame(self, channel, numAsked=0, epoch=None): channelCanonical = ircutils.toLower(channel) if self.gameExists(channel): return self.updateGame(channel, numAsked) if epoch is None: epoch = int(time.mktime(time.localtime())) c = self.conn.cursor() c.execute('insert into triviagames values (NULL, ?, ?, ?, 0, 0, ?, 0, "", "")', (channel,numAsked,epoch,channelCanonical)) self.conn.commit() c.close() def insertGameLog(self, channel, roundNumber, lineNumber, questionText, askedAt=None): channelCanonical = ircutils.toLower(channel) if askedAt is None: askedAt = int(time.mktime(time.localtime())) c = self.conn.cursor() c.execute('insert into triviagameslog values (NULL, ?, ?, ?, ?, ?, ?)', (channel,roundNumber,lineNumber,questionText,askedAt,channelCanonical)) self.conn.commit() c.close() def insertReport(self, channel, username, reportText, questionNum, reportedAt=None): channelCanonical = ircutils.toLower(channel) usernameCanonical = ircutils.toLower(username) if reportedAt is None: reportedAt = int(time.mktime(time.localtime())) c = self.conn.cursor() c.execute('insert into triviareport values (NULL, ?, ?, ?, ?, NULL, NULL, ?, ?, ?)', (channel,username,reportText,reportedAt,questionNum,usernameCanonical,channelCanonical)) self.conn.commit() c.close() def insertQuestionsBulk(self, questions): c = self.conn.cursor() #skipped=0 divData = self.chunk(questions) # divide into 10000 rows each for chunk in divData: c.executemany('''insert into triviaquestion values (NULL, ?, ?, 0, 0, 0)''', chunk) self.conn.commit() skipped = self.removeDuplicateQuestions() c.close() return ((len(questions) - skipped), skipped) def insertEdit(self, questionId, questionText, username, channel, createdAt=None): c = self.conn.cursor() channelCanonical = ircutils.toLower(channel) usernameCanonical = ircutils.toLower(username) if createdAt is None: createdAt = int(time.mktime(time.localtime())) c.execute('''INSERT INTO triviaedit VALUES (NULL, ?, ?, NULL, ?, ?, ?, ?, ?)''', (questionId,questionText,username,channel,createdAt, usernameCanonical, channelCanonical)) self.conn.commit() c.close() def insertTemporaryQuestion(self, username, channel, question): c = self.conn.cursor() channelCanonical = ircutils.toLower(channel) usernameCanonical = ircutils.toLower(username) c.execute('''INSERT INTO triviatemporaryquestion VALUES (NULL, ?, ?, ?, ?, ?)''', (username,channel,question,usernameCanonical,channelCanonical)) self.conn.commit() c.close() def isQuestionDeleted(self, id): c = self.conn.cursor() c.execute('''SELECT COUNT(*) FROM triviaquestion WHERE deleted=1 AND id=?''', (id,)) row = c.fetchone() c.close() return row[0] > 0 def isQuestionPendingDeletion(self, id): c = self.conn.cursor() c.execute('''SELECT COUNT(*) FROM triviadelete WHERE line_num=?''', (id,)) row = c.fetchone() c.close() return row[0] > 0 def makeActivityTable(self): c = self.conn.cursor() try: c.execute('''create table triviaactivity ( id integer primary key autoincrement, type text, activity text, channel text, channel_canonical text, network text, timestamp integer )''') except: pass self.conn.commit() c.close() def makeDeleteTable(self): c = self.conn.cursor() try: c.execute('''create table triviadelete ( id integer primary key autoincrement, username text, username_canonical text, line_num integer, channel text, channel_canonical text, reason text )''') except: pass self.conn.commit() c.close() def makeLoginTable(self): c = self.conn.cursor() try: c.execute('''create table trivialogin ( id integer primary key autoincrement, username text, username_canonical text not null unique, salt text, is_hashed integer not null default 1, password text, capability text )''') except: pass self.conn.commit() c.close() def makeUserTable(self): c = self.conn.cursor() try: c.execute('''create table triviausers ( id integer primary key autoincrement, username text, num_editted integer, num_editted_accepted integer, username_canonical text not null unique, num_reported integer, num_questions_added integer, num_questions_accepted integer, highest_streak integer )''') except: pass self.conn.commit() c.close() def makeUserLogTable(self): c = self.conn.cursor() try: c.execute('''create table triviauserlog ( id integer primary key autoincrement, username text, points_made integer, num_answered integer, day integer, month integer, year integer, last_updated integer, average_time integer, average_score integer, username_canonical text, channel text, channel_canonical text, unique(username_canonical,channel_canonical, day, month, year) on conflict replace )''') except: pass self.conn.commit() c.close() def makeGameTable(self): c = self.conn.cursor() try: c.execute('''create table triviagames ( id integer primary key autoincrement, channel text, num_asked integer, round_started integer, last_winner text, streak integer, channel_canonical text not null unique, longest_streak integer, longest_streak_holder text, longest_streak_holder_canonical text )''') except: pass self.conn.commit() c.close() def makeGameLogTable(self): c = self.conn.cursor() try: c.execute('''create table triviagameslog ( id integer primary key autoincrement, channel text, round_num integer, line_num integer, question text, asked_at integer, channel_canonical text )''') c.execute('''create index gamelograndomindex on triviagameslog (channel, line_num, asked_at) )''') except: pass self.conn.commit() c.close() def makeReportTable(self): c = self.conn.cursor() try: c.execute('''create table triviareport ( id integer primary key autoincrement, channel text, username text, report_text text, reported_at integer, fixed_at integer, fixed_by text, question_num integer, username_canonical text, channel_canonical text )''') except: pass self.conn.commit() c.close() def makeTemporaryQuestionTable(self): c = self.conn.cursor() try: c.execute('''create table triviatemporaryquestion ( id integer primary key autoincrement, username text, channel text, question text, username_canonical text, channel_canonical text )''') except: pass self.conn.commit() c.close() def makeQuestionTable(self): c = self.conn.cursor() try: c.execute('''create table triviaquestion ( id integer primary key autoincrement, question_canonical text, question text, deleted integer not null default 0, num_answered integer, num_missed integer )''') c.execute('''create index questionrandomindex on triviagameslog (id, deleted) )''') except: pass self.conn.commit() c.close() def makeEditTable(self): c = self.conn.cursor() try: c.execute('''create table triviaedit ( id integer primary key autoincrement, question_id integer, question text, status text, username text, channel text, created_at text, username_canonical text, channel_canonical )''') except: pass self.conn.commit() c.close() def questionExists(self, question): c = self.conn.cursor() c.execute('''SELECT COUNT(id) FROM triviaquestion WHERE question=? OR question_canonical=?''', (question,question)) row = c.fetchone() c.close() return row[0] > 0 def questionIdExists(self, id): c = self.conn.cursor() c.execute('''SELECT COUNT(id) FROM triviaquestion WHERE id=?''', (id,)) row = c.fetchone() c.close() return row[0] > 0 def removeOldActivity(self,count=100): c = self.conn.cursor() c.execute('''delete from triviaactivity where id not in ( select id from triviaactivity order by id desc limit ? )''', (count,)) self.conn.commit() c.close() def removeDelete(self, deleteId): c = self.conn.cursor() c.execute('''delete from triviadelete where id=?''', (deleteId,)) self.conn.commit() c.close() def removeDuplicateQuestions(self): c = self.conn.cursor() c.execute('''delete from triviaquestion where id not in (select min(id) from triviaquestion GROUP BY question_canonical)''') num = c.rowcount self.conn.commit() c.close() return num def removeEdit(self, editId): c = self.conn.cursor() c.execute('''delete from triviaedit where id=?''', (editId,)) self.conn.commit() c.close() def removeLogin(self, username): usernameCanonical = ircutils.toLower(username) c = self.conn.cursor() c.execute('''delete from trivialogin where username_canonical=?''', (usernameCanonical,)) self.conn.commit() c.close() def removeReport(self, repId): c = self.conn.cursor() c.execute('''delete from triviareport where id=?''', (repId,)) self.conn.commit() c.close() def removeReportByQuestionNumber(self, id): c = self.conn.cursor() c.execute('''delete from triviareport where question_num=?''', (id,)) self.conn.commit() c.close() def removeEditByQuestionNumber(self, id): c = self.conn.cursor() c.execute('''delete from triviaedit where question_id=?''', (id,)) self.conn.commit() c.close() def removeDeleteByQuestionNumber(self, id): c = self.conn.cursor() c.execute('''delete from triviadelete where line_num=?''', (id,)) self.conn.commit() c.close() def removeTemporaryQuestion(self, id): c = self.conn.cursor() c.execute('''delete from triviatemporaryquestion where id=?''', (id,)) self.conn.commit() c.close() def removeUserLogs(self, username, channel): usernameCanonical = ircutils.toLower(username) channelCanonical = ircutils.toLower(channel) c = self.conn.cursor() c.execute('''delete from triviauserlog where username_canonical=? and channel_canonical=?''', (usernameCanonical, channelCanonical)) self.conn.commit() c.close() def restoreQuestion(self, id): c = self.conn.cursor() test = c.execute('''update triviaquestion set deleted=0 where id=?''', (id,)) self.conn.commit() c.close() def transferUserLogs(self, userFrom, userTo, channel): userFromCanonical = ircutils.toLower(userFrom) userToCanonical = ircutils.toLower(userTo) channelCanonical = ircutils.toLower(channel) c = self.conn.cursor() c.execute(''' update triviauserlog set num_answered=num_answered +ifnull( ( select t3.num_answered from triviauserlog t3 where t3.day=triviauserlog.day and t3.month=triviauserlog.month and t3.year=triviauserlog.year and t3.channel_canonical=? and t3.username_canonical=? ) ,0), points_made=points_made +ifnull( ( select t2.points_made from triviauserlog t2 where t2.day=triviauserlog.day and t2.month=triviauserlog.month and t2.year=triviauserlog.year and t2.channel_canonical=? and t2.username_canonical=? ) ,0) where id in ( select id from triviauserlog tl where channel_canonical=? and username_canonical=? and exists ( select id from triviauserlog tl2 where tl2.day=tl.day and tl2.month=tl.month and tl2.year=tl.year and tl2.channel_canonical=? and tl2.username_canonical=? ) ) ''', (channelCanonical, userFromCanonical, channelCanonical, userFromCanonical, channelCanonical, userToCanonical, channelCanonical, userFromCanonical)) c.execute(''' update triviauserlog set username=?, username_canonical=? where username_canonical=? and channel_canonical=? and not exists ( select 1 from triviauserlog tl where tl.day=triviauserlog.day and tl.month=triviauserlog.month and tl.year=triviauserlog.year and tl.channel_canonical=? and tl.username_canonical=? ) ''', (userTo, userToCanonical, userFromCanonical, channelCanonical, channelCanonical, userToCanonical)) self.conn.commit() self.removeUserLogs(userFrom, channel) def userLogExists(self, username, channel, day, month, year): c = self.conn.cursor() args = (ircutils.toLower(username),ircutils.toLower(channel),day,month,year) c.execute('''SELECT COUNT(id) FROM triviauserlog WHERE username_canonical=? AND channel_canonical=? AND day=? AND month=? and year=?''', args) row = c.fetchone() c.close() return row[0] > 0 def userExists(self, username): c = self.conn.cursor() usr = (ircutils.toLower(username),) c.execute('''SELECT COUNT(id) FROM triviausers WHERE username_canonical=?''', usr) row = c.fetchone() c.close() return row[0] > 0 def updateLogin(self, username, salt, isHashed, password, capability): if not self.loginExists(username): return self.insertLogin(username, salt, isHashed, password, capability) usernameCanonical = ircutils.toLower(username) if not isHashed: isHashed = 0 else: isHashed = 1 c = self.conn.cursor() c.execute('''update trivialogin set username=?, salt=?, is_hashed=?, password=?, capability=? where username_canonical=?''', (username, salt, isHashed, password, capability, usernameCanonical) ) self.conn.commit() c.close() def updateUserLog(self, username, channel, score, numAnswered, timeTaken, day=None, month=None, year=None, epoch=None): if not self.userExists(username): self.insertUser(username) if day == None and month == None and year == None: dateObject = datetime.date.today() day = dateObject.day month = dateObject.month year = dateObject.year if epoch is None: epoch = int(time.mktime(time.localtime())) if not self.userLogExists(username, channel, day, month, year): return self.insertUserLog(username, channel, score, numAnswered, timeTaken, day, month, year, epoch) c = self.conn.cursor() usernameCanonical = ircutils.toLower(username) channelCanonical = ircutils.toLower(channel) test = c.execute('''update triviauserlog set username=?, points_made = points_made+?, average_time=( average_time * (1.0*num_answered/(num_answered+?)) + ? * (1.0*?/(num_answered+?)) ), average_score=( average_score * (1.0*num_answered/(num_answered+?)) + ? * (1.0*?/(num_answered+?)) ), num_answered = num_answered+?, last_updated = ? where username_canonical=? and channel_canonical=? and day=? and month=? and year=?''', (username,score,numAnswered,timeTaken,numAnswered,numAnswered,numAnswered,score,numAnswered,numAnswered,numAnswered,epoch,usernameCanonical,channelCanonical,day,month,year)) self.conn.commit() c.close() def updateUser(self, username, numEditted=0, numEdittedAccepted=0, numReported=0, numQuestionsAdded=0, numQuestionsAccepted=0): if not self.userExists(username): return self.insertUser(username, numEditted, numEdittedAccepted, numReported, numQuestionsAdded, numQuestionsAccepted) usernameCanonical = ircutils.toLower(username) c = self.conn.cursor() c.execute('''update triviausers set username=?, num_editted=num_editted+?, num_editted_accepted=num_editted_accepted+?, num_reported=num_reported+?, num_questions_added=num_questions_added+?, num_questions_accepted=num_questions_accepted+? where username_canonical=?''', ( username, numEditted, numEdittedAccepted, numReported, numQuestionsAdded, numQuestionsAccepted, usernameCanonical ) ) self.conn.commit() c.close() def updateUserHighestStreak(self, username, streak): if not self.userExists(username): return self.insertUser(username) usernameCanonical = ircutils.toLower(username) c = self.conn.cursor() c.execute('''update triviausers set highest_streak=? where highest_streak < ? and username_canonical=?''', (streak, streak, usernameCanonical) ) self.conn.commit() c.close() def updateGame(self, channel, numAsked): if not self.gameExists(channel): return self.insertGame(channel, numAsked) c = self.conn.cursor() channelCanonical = ircutils.toLower(channel) test = c.execute('''update triviagames set channel=?, num_asked=? where channel_canonical=?''', (channel, numAsked, channelCanonical)) self.conn.commit() c.close() def updateGameLongestStreak(self, channel, lastWinner, streak): c = self.conn.cursor() channelCanonical = ircutils.toLower(channel) lastWinnerCanonical = ircutils.toLower(lastWinner) test = c.execute('''update triviagames set longest_streak=?, longest_streak_holder=?, longest_streak_holder_canonical=? where channel_canonical=? and longest_streak 0: weekSqlString += ' or ' weekSqlString += ''' (year=%d AND month=%d AND day=%d)''' % (d.year, d.month, d.day) d += datetime.timedelta(1) query = '''SELECT id, username, SUM(points_made) AS points, SUM(num_answered) AS num FROM triviauserlog WHERE (%s)''' % weekSqlString arguments = [] if channel is not None: channelCanonical = ircutils.toLower(channel) query = '''%s AND channel_canonical=?''' % (query) arguments.append(channelCanonical) query = '''%s GROUP BY username_canonical ORDER BY points DESC LIMIT ?, 10''' % (query) arguments.append(numUpTo) c = self.conn.cursor() c.execute(query, tuple(arguments)) rows = c.fetchall() c.close() return rows def wasUserActiveIn(self, username, channel, timeSeconds): usernameCanonical = ircutils.toLower(username) channelCanonical = ircutils.toLower(channel) epoch = int(time.mktime(time.localtime())) dateObject = datetime.date.today() day = dateObject.day month = dateObject.month year = dateObject.year c = self.conn.cursor() c.execute('''SELECT count(*) FROM triviauserlog WHERE day=? AND month=? AND year=? AND username_canonical=? AND channel_canonical=? AND last_updated>?''', (day, month, year, usernameCanonical, channelCanonical, (epoch-timeSeconds))) row = c.fetchone() c.close() return row[0] > 0 def getVersion(self): c = self.conn.cursor(); try: c.execute('''select version from triviainfo''') return c.fetchone() except: pass def makeInfoTable(self): c = self.conn.cursor() try: c.execute('''create table triviainfo ( version integer )''') except: pass #A log wrapper, ripoff of ChannelLogger class Logger: def __init__(self, base): self.logs = {} self.registryValue = base.registryValue def logNameTimestamp(self, channel): return time.strftime('%Y-%m-%d') def getLogName(self, channel): return '%s.%s.log' % (channel, self.logNameTimestamp(channel)) def getLogDir(self, irc, channel): logDir = conf.supybot.directories.log.dirize('TriviaTime') logDir = os.path.join(logDir, irc.network) logDir = os.path.join(logDir, channel) timeDir = time.strftime('%B') logDir = os.path.join(logDir, timeDir) if not os.path.exists(logDir): os.makedirs(logDir) return logDir def timestamp(self, log): format = conf.supybot.log.timestampFormat() if format: log.write(time.strftime(format)) log.write(' ') def checkLogNames(self): for (irc, logs) in self.logs.items(): for (channel, log) in logs.items(): name = self.getLogName(channel) if name != log.name: log.close() del logs[channel] def getLog(self, irc, channel): self.checkLogNames() try: logs = self.logs[irc] except KeyError: logs = ircutils.IrcDict() self.logs[irc] = logs if channel in logs: return logs[channel] else: try: name = self.getLogName(channel) logDir = self.getLogDir(irc, channel) log = file(os.path.join(logDir, name), 'a') logs[channel] = log return log except IOError: self.log.exception('Error opening log:') return self.FakeLog() def doLog(self, irc, channel, s, *args): if not self.registryValue('general.logGames'): return s = format(s, *args) channel = self.normalizeChannel(irc, channel) log = self.getLog(irc, channel) self.timestamp(log) s = ircutils.stripFormatting(s) log.write(s) log.write('\n') log.flush() def normalizeChannel(self, irc, channel): return ircutils.toLower(channel) class FakeLog(object): def flush(self): return def close(self): return def write(self, s): return Class = TriviaTime # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: