From 88360d31154db7752709b84bfb1b1d5a819aaaf1 Mon Sep 17 00:00:00 2001 From: Oliver Raduner Date: Thu, 4 Jul 2019 18:17:10 +0200 Subject: [PATCH 1/7] Update README Changed the README to proper markdown format and added some good-to-know content. - file extension .md added - some minor styling updates (e.g. code formatting) - referenced files will now be linked to files in repo - added how-to installation of dependencies - added new section with IRC commands --- README => README.md | 48 +++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) rename README => README.md (76%) diff --git a/README b/README.md similarity index 76% rename from README rename to README.md index c03571f..cccdbde 100644 --- a/README +++ b/README.md @@ -1,7 +1,6 @@ quizbot ======= --------------------------------- Small and simple quizbot for IRC -------------------------------- @@ -9,15 +8,39 @@ Small and simple quizbot for IRC Use === -Hack config.py to your liking (it's pretty self-explanatory). Write questions -in questions.py, as per the format. Note: you need at least eleven question -for quizbot to work properly. +Hack [config.py](config.py) to your liking (it's pretty self-explanatory). + +Write questions in [questions.py](questions.py), as per the format. +*Note*: you need at least eleven question for quizbot to work properly. Start the bot with: - $ ./q -or: - $ python q +`$ ./q` +or +`$ python q` + +Start and keep the bot running in a background task: +`$ nohup ./q &` +or +`$ python ./q &` + +IRC +---- + +Join the same channel where quizbot is connected to. +*Note*: channel users with nick names as defined in the config under `masters=[]` can control the bot, e.g. force a reload of the questions. + +`!help` : lists available commands + +Masters commands: +`!op` : OP a master. +`!deop` : DEOP a master +`!reload` : Reload the question/answer list. + +General commands: +`!score` : Print the top five quizzers +`!hiscore` : Print the top five quizzers of all time +`!botsnack` : Feed quizbot to keep it going Dependencies ============ @@ -27,9 +50,11 @@ below are listed with the oldest versions that are confirmed to work. Older versions *might* work. If they do, please report it to , so that he can update this file. --python 2.7.x --twisted >= 11.0.0 --twisted-words >= 11.0.0 +- python 2.7.x +- twisted >= 11.0.0 +`$ apt-get install python-twisted` +- twisted-words >= 11.0.0 +`$ apt-get install python-twisted-words` A Note on Python 3 ------------------ @@ -38,7 +63,7 @@ The 3.x interpreter will try to run this and fail. You *need* to use a 2.x interpreter (2.7.x is the only one with which quizbot is formally tested). This may be accomplished by specifically invoking a 2.x interpreter on some systems. - $ python2 q +`$ python2 q` quizbot in #quiznode on Freenode @@ -106,5 +131,4 @@ Author Alexander Berntsen - /* vim: set textwidth=78 formatoptions=actw2 autoindent: */ From dab19420e42a90a9a0e59b38fb9897eb7bca6bf6 Mon Sep 17 00:00:00 2001 From: Oliver Raduner Date: Thu, 4 Jul 2019 18:19:18 +0200 Subject: [PATCH 2/7] Introduce UTF-8 encoding and stamina Added UTF-8 encoding to q in order to support Umlauts or other special characters from the questions file. Added a new config to set a higher level of stamina for the but, until it gets hungry. --- config.py | 2 ++ q | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/config.py b/config.py index ddb3894..45a3101 100644 --- a/config.py +++ b/config.py @@ -12,6 +12,8 @@ password = False # IRC nick names that can control the bot masters = [nickname, 'my_nickname'] +# How quickly the bot will get hungry +stamina = 6 # High score database file (is automatically created) hiscoresdb = 'hiscores.sql' # Whether to print 'category - question - answer' to STDOUT diff --git a/q b/q index 8e0b7ab..171fe9a 100644 --- a/q +++ b/q @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # Copyright (C) 2012, 2013 Alexander Berntsen # Copyright (C) 2012, 2013 Stian Ellingsen @@ -60,6 +61,7 @@ class Bot(irc.IRCClient): ' unique, wins INTEGER)') self.db.commit() self.hunger = 0 + self.stamina = config.stamina self.complained = False irc.IRCClient.connectionMade(self) @@ -151,7 +153,7 @@ class Bot(irc.IRCClient): def ask(self): """Ask a question.""" self.hunger += 1 - if self.hunger > 6: + if self.hunger > self.stamina: if not self.complained: self.msg(self.factory.channel, "I'm hungry. Please feed me with !botsnack.") From e56eefe425ac9acd42714be3a9d8035e567bc264 Mon Sep 17 00:00:00 2001 From: Oliver Raduner Date: Thu, 4 Jul 2019 19:05:42 +0200 Subject: [PATCH 3/7] Introduce UTF-8 encoding and stamina Added UTF-8 encoding to q in order to support Umlauts or other special characters from the questions file. Added a new config to set a higher level of stamina for the but, until it gets hungry. --- questions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/questions.py b/questions.py index c51e2f7..d0c94ab 100644 --- a/questions.py +++ b/questions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- questions = [ ['Category', 'Question 1?', 'Answer'], ['Category', 'Question 2?', 'Answer'], From 2f4bf41d36321f435972bdce373c66c9aee7d7d1 Mon Sep 17 00:00:00 2001 From: Oliver Raduner Date: Thu, 4 Jul 2019 20:18:50 +0200 Subject: [PATCH 4/7] Introduce hint patience time Added a new config to set a custom waiting time until the bot drops hints to question --- config.py | 4 +++- q | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index 45a3101..25c4db3 100644 --- a/config.py +++ b/config.py @@ -12,8 +12,10 @@ password = False # IRC nick names that can control the bot masters = [nickname, 'my_nickname'] -# How quickly the bot will get hungry +# How quickly the bot will get hungry (by asking questions or giving hints) stamina = 6 +# Patience the bot has until it gives hints (in seconds, minimum 5) +hintpatience = 10 # High score database file (is automatically created) hiscoresdb = 'hiscores.sql' # Whether to print 'category - question - answer' to STDOUT diff --git a/q b/q index 171fe9a..50e370b 100644 --- a/q +++ b/q @@ -50,6 +50,7 @@ class Bot(irc.IRCClient): self.password = self.factory.password self.username = self.factory.username self.quizzers = {} + self.hint_patience = config.hintpatience self.last_decide = 10 self.answered = 5 self.winner = '' @@ -143,7 +144,7 @@ class Bot(irc.IRCClient): """Figure out whether to post a question or a hint.""" t = time() f, dt = ((self.ask, self.answered + 5 - t) if self.answered else - (self.hint, self.last_decide + 10 - t)) + (self.hint, self.last_decide + self.hint_patience - t)) if dt < 0.5: f() self.last_decide = t From c15bf94f6632f1e2d7b87e8a109a930031717a6c Mon Sep 17 00:00:00 2001 From: Oliver Raduner Date: Thu, 4 Jul 2019 22:44:10 +0200 Subject: [PATCH 5/7] Customizable message strings In order to allow customizing (or translation, if one wants to) of the common message strings, a new file strings.py has been added. Additionally, the README has been updated to reflect this new feature. --- README.md | 3 +++ q | 52 ++++++++++++++++++++++++---------------------------- strings.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 28 deletions(-) create mode 100644 strings.py diff --git a/README.md b/README.md index cccdbde..bf33e07 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + quizbot ======= @@ -13,6 +14,8 @@ Hack [config.py](config.py) to your liking (it's pretty self-explanatory). Write questions in [questions.py](questions.py), as per the format. *Note*: you need at least eleven question for quizbot to work properly. +Edit or change common messages by redefining the according variables in [strings.py](strings.py). + Start the bot with: `$ ./q` or diff --git a/q b/q index 50e370b..e537603 100644 --- a/q +++ b/q @@ -31,6 +31,7 @@ from time import time from twisted.words.protocols import irc from twisted.internet import protocol, reactor +import strings import questions as q @@ -138,7 +139,7 @@ class Bot(irc.IRCClient): # Unknown command. elif msg[0] == '!': self.msg(self.factory.channel if channel != self.nickname else - name, '... wat.') + name, strings.unknowncmd) def decide(self): """Figure out whether to post a question or a hint.""" @@ -157,7 +158,7 @@ class Bot(irc.IRCClient): if self.hunger > self.stamina: if not self.complained: self.msg(self.factory.channel, - "I'm hungry. Please feed me with !botsnack.") + strings.botsnack) self.complained = True return # Make sure there have been ten questions in between this question. @@ -170,7 +171,7 @@ class Bot(irc.IRCClient): self.recently_asked.pop(0) self.recently_asked.append(self.question) self.answer = cqa[2] - self.msg(self.factory.channel, 'TOPIC: %s - Q: %s' % + self.msg(self.factory.channel, strings.question % (self.category, self.question)) if config.verbose: print '%s - %s - %s' % (self.category, self.question, self.answer) @@ -194,7 +195,7 @@ class Bot(irc.IRCClient): # Max 5 hints, and don't give hints when the answer is so short. if len(str(self.answer)) <= self.hint_num + 1 or self.hint_num >= 5: if (len(str(self.answer)) == 1 and self.hint_num == 0): - self.msg(self.factory.channel, 'HINT: only one character!') + self.msg(self.factory.channel, strings.hintone) self.hint_num += 1 else: self.fail() @@ -210,19 +211,19 @@ class Bot(irc.IRCClient): self.answer_hint = ''.join( '*' if idx in self.answer_masks and c is not ' ' else c for idx, c in enumerate(str(self.answer))) - self.msg(self.factory.channel, 'HINT: %s' % self.answer_hint) + self.msg(self.factory.channel, strings.hint % self.answer_hint) self.hint_num += 1 def fail(self): """Timeout/giveup on answer.""" - self.msg(self.factory.channel, 'the answer was: "%s"' % self.answer) - self.msg(self.factory.channel, 'better luck with the next question!') + self.msg(self.factory.channel, strings.rightanswer % self.answer) + self.msg(self.factory.channel, strings.wishluck) self.answered = time() def award(self, awardee): """Gives a point to awardee.""" self.quizzers[awardee] += 1 - self.msg(self.factory.channel, '%s is right! congratulations, %s!' % + self.msg(self.factory.channel, strings.correctanswer % (self.answer, awardee)) if self.quizzers[awardee] == self.target_score: self.win(awardee) @@ -255,7 +256,7 @@ class Bot(irc.IRCClient): self.winner = winner self.msg(self.factory.channel, - 'congratulations to %s, you\'re winner!!!' % self.winner) + strings.winner % self.winner) self.reset() def help(self, user): @@ -263,15 +264,12 @@ class Bot(irc.IRCClient): # Prevent spamming to non-quizzers, AKA random Freenode users. if user not in self.quizzers: return - self.msg(user, self.factory.channel + ' is a quiz channel.') - self.msg(user, 'I am ' + self.nickname + ', and *I* ask the' + - ' questions around here! :->') - self.msg(user, '!score prints the current top 5 quizzers.') - self.msg(user, '!hiscore prints the all time top 5 quizzers.') - self.msg(user, 'happy quizzing!') - self.msg(user, '(o, and BTW, I\'m hungry, like *all* the freaking' + - ' time.') - self.msg(user, 'you can feed me with !botsnack. please do. often.)') + + self.msg(user, strings.help_channelinfo % self.factory.channel) + self.msg(user, strings.help_botinfo % self.nickname) + + for msgline in strings.help: + self.msg(user, msgline) def reload_questions(self, user): """Reload the question/answer list.""" @@ -283,7 +281,7 @@ class Bot(irc.IRCClient): """Feed quizbot.""" self.hunger = 0 self.complained = False - self.msg(self.factory.channel, 'ta. :-)') + self.msg(self.factory.channel, strings.thanks) def op(self, user): """OP a master.""" @@ -304,7 +302,7 @@ class Bot(irc.IRCClient): if points: if points != prev_points: j = i - self.msg(self.factory.channel, '%d. %s: %d points' % + self.msg(self.factory.channel, strings.score % (j, quizzer, points)) prev_points = points @@ -313,7 +311,7 @@ class Bot(irc.IRCClient): self.dbcur.execute('SELECT * FROM hiscore ORDER by wins DESC LIMIT 5') hiscore = self.dbcur.fetchall() for i, (quizzer, wins) in enumerate(hiscore): - self.msg(self.factory.channel, '%d. %s: %d points' % + self.msg(self.factory.channel, strings.score % (i + 1, quizzer.encode('UTF-8'), wins)) def set_topic(self): @@ -322,11 +320,9 @@ class Bot(irc.IRCClient): if alltime is None: alltime = ["no one", 0] self.topic( - self.factory.channel, - 'happy quizzing. :-> target score: %d. previous winner: %s. ' - 'all-time winner: %s (%d).' % - (self.target_score, self.winner, alltime[0].encode('UTF-8'), - alltime[1])) + self.factory.channel, strings.channeltopic % + (self.target_score, self.winner, alltime[0].encode('UTF-8'), + alltime[1])) def reset(self): """Set all quizzers' points to 0 and change topic.""" @@ -359,8 +355,8 @@ class Bot(irc.IRCClient): return True if role == self.quizzers: return False - self.msg(self.factory.channel, 'not on my watch, %s!' % name) - self.kick(self.factory.channel, name, 'lol.') + self.msg(self.factory.channel, strings.invalidname % name) + self.kick(self.factory.channel, name, strings.kickmsg) self.del_quizzer(name) return False diff --git a/strings.py b/strings.py new file mode 100644 index 0000000..d44ddea --- /dev/null +++ b/strings.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Default message strings - DON'T change these! +question = 'TOPIC: %s - Q: %s' +botsnack = "I'm hungry. Please feed me with !botsnack." +hint = 'HINT: %s' +hintone = 'HINT: only one character!' +rightanswer = 'the answer was: "%s"' +wishluck = 'better luck with the next question!' +correctanswer = '%s is right! congratulations, %s!' +winner = 'congratulations to %s, you\'re winner!!!' +thanks = 'ta. :-)' +score = '%d. %s: %d points' +channeltopic = 'happy quizzing. :-> target score: %d. previous winner: %s. all-time winner: %s (%d).' +invalidname = 'not on my watch, %s!' +kickmsg = 'lol.' +help_channelinfo = '%s is a quiz channel.' +help_botinfo = 'I am %s, and *I* ask the questions around here! :->' +help = [ + '!score prints the current top 5 quizzers.', + '!hiscore prints the all time top 5 quizzers.', + 'happy quizzing!', + '(o, and BTW, I\'m hungry, like *all* the freaking time.', + 'you can feed me with !botsnack. please do. often.)' +] +unknowncmd = '... wat.' + +# Message strings - customize these as you desire +question = 'TOPIC: %s - Q: %s' +botsnack = "I'm hungry. Please feed me with !botsnack." +#... From 5aec7e7b5dcaf7b90ed36a7880887ba27c02508e Mon Sep 17 00:00:00 2001 From: Oliver Raduner Date: Fri, 5 Jul 2019 16:30:43 +0200 Subject: [PATCH 6/7] Enhance play settings and sqlite Introducing various enhancements to play quizzes with the bot and not loosing score when chaning IRC nick name. - a new "minplayer" config is used to define a minimum number of players who must be in a channel in order to start the questions - the "recently asked questions" are now calculated based on a percentage of total number of all questions - renamed the default hiscore sql-file to sqlite - new option to allow users to keep their score while changing their IRC nick name - added config setting validation with fallback to default values - sqlite execute() commands are wrapped with try:catch - sqlite3 connect() is initialised with isolation_level --- config.py | 8 +++++- q | 71 ++++++++++++++++++++++++++++++++++++++---------------- strings.py | 1 + 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/config.py b/config.py index 25c4db3..5047c1f 100644 --- a/config.py +++ b/config.py @@ -16,7 +16,13 @@ stamina = 6 # Patience the bot has until it gives hints (in seconds, minimum 5) hintpatience = 10 +# Minimum number of players required to start asking questions. +minplayers = 2 +# Threshold representing % of total questions to keep as "recently asked questions" +qrecyclethreshold = 20 # High score database file (is automatically created) -hiscoresdb = 'hiscores.sql' +hiscoresdb = 'hiscores.sqlite' +# Keep a player's scores when IRC nick changes. +keepscore = True # Whether to print 'category - question - answer' to STDOUT verbose = True diff --git a/q b/q index e537603..6873481 100644 --- a/q +++ b/q @@ -51,19 +51,26 @@ class Bot(irc.IRCClient): self.password = self.factory.password self.username = self.factory.username self.quizzers = {} - self.hint_patience = config.hintpatience + self.minplayernum = 2 if config.minplayers is None else config.minplayers + self.hint_patience = 6 if config.hintpatience is None else config.hintpatience self.last_decide = 10 self.answered = 5 self.winner = '' self.question = '' + if config.qrecyclethreshold is None: + self.recently_asked_threshold = ((20*len(q.questions))/100.0) + else: + self.recently_asked_threshold = ((config.qrecyclethreshold*len(q.questions))/100.0) self.recently_asked = [] - self.db = sqlite3.connect(config.hiscoresdb) + self.db = sqlite3.connect(config.hiscoresdb, isolation_level=None) self.dbcur = self.db.cursor() - self.dbcur.execute('CREATE TABLE IF NOT EXISTS hiscore (quizzer TEXT' - ' unique, wins INTEGER)') + try: + self.dbcur.execute('CREATE TABLE IF NOT EXISTS hiscore (quizzer TEXT unique, wins INTEGER)') + except self.db.IntegrityError as e: + print('sqlite error: ', e.args[0]) self.db.commit() self.hunger = 0 - self.stamina = config.stamina + self.stamina = 6 if config.stamina is None else config.stamina self.complained = False irc.IRCClient.connectionMade(self) @@ -99,6 +106,15 @@ class Bot(irc.IRCClient): """Overrides USERRENAMED.""" self.del_quizzer(oldname) self.add_quizzer(newname) + # Change quizzer name in DB to keep score. + if config.keepscore: + self.dbcur.execute('SELECT * FROM hiscore WHERE quizzer=?', + (oldname,)) + row = self.dbcur.fetchone() + if row is not None: + self.dbcur.execute('UPDATE hiscore SET quizzer=? WHERE quizzer=?', + (newname, oldname)) + self.db.commit() def irc_RPL_NAMREPLY(self, prefix, params): """Overrides RPL_NAMEREPLY.""" @@ -142,18 +158,27 @@ class Bot(irc.IRCClient): name, strings.unknowncmd) def decide(self): - """Figure out whether to post a question or a hint.""" - t = time() - f, dt = ((self.ask, self.answered + 5 - t) if self.answered else - (self.hint, self.last_decide + self.hint_patience - t)) - if dt < 0.5: - f() - self.last_decide = t - dt = 5 - reactor.callLater(min(5, dt), self.decide) + """Wait for enough players.""" + numPlayers = len(self.quizzers) + if numPlayers < self.minplayernum: + self.msg(self.factory.channel, strings.waiting) + reactor.callLater(30, self.decide) + return + else: + numPlayers += 1 + if numPlayers >= self.minplayernum: + """Figure out whether to post a question or a hint.""" + t = time() + f, dt = ((self.ask, self.answered + 5 - t) if self.answered else + (self.hint, self.last_decide + self.hint_patience - t)) + if dt < 0.5: + f() + self.last_decide = t + dt = 5 + reactor.callLater(min(5, dt), self.decide) def ask(self): - """Ask a question.""" + """Make bot hungy.""" self.hunger += 1 if self.hunger > self.stamina: if not self.complained: @@ -161,13 +186,14 @@ class Bot(irc.IRCClient): strings.botsnack) self.complained = True return + """Ask a question.""" # Make sure there have been ten questions in between this question. while self.question in self.recently_asked or not self.question: cqa = choice(q.questions) self.question = cqa[1] self.category = cqa[0] - # This num should be changed depending on how many questions you have. - if len(self.recently_asked) >= 10: + # Clear recently asked questions when threshold is reached + if len(self.recently_asked) >= self.recently_asked_threshold: self.recently_asked.pop(0) self.recently_asked.append(self.question) self.answer = cqa[2] @@ -243,15 +269,18 @@ class Bot(irc.IRCClient): if numAnswerers > 1: winner = quizzersByPoints[0][0] self.dbcur.execute('SELECT * FROM hiscore WHERE quizzer=?', - (winner,)) + (winner,)) wins = 1 row = self.dbcur.fetchone() if row is not None: wins = row[1] + 1 - sql = 'UPDATE hiscore SET wins = ? WHERE quizzer = ?' + sql = 'UPDATE hiscore SET wins=? WHERE quizzer=?' else: - sql = 'INSERT INTO hiscore (wins,quizzer) VALUES (?,?)' - self.dbcur.execute(sql, (wins, winner)) + sql = 'INSERT INTO hiscore (wins, quizzer) VALUES (?, ?)' + try: + self.dbcur.execute(sql, (wins, winner)) + except self.db.IntegrityError as e: + print('sqlite error: ', e.args[0]) self.db.commit() self.winner = winner diff --git a/strings.py b/strings.py index d44ddea..be74d02 100644 --- a/strings.py +++ b/strings.py @@ -2,6 +2,7 @@ # Default message strings - DON'T change these! question = 'TOPIC: %s - Q: %s' +waiting = "Playing alone is no fun. Let's wait for others..." botsnack = "I'm hungry. Please feed me with !botsnack." hint = 'HINT: %s' hintone = 'HINT: only one character!' From d6d28aaf4058af3eede5f920f7803d531debea8d Mon Sep 17 00:00:00 2001 From: oliveratgithub Date: Sat, 16 Nov 2024 14:41:39 +0100 Subject: [PATCH 7/7] Add functionality for static password from config --- q | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/q b/q index 6873481..f11583f 100644 --- a/q +++ b/q @@ -407,7 +407,9 @@ class BotFactory(protocol.ClientFactory): self.channel = channel self.nickname = config.nickname self.username = config.username - if config.password: + if config.password and isinstance(config.password, str): + self.password = config.password + elif config.password and config.password is True: self.password = getpass('enter password (will not be echoed): ') self.masters = config.masters