Rewrite to support basic quiz gameplay.
This commit is contained in:
parent
bbb90264b4
commit
52fd6a5b37
2 changed files with 345 additions and 352 deletions
496
lolbot.py
Executable file → Normal file
496
lolbot.py
Executable file → Normal file
|
|
@ -1,211 +1,207 @@
|
||||||
#! /usr/bin/env python
|
#! /usr/bin/env python
|
||||||
#
|
|
||||||
# LolBot
|
|
||||||
#
|
|
||||||
# New version based on Twisted IRC.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Useful bot for folks stuck behind censor walls at work
|
LOLBOT 2
|
||||||
Logs a channel and collects URLs for later.
|
|
||||||
|
- die: Let the bot cease to exist.
|
||||||
|
- ask: Ask a MoxQuizz question.
|
||||||
|
- list: list some URLs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import print_function # unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
try:
|
import sqlite3
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import string
|
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
import getopt
|
import irc.strings
|
||||||
import sqlite3
|
from irc.bot import SingleServerIRCBot
|
||||||
from twisted.words.protocols import irc
|
|
||||||
from twisted.internet import protocol
|
|
||||||
from twisted.internet import reactor
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from models import Log, Url, Model
|
||||||
from pymoxquizz import QuestionBank, Question
|
from pymoxquizz import QuestionBank, Question
|
||||||
from models import Url, Log, Model
|
from os import listdir, path
|
||||||
except ImportError:
|
|
||||||
print("Some modules missing: Lolbot relies on Twisted IRC, Mechanize and SQLAlchemy.\n")
|
|
||||||
sys.exit
|
|
||||||
|
|
||||||
# Exclamations - wrong input
|
|
||||||
exclamations = [
|
|
||||||
"Zing!",
|
|
||||||
"Burns!",
|
|
||||||
"Tard!",
|
|
||||||
"Lol.",
|
|
||||||
"Crazy!",
|
|
||||||
"WTF?",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Ponderings
|
|
||||||
ponderings = [
|
|
||||||
"Hi, can I have a medium lamb roast, with just potatoes.",
|
|
||||||
"Can I slurp on your Big Cock?",
|
|
||||||
"Quentin Tarantino is so awesome I want to have his babies.",
|
|
||||||
"No it's a week night 8pm is past my bedtime.",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class LolBot(irc.IRCClient):
|
DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
|
def debug(msg):
|
||||||
|
if DEBUG:
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class LolBot(SingleServerIRCBot):
|
||||||
"""
|
"""
|
||||||
The Lolbot itself.
|
An IRC bot to entertain the troops with MoxQuizz questions, log URLs, and
|
||||||
|
other shenanigans.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _get_connection(self):
|
qb = list()
|
||||||
connection = sqlite3.Connection(self.config['db.file'])
|
|
||||||
connection.text_factory = str
|
def __init__(self, channel, nickname, server, database, port=6667):
|
||||||
return connection
|
debug("Instantiating SingleServerIRCBot")
|
||||||
|
SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname)
|
||||||
|
self.channel = channel
|
||||||
|
|
||||||
def created(self, when):
|
|
||||||
# load some MoxQuizz questions
|
# load some MoxQuizz questions
|
||||||
self.qb = QuestionBank('/home/johnno/questions.doctorlard.en').questions
|
qfiles = [f for f in listdir('questions') if path.isfile(path.join('questions', f))]
|
||||||
|
debug("Loading MoxQuizz questions")
|
||||||
|
for f in qfiles:
|
||||||
|
qfile = path.abspath(path.join('questions', f))
|
||||||
|
debug(" - from MoxQuizz bank '%s'" % qfile)
|
||||||
|
self.qb += QuestionBank(qfile).questions
|
||||||
random.shuffle(self.qb)
|
random.shuffle(self.qb)
|
||||||
|
self.quiz = 0
|
||||||
self.question = None
|
self.question = None
|
||||||
|
|
||||||
# connect to the database
|
# connect to the database
|
||||||
|
debug("Connecting to SQLite database '%s'" % database)
|
||||||
|
self.dbfile = database
|
||||||
self.dbengine = create_engine('sqlite+pysqlite://', creator=self._get_connection)
|
self.dbengine = create_engine('sqlite+pysqlite://', creator=self._get_connection)
|
||||||
Model.metadata.bind = self.dbengine
|
Model.metadata.bind = self.dbengine
|
||||||
Model.metadata.create_all()
|
Model.metadata.create_all()
|
||||||
self.get_session = sessionmaker(bind=self.dbengine)
|
self.db_session = sessionmaker(bind=self.dbengine)
|
||||||
|
|
||||||
self.helptext = "Keeps a list of URLs. Commands: list [n|x-y] - prints the last 10 URLs (or n URLs, or x through y); clear - clears the list; lol - say something funny; <url> - adds the URL to the list; help - this message."
|
self.helptext = "Keeps a list of URLs. Commands: list [n|x-y] - prints the last 10 URLs (or n URLs, or x through y); <url> - adds the URL to the list; help - this message."
|
||||||
|
debug("Exiting lolbot constructor")
|
||||||
|
|
||||||
def now(self):
|
def _get_connection(self):
|
||||||
return datetime.today().strftime("%Y-%m-%d %H:%M:%S")
|
"""Creator function for SQLAlchemy."""
|
||||||
|
connection = sqlite3.Connection(self.dbfile)
|
||||||
|
connection.text_factory = str
|
||||||
|
debug("Creating SQLAlchemy connection")
|
||||||
|
return connection
|
||||||
|
|
||||||
def save_url(self, nickname, url):
|
def on_nicknameinuse(self, connection, event):
|
||||||
title = False
|
nick = connection.get_nickname()
|
||||||
|
debug("Nick '%s' in use, trying '%s_'" % nick)
|
||||||
|
connection.nick(nick + "_")
|
||||||
|
|
||||||
|
def on_welcome(self, connection, event):
|
||||||
|
debug("Joining channel '%s'" % self.channel)
|
||||||
|
connection.join(self.channel)
|
||||||
|
|
||||||
|
def on_privmsg(self, connection, event):
|
||||||
|
self.do_command(event, event.arguments[0])
|
||||||
|
|
||||||
|
def on_pubmsg(self, connection, event):
|
||||||
|
"""
|
||||||
|
Handle an event on the channel.
|
||||||
|
Handle commands addressed to the bot.
|
||||||
|
If there's a question, see if it's been answered.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
db = self.get_session()
|
(nick, message) = event.arguments[0].split(":", 1)
|
||||||
if not db.query(Url).filter(Url.url == url).count():
|
# handle command, if addressed
|
||||||
theurl = Url(nickname, url)
|
if irc.strings.lower(nick) == irc.strings.lower(self.connection.get_nickname()):
|
||||||
db.add(theurl)
|
self.do_command(event, message.strip())
|
||||||
db.commit()
|
except ValueError:
|
||||||
else:
|
message = event.arguments[0]
|
||||||
theurl = db.query(Url).filter(Url.url == url).one()
|
nick = event.source.nick
|
||||||
print(theurl)
|
|
||||||
title = theurl.title
|
|
||||||
except Exception as ex:
|
|
||||||
print("Exception caught saving URL: %s" % ex)
|
|
||||||
return title
|
|
||||||
|
|
||||||
def log_event(self, nick, text):
|
|
||||||
try:
|
|
||||||
entry = Log(nick, text)
|
|
||||||
db = self.get_session()
|
|
||||||
db.add(entry)
|
|
||||||
db.commit()
|
|
||||||
print(entry)
|
|
||||||
except Exception as ex:
|
|
||||||
print("Exception caught logging event: %s" % ex)
|
|
||||||
|
|
||||||
def _get_nickname(self):
|
|
||||||
return self.factory.nickname
|
|
||||||
nickname = property(_get_nickname)
|
|
||||||
|
|
||||||
def _get_channel(self):
|
|
||||||
return self.factory.channel
|
|
||||||
channel = property(_get_channel)
|
|
||||||
|
|
||||||
def _get_config(self):
|
|
||||||
return self.factory.config
|
|
||||||
config = property(_get_config)
|
|
||||||
|
|
||||||
def signedOn(self):
|
|
||||||
self.join(self.channel)
|
|
||||||
print("Signed on as %s." % (self.nickname,))
|
|
||||||
|
|
||||||
def joined(self, channel):
|
|
||||||
print("Joined %s." % (channel,))
|
|
||||||
|
|
||||||
def privmsg(self, user, channel, msg):
|
|
||||||
user = user.split('!')[0]
|
|
||||||
|
|
||||||
if channel == self.nickname:
|
|
||||||
# Private /msg from a user
|
|
||||||
self.do_command(msg, user)
|
|
||||||
else:
|
|
||||||
# log it
|
|
||||||
self.log_event(user, msg)
|
|
||||||
|
|
||||||
# deal with MoxQuizz question
|
# deal with MoxQuizz question
|
||||||
answered = False
|
if self.quiz:
|
||||||
if isinstance(self.question, Question) and self.question.answer.lower() in msg.lower():
|
self.handle_quiz(nick, message)
|
||||||
answered = True
|
|
||||||
print("%s +1" % user)
|
def start_quiz(self, nick):
|
||||||
self.reply('Correct! %s scores one point.' % user)
|
self.quiz = 0
|
||||||
if answered:
|
self.quiz_scores = dict()
|
||||||
|
self.connection.notice(self.channel, 'Quiz begun by %s.' % nick)
|
||||||
|
self.quiz_get_next()
|
||||||
|
|
||||||
|
def stop_quiz(self):
|
||||||
|
self.quiz = 0
|
||||||
|
self.quiz_scores = None
|
||||||
self.question = None
|
self.question = None
|
||||||
|
|
||||||
args = string.split(msg, ":", 1)
|
def quiz_get_next(self):
|
||||||
if len(args) > 1 and args[0] == self.nickname:
|
self.quiz += 1
|
||||||
self.do_command(string.strip(args[1]))
|
self.question = random.choice(self.qb)
|
||||||
else:
|
print(str(self.question.question))
|
||||||
# parse it for links, add URLs to the list
|
self.connection.notice(self.channel, str(self.question.question))
|
||||||
words = msg.split(" ")
|
|
||||||
for w in words:
|
|
||||||
if w.startswith('http://') or w.startswith('https://'):
|
|
||||||
title = self.save_url(user, w)
|
|
||||||
if title is False:
|
|
||||||
self.say_public("Sorry, I'm useless at UTF-8.")
|
|
||||||
else:
|
|
||||||
self.say_public("URL added. %s" % title)
|
|
||||||
|
|
||||||
def say_public(self, text):
|
def quiz_award_points(self, nick):
|
||||||
"Print TEXT into public channel, for all to see."
|
score = "%s point" % self.question.score
|
||||||
self.notice(self.channel, text)
|
if self.question.score != 1:
|
||||||
self.log_event(self.nickname, text)
|
score += "s"
|
||||||
|
|
||||||
def say_private(self, nick, text):
|
self.connection.notice(self.channel, 'Correct! The answer was %s. %s scores %s.' % (self.question.answer, nick, score))
|
||||||
"Send private message of TEXT to NICK."
|
if nick not in self.quiz_scores.keys():
|
||||||
self.notice(nick, text)
|
self.quiz_scores[nick] = 0
|
||||||
|
self.quiz_scores[nick] += self.question.score
|
||||||
|
|
||||||
def reply(self, text, to_private=None):
|
def quiz_check_win(self, nick):
|
||||||
"Send TEXT to either public channel or TO_PRIVATE nick (if defined)."
|
if self.quiz_scores[nick] == 10:
|
||||||
if to_private is not None:
|
self.connection.notice(self.channel, '%s wins with 10 points!' % nick)
|
||||||
self.say_private(to_private, text)
|
self.quiz_scoreboard()
|
||||||
else:
|
self.stop_quiz()
|
||||||
self.say_public(text)
|
|
||||||
|
|
||||||
def ponder(self):
|
def quiz_scoreboard(self):
|
||||||
"Return a random pondering."
|
self.connection.notice(self.channel, 'Scoreboard:')
|
||||||
return random.choice(ponderings)
|
for nick in self.quiz_scores.keys():
|
||||||
|
score = "%s point" % self.quiz_scores[nick]
|
||||||
|
if self.quiz_scores[nick] != 1:
|
||||||
|
score += "s"
|
||||||
|
self.connection.notice(self.channel, '%s has %s.' % (nick, score))
|
||||||
|
|
||||||
def exclaim(self):
|
def handle_quiz(self, nick, message):
|
||||||
"Return a random exclamation string."
|
# bail if there's no quiz or unanswered question.
|
||||||
return random.choice(exclamations)
|
if not self.quiz or not isinstance(self.question, Question):
|
||||||
|
return
|
||||||
|
|
||||||
def do_command(self, cmd, target=None):
|
# see if anyone answered correctly.
|
||||||
|
if self.question.attempt(message):
|
||||||
|
self.quiz_award_points(nick)
|
||||||
|
self.quiz_check_win(nick)
|
||||||
|
# if nobody has won, carry on
|
||||||
|
if self.quiz:
|
||||||
|
self.quiz_get_next()
|
||||||
|
|
||||||
|
def do_command(self, e, cmd):
|
||||||
"""
|
"""
|
||||||
This is the function called whenever someone sends a public or
|
Handle bot commands.
|
||||||
private message addressed to the bot. (e.g. "bot: blah").
|
|
||||||
"""
|
"""
|
||||||
|
nick = e.source.nick
|
||||||
|
c = self.connection
|
||||||
|
|
||||||
try:
|
if cmd == "die":
|
||||||
if cmd == 'help':
|
self.die()
|
||||||
self.reply(self.helptext, target)
|
|
||||||
|
elif cmd == 'help':
|
||||||
|
c.notice(nick, self.helptext)
|
||||||
|
|
||||||
|
elif cmd == 'status':
|
||||||
|
c.notice(nick, "I know %s questions." % len(self.qb))
|
||||||
|
|
||||||
|
elif cmd == 'halt' or cmd == 'quit':
|
||||||
|
if self.quiz:
|
||||||
|
self.quiz_scoreboard()
|
||||||
|
self.stop_quiz()
|
||||||
|
c.notice(self.channel, "Quiz halted by %s. Use ask to start a new one." % nick)
|
||||||
|
else:
|
||||||
|
c.notice(self.channel, "No quiz running.")
|
||||||
|
|
||||||
elif cmd == 'ask':
|
elif cmd == 'ask':
|
||||||
self.question = random.choice(self.qb)
|
if self.quiz:
|
||||||
self.reply(str(self.question.question))
|
c.notice(self.channel, "Quiz is running. Use halt or quit to stop.")
|
||||||
|
c.notice(self.channel, str(self.question.question))
|
||||||
|
elif isinstance(self.question, Question):
|
||||||
|
c.notice(self.channel, "There is an unanswered question.")
|
||||||
|
c.notice(self.channel, str(self.question.question))
|
||||||
|
else:
|
||||||
|
self.start_quiz(nick)
|
||||||
|
|
||||||
elif cmd == 'lol':
|
elif cmd == 'revolt':
|
||||||
self.reply(self.ponder(), target)
|
if isinstance(self.question, Question):
|
||||||
|
c.notice(self.channel, "Fine, the answer is: %s" % self.question.answer)
|
||||||
elif cmd == 'urls' or cmd == 'list':
|
self.quiz_get_next()
|
||||||
db = self.get_session()
|
|
||||||
for url in db.query(Url).order_by(Url.timestamp.desc())[:10]:
|
|
||||||
line = "%s %s" % (url.url, url.title)
|
|
||||||
self.reply(line, target)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
elif cmd.startswith('urls') or cmd.startswith('list'):
|
elif cmd.startswith('urls') or cmd.startswith('list'):
|
||||||
db = self.get_session()
|
db = self.db_session()
|
||||||
|
try:
|
||||||
(listcmd, n) = cmd.split(" ", 1)
|
(listcmd, n) = cmd.split(" ", 1)
|
||||||
|
except ValueError:
|
||||||
|
n = '5'
|
||||||
|
|
||||||
n = n.strip()
|
n = n.strip()
|
||||||
if n == "all":
|
if n == "all":
|
||||||
rows = db.query(Url).order_by(Url.timestamp.desc())
|
rows = db.query(Url).order_by(Url.timestamp.desc())
|
||||||
|
|
@ -217,174 +213,52 @@ class LolBot(irc.IRCClient):
|
||||||
if y < x:
|
if y < x:
|
||||||
x, y = y, x
|
x, y = y, x
|
||||||
except ValueError as ex:
|
except ValueError as ex:
|
||||||
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
c.notice(nick, "Give me a number or a range of numbers, e.g. list 5 or list 11-20")
|
||||||
raise ex
|
raise ex
|
||||||
rows = db.query(Url).order_by(Url.timestamp.desc())[x - 1: y]
|
rows = db.query(Url).order_by(Url.timestamp.desc())[x - 1: y]
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
n = abs(int(n))
|
n = abs(int(n))
|
||||||
except ValueError as ex:
|
except ValueError as ex:
|
||||||
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
c.notice(nick, "Give me a number or a range of numbers, e.g. list 5 or list 11-20")
|
||||||
raise ex
|
raise ex
|
||||||
rows = db.query(Url).order_by(Url.timestamp.desc())[:n]
|
rows = db.query(Url).order_by(Url.timestamp.desc())[:n]
|
||||||
|
|
||||||
for url in rows:
|
for url in rows:
|
||||||
line = "%s %s" % (url.url, url.title)
|
line = "%s %s" % (url.url, url.title)
|
||||||
self.reply(line, target)
|
c.notice(nick, line)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
elif cmd.startswith('http:') or cmd.startswith('https:'):
|
|
||||||
title = self.save_url(target, cmd)
|
|
||||||
if title is False:
|
|
||||||
self.say_public("Sorry, I'm useless at UTF-8.")
|
|
||||||
else:
|
else:
|
||||||
self.reply('URL added. %s' % title, target)
|
c.notice(nick, "Not understood: " + cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) != 5:
|
||||||
|
print("Usage: lolbot2.py <server[:port]> <channel> <nickname> <db>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
s = sys.argv[1].split(":", 1)
|
||||||
|
server = s[0]
|
||||||
|
if len(s) == 2:
|
||||||
|
try:
|
||||||
|
port = int(s[1])
|
||||||
|
except ValueError:
|
||||||
|
print("Error: Erroneous port.")
|
||||||
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
self.reply(self.exclaim(), target)
|
port = 6667
|
||||||
|
channel = sys.argv[2]
|
||||||
|
nickname = sys.argv[3]
|
||||||
|
database = sys.argv[4]
|
||||||
|
|
||||||
except Exception as ex:
|
debug("Parameters: server=%s port=%s nickname=%s channel=%s database=%s" % (server, port, nickname, channel, database))
|
||||||
print("Exception caught processing command: %s" % ex)
|
|
||||||
print(" command was '%s' from %s" % (cmd, target))
|
|
||||||
self.reply("Sorry, I didn't understand: %s" % cmd, target)
|
|
||||||
self.reply(self.helptext, target)
|
|
||||||
|
|
||||||
|
irc.client.ServerConnection.buffer_class = irc.buffer.LenientDecodingLineBuffer
|
||||||
class LolBotFactory(protocol.ClientFactory):
|
bot = LolBot(channel, nickname, server, database, port)
|
||||||
protocol = LolBot
|
bot.start()
|
||||||
|
|
||||||
def __init__(self, config_path):
|
|
||||||
self.config = get_config(config_path)
|
|
||||||
self.server = self.config['irc.server']
|
|
||||||
self.port = self.config['irc.port']
|
|
||||||
self.nickname = self.config['irc.nickname']
|
|
||||||
self.channel = self.config['irc.channel']
|
|
||||||
|
|
||||||
def clientConnectionLost(self, connector, reason):
|
|
||||||
print("Lost connection (%s), reconnecting." % (reason,))
|
|
||||||
connector.connect()
|
|
||||||
|
|
||||||
def clientConnectionFailed(self, connector, reason):
|
|
||||||
print("Could not connect: %s" % (reason,))
|
|
||||||
|
|
||||||
|
|
||||||
def get_options():
|
|
||||||
try:
|
|
||||||
(options, args) = getopt.getopt(sys.argv[1:], "hc:", ["help", "config=", ])
|
|
||||||
except getopt.GetoptError as err:
|
|
||||||
print(str(err))
|
|
||||||
usage()
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
config_path = ""
|
|
||||||
for option, value in options:
|
|
||||||
if option in ("-h", "--help"):
|
|
||||||
usage()
|
|
||||||
sys.exit(2)
|
|
||||||
if option in ("-c", "--config"):
|
|
||||||
config_path = value
|
|
||||||
return {'config_path': config_path}
|
|
||||||
|
|
||||||
|
|
||||||
def get_config(config_path):
|
|
||||||
"""
|
|
||||||
This method loads configuration options from a lolbot.conf file. The file
|
|
||||||
should look like this:
|
|
||||||
|
|
||||||
irc.server = irc.freenode.net
|
|
||||||
irc.port = 6667
|
|
||||||
irc.channel = #lolbottest
|
|
||||||
irc.nickname = lolbot
|
|
||||||
db.url = sqlite:///lolbot.db
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not config_path:
|
|
||||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lolbot.conf")
|
|
||||||
if not os.path.exists(config_path):
|
|
||||||
print("Error: configuration file not found. By default lolbot will look for a lolbot.conf file in the same directory as the lolbot script, but you can override this by specifying a path on the command line with the --config option.")
|
|
||||||
usage()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# open the configuration file and grab all param=value declarations.
|
|
||||||
config = {}
|
|
||||||
with open(config_path) as f:
|
|
||||||
for line in f:
|
|
||||||
# skip comments
|
|
||||||
if line.strip().startswith("#") or line.strip() == "":
|
|
||||||
continue
|
|
||||||
|
|
||||||
# collect up param = value
|
|
||||||
try:
|
|
||||||
(param, value) = line.strip().split("=", 1)
|
|
||||||
if param.strip() != "":
|
|
||||||
config[param.strip()] = value.strip()
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# validate IRC host
|
|
||||||
if "irc.server" not in config.keys():
|
|
||||||
print("Error: the IRC server was not specified. Use --help for more information.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# validate IRC port
|
|
||||||
if "irc.port" not in config.keys():
|
|
||||||
config["irc.port"] = "6667"
|
|
||||||
try:
|
|
||||||
config["irc.port"] = int(config["irc.port"])
|
|
||||||
except ValueError:
|
|
||||||
print("Error: the IRC port must be an integer. If not specified, lolbot will use the default IRC port value 6667. Use --help for more information.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# validate IRC channel
|
|
||||||
if "irc.channel" not in config.keys() or not config["irc.channel"].startswith("#"):
|
|
||||||
print("Error: the IRC channel is not specified or incorrect. It must begin with a # - e.g. #mychatchannel. Use --help for more information.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# validate bot nickname
|
|
||||||
if "irc.nickname" not in config.keys():
|
|
||||||
config["irc.nickname"] = "lolbot"
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def usage():
|
|
||||||
print("""Run a lolbot.
|
|
||||||
|
|
||||||
-h, --help
|
|
||||||
This message.
|
|
||||||
|
|
||||||
-c, --config=<filename>
|
|
||||||
Specify a configuration file. Defaults to lolbot.conf in the same
|
|
||||||
directory as the script.
|
|
||||||
|
|
||||||
Configuration:
|
|
||||||
|
|
||||||
irc.server = <host>
|
|
||||||
The IRC server, e.g. irc.freenode.net
|
|
||||||
|
|
||||||
irc.port = <port>
|
|
||||||
The IRC server port. Default: 6667
|
|
||||||
|
|
||||||
irc.channel = <channel>
|
|
||||||
The chat channel to join, e.g. #trainspotting
|
|
||||||
|
|
||||||
irc.nickname = <nickname>
|
|
||||||
The nickname for your lolbot. Default: "lolbot"
|
|
||||||
|
|
||||||
db.url = <url>
|
|
||||||
The URL to your lolbot database. This can be any valid URL accepted
|
|
||||||
by SQL Alchemy. If not specified, lolbot will attempt to use a
|
|
||||||
SQLite database called lolbot.db in the same directory as the
|
|
||||||
script.
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
args = get_options()
|
main()
|
||||||
config = get_config(args['config_path'])
|
|
||||||
try:
|
|
||||||
reactor.connectTCP(config['irc.server'], config['irc.port'], LolBotFactory(args['config_path']))
|
|
||||||
reactor.run()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("Shutting down.")
|
|
||||||
|
|
|
||||||
137
pymoxquizz.py
137
pymoxquizz.py
|
|
@ -1,7 +1,14 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
A MozQuizz question library for Python.
|
||||||
|
See http://moxquizz.de/ for the original implementation in TCL.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, print_function
|
from __future__ import unicode_literals, print_function
|
||||||
from io import open
|
from io import open
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class Question:
|
class Question:
|
||||||
|
|
@ -10,30 +17,103 @@ class Question:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
category = None
|
category = None
|
||||||
|
"""
|
||||||
|
The question category. Arbitrary text; optional.
|
||||||
|
"""
|
||||||
|
|
||||||
question = None
|
question = None
|
||||||
|
"""
|
||||||
|
The question. Arbitrary text; required.
|
||||||
|
"""
|
||||||
|
|
||||||
answer = None
|
answer = None
|
||||||
|
"""
|
||||||
|
The answer. Arbitrary text; required. Correct answers can also be covered
|
||||||
|
by the :attr:`regexp` property.
|
||||||
|
"""
|
||||||
|
|
||||||
regexp = None
|
regexp = None
|
||||||
|
"""
|
||||||
|
A regular expression that will generate correct answers. Optional. See
|
||||||
|
also the :attr:`answer` property.
|
||||||
|
"""
|
||||||
|
|
||||||
author = None
|
author = None
|
||||||
level = None
|
"""
|
||||||
|
The question author. Arbitrary text; optional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
level = None # Default: NORMAL (constructor)
|
||||||
|
"""
|
||||||
|
The difficulty level. Value must be from the :attr:`LEVELS` tuple.
|
||||||
|
The default value is :attr:`NORMAL`.
|
||||||
|
"""
|
||||||
|
|
||||||
comment = None
|
comment = None
|
||||||
score = 0
|
"""
|
||||||
|
A comment. Arbitrary text; optional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
score = 1
|
||||||
|
"""
|
||||||
|
The points scored for the correct answer. Integer value; default is 1.
|
||||||
|
"""
|
||||||
tip = list()
|
tip = list()
|
||||||
|
"""
|
||||||
|
An ordered list of tips (hints) to display to users. Optional.
|
||||||
|
"""
|
||||||
|
|
||||||
tipcycle = 0
|
tipcycle = 0
|
||||||
|
"""
|
||||||
|
Indicates which tip is to be displayed next, if any.
|
||||||
|
"""
|
||||||
|
|
||||||
TRIVIAL = 1
|
TRIVIAL = 1
|
||||||
|
"""
|
||||||
|
A value for :attr:`level` that indicates a question of trivial difficulty.
|
||||||
|
"""
|
||||||
|
|
||||||
EASY = 2
|
EASY = 2
|
||||||
|
"""
|
||||||
|
A value for :attr:`level` that indicates a question of easy difficulty.
|
||||||
|
"""
|
||||||
|
|
||||||
NORMAL = 3
|
NORMAL = 3
|
||||||
|
"""
|
||||||
|
A value for :attr:`level` that indicates a question of average or normal
|
||||||
|
difficulty.
|
||||||
|
"""
|
||||||
|
|
||||||
HARD = 4
|
HARD = 4
|
||||||
|
"""
|
||||||
|
A value for :attr:`level` that indicates a question of hard difficulty.
|
||||||
|
"""
|
||||||
|
|
||||||
EXTREME = 5
|
EXTREME = 5
|
||||||
|
"""
|
||||||
|
A value for :attr:`level` that indicates a question of extreme difficulty
|
||||||
|
or obscurity.
|
||||||
|
"""
|
||||||
|
|
||||||
LEVELS = (TRIVIAL, EASY, NORMAL, HARD, EXTREME)
|
LEVELS = (TRIVIAL, EASY, NORMAL, HARD, EXTREME)
|
||||||
|
"""
|
||||||
|
The available :attr:`level` difficulty values, :attr:`TRIVIAL`, :attr:`EASY`,
|
||||||
|
:attr:`NORMAL`, :attr:`HARD` and :attr:`EXTREME`.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, attributes_dict):
|
def __init__(self, attributes_dict):
|
||||||
|
"""
|
||||||
|
Constructor that takes a dictionary of MoxQuizz key-value pairs. Usually
|
||||||
|
called from a :class:`QuestionBank`.
|
||||||
|
"""
|
||||||
|
# Set defaults first.
|
||||||
|
self.level = self.NORMAL
|
||||||
self.parse(attributes_dict)
|
self.parse(attributes_dict)
|
||||||
|
|
||||||
def parse(self, attributes_dict):
|
def parse(self, attributes_dict):
|
||||||
"""
|
"""
|
||||||
Populate fields from a dictionary of attributes (from a question bank).
|
Populate fields from a dictionary of attributes, usually provided by a
|
||||||
|
:class:`QuestionBank` :attr:`~QuestionBank.parse` call.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
## Valid keys:
|
## Valid keys:
|
||||||
|
|
@ -69,7 +149,9 @@ class Question:
|
||||||
self.category = attributes_dict['Author']
|
self.category = attributes_dict['Author']
|
||||||
|
|
||||||
if 'Level' in attributes_dict.keys() and attributes_dict['Level'] in self.LEVELS:
|
if 'Level' in attributes_dict.keys() and attributes_dict['Level'] in self.LEVELS:
|
||||||
self.level = attributes_dict['level']
|
self.level = attributes_dict['Level']
|
||||||
|
elif 'Level' in attributes_dict.keys() and attributes_dict['Level'] in QuestionBank.LEVEL_VALUES.keys():
|
||||||
|
self.level = QuestionBank.LEVEL_VALUES[attributes_dict['Level']]
|
||||||
|
|
||||||
if 'Comment' in attributes_dict.keys():
|
if 'Comment' in attributes_dict.keys():
|
||||||
self.comment = attributes_dict['Comment']
|
self.comment = attributes_dict['Comment']
|
||||||
|
|
@ -83,6 +165,9 @@ class Question:
|
||||||
if 'Tipcycle' in attributes_dict.keys():
|
if 'Tipcycle' in attributes_dict.keys():
|
||||||
self.tipcycle = attributes_dict['Tipcycle']
|
self.tipcycle = attributes_dict['Tipcycle']
|
||||||
|
|
||||||
|
def attempt(self, answer):
|
||||||
|
return (self.answer is not None and self.answer.lower() == answer.lower()) or (
|
||||||
|
self.regexp is not None and re.search(self.regexp, answer, re.IGNORECASE) is not None)
|
||||||
|
|
||||||
class QuestionBank:
|
class QuestionBank:
|
||||||
"""
|
"""
|
||||||
|
|
@ -90,7 +175,15 @@ class QuestionBank:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filename = ''
|
filename = ''
|
||||||
|
"""
|
||||||
|
The path or filename of the question bank file.
|
||||||
|
"""
|
||||||
|
|
||||||
questions = list()
|
questions = list()
|
||||||
|
"""
|
||||||
|
A list of :class:`Question` objects, constituting the questions in the
|
||||||
|
question bank.
|
||||||
|
"""
|
||||||
|
|
||||||
# Case sensitive, to remain backwards-compatible with MoxQuizz.
|
# Case sensitive, to remain backwards-compatible with MoxQuizz.
|
||||||
KEYS = ('Answer',
|
KEYS = ('Answer',
|
||||||
|
|
@ -104,17 +197,34 @@ class QuestionBank:
|
||||||
'Tip',
|
'Tip',
|
||||||
'Tipcycle',
|
'Tipcycle',
|
||||||
)
|
)
|
||||||
|
"""
|
||||||
|
The valid attributes available in a MoxQuizz question bank file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
LEVEL_VALUES = {
|
||||||
|
'trivial': Question.TRIVIAL,
|
||||||
|
'baby': Question.TRIVIAL,
|
||||||
|
'easy': Question.EASY,
|
||||||
|
'normal': Question.NORMAL,
|
||||||
|
'hard': Question.HARD,
|
||||||
|
'difficult': Question.HARD,
|
||||||
|
'extreme': Question.EXTREME
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
Text labels for the :attr:`Question.level` difficulty values.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, filename):
|
def __init__(self, filename):
|
||||||
"""
|
"""
|
||||||
Construct a question bank from a file.
|
Constructor, takes a MozQuizz-formatted question bank filename.
|
||||||
"""
|
"""
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.questions = self.parse(filename)
|
self.questions = self.parse(filename)
|
||||||
|
|
||||||
def parse(self, filename):
|
def parse(self, filename):
|
||||||
"""
|
"""
|
||||||
Read a Moxquizz question bank file into a list.
|
Read a MoxQuizz-formatted question bank file. Returns a ``list`` of
|
||||||
|
:class:`Question` objects found in the file.
|
||||||
"""
|
"""
|
||||||
questions = list()
|
questions = list()
|
||||||
|
|
||||||
|
|
@ -149,6 +259,8 @@ class QuestionBank:
|
||||||
# Fetch the next parameter.
|
# Fetch the next parameter.
|
||||||
try:
|
try:
|
||||||
(key, value) = line.split(':', 1)
|
(key, value) = line.split(':', 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("Unexpected weirdness in MoxQuizz questionbank '%s', line %s." % (self.filename, i))
|
print("Unexpected weirdness in MoxQuizz questionbank '%s', line %s." % (self.filename, i))
|
||||||
continue
|
continue
|
||||||
|
|
@ -162,6 +274,11 @@ class QuestionBank:
|
||||||
# Enumerate the Tips.
|
# Enumerate the Tips.
|
||||||
if key == 'Tip':
|
if key == 'Tip':
|
||||||
q['Tip'].append(value.strip())
|
q['Tip'].append(value.strip())
|
||||||
|
elif key == 'Level':
|
||||||
|
if value not in self.LEVEL_VALUES:
|
||||||
|
print("Unexpected Level value '%s' in MoxQuizz questionbank '%s', line '%s'." % (value, self.filename, i))
|
||||||
|
else:
|
||||||
|
q['Level'] = self.LEVEL_VALUES[value]
|
||||||
else:
|
else:
|
||||||
q[key] = value.strip()
|
q[key] = value.strip()
|
||||||
|
|
||||||
|
|
@ -170,12 +287,14 @@ class QuestionBank:
|
||||||
|
|
||||||
# A crappy test.
|
# A crappy test.
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
qb = QuestionBank('questions.doctorlard.en')
|
qb = QuestionBank('questions/questions.doctorlard.en')
|
||||||
for q in qb.questions:
|
for q in qb.questions:
|
||||||
print(q.question)
|
print(q.question)
|
||||||
|
if sys.version.startswith('2'):
|
||||||
a = unicode(raw_input('A: '), 'utf8')
|
a = unicode(raw_input('A: '), 'utf8')
|
||||||
#a = input('A: ') # Python 3
|
else:
|
||||||
if a.lower() == q.answer.lower():
|
a = input('A: ')
|
||||||
|
if q.attempt(a):
|
||||||
print("Correct!")
|
print("Correct!")
|
||||||
else:
|
else:
|
||||||
print("Incorrect - the answer is '%s'" % q.answer)
|
print("Incorrect - the answer is '%s'" % q.answer)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue