From ea939289b647567756735133c6b5b0157c770b09 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 3 Sep 2019 15:16:12 +0200 Subject: [PATCH] Add trivia command (closes #78) --- royalnet/bots/generic.py | 1 + royalnet/bots/telegram.py | 27 +++++-- royalnet/commands/royalgames/__init__.py | 4 +- royalnet/commands/royalgames/trivia.py | 94 ++++++++++++++++++++++++ royalnet/error.py | 4 + royalnet/royalgames.py | 7 +- 6 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 royalnet/commands/royalgames/trivia.py diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py index b6a75b67..d6e0d57a 100644 --- a/royalnet/bots/generic.py +++ b/royalnet/bots/generic.py @@ -27,6 +27,7 @@ class GenericBot: self.commands = {} self.network_handlers: typing.Dict[str, typing.Type[NetworkHandler]] = {} for SelectedCommand in commands: + log.debug(f"Binding {SelectedCommand.name}...") interface = self._Interface() self.commands[f"{interface.prefix}{SelectedCommand.name}"] = SelectedCommand(interface) log.debug(f"Successfully bound commands") diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index 9a4621a4..0841df14 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -152,17 +152,30 @@ class TelegramBot(GenericBot): async def _handle_callback_query(self, update: telegram.Update): query: telegram.CallbackQuery = update.callback_query source: telegram.Message = query.message + callback: typing.Optional[typing.Callable] = None + command: typing.Optional[Command] = None for command in self.commands.values(): - try: + if query.data in command.interface.keys_callbacks: callback = command.interface.keys_callbacks[query.data] - except KeyError: - continue - await callback(data=self._Data(interface=command.interface, update=update)) - await asyncify(query.answer) - break - else: + break + if callback is None: await asyncify(source.edit_reply_markup, reply_markup=None) await asyncify(query.answer, text="⛔️ This keyboard has expired.") + return + try: + response = await callback(data=self._Data(interface=command.interface, update=update)) + except KeyboardExpiredError: + # FIXME: May cause a memory leak, as keys are not deleted after use + await asyncify(source.edit_reply_markup, reply_markup=None) + await asyncify(query.answer, text="⛔️ This keyboard has expired.") + return + except Exception as e: + error_text = f"⛔️ {e.__class__.__name__}\n" + error_text += '\n'.join(e.args) + await asyncify(query.answer, text=error_text) + return + else: + await asyncify(query.answer, text=response) async def run(self): while True: diff --git a/royalnet/commands/royalgames/__init__.py b/royalnet/commands/royalgames/__init__.py index 2ac2e64e..4cfc4841 100644 --- a/royalnet/commands/royalgames/__init__.py +++ b/royalnet/commands/royalgames/__init__.py @@ -22,6 +22,7 @@ from .summon import SummonCommand from .videochannel import VideochannelCommand from .dnditem import DnditemCommand from .dndspell import DndspellCommand +from .trivia import TriviaCommand __all__ = [ "CiaoruoziCommand", @@ -42,5 +43,6 @@ __all__ = [ "SummonCommand", "VideochannelCommand", "DnditemCommand", - "DndspellCommand" + "DndspellCommand", + "TriviaCommand" ] diff --git a/royalnet/commands/royalgames/trivia.py b/royalnet/commands/royalgames/trivia.py new file mode 100644 index 00000000..31ab7908 --- /dev/null +++ b/royalnet/commands/royalgames/trivia.py @@ -0,0 +1,94 @@ +import typing +import asyncio +import aiohttp +import random +import uuid +from ..command import Command +from ..commandargs import CommandArgs +from ..commanddata import CommandData +from ..commandinterface import CommandInterface +from ...error import * + + +class TriviaCommand(Command): + name: str = "trivia" + + description: str = "Manda una domanda dell'OpenTDB in chat." + + _letter_emojis = ["🇦", "🇧", "🇨", "🇩"] + + _correct_emoji = "✅" + + _wrong_emoji = "❌" + + _answer_time = 15 + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + self.answerers: typing.Dict[uuid.UUID, typing.Dict[..., bool]] = {} + + async def run(self, args: CommandArgs, data: CommandData) -> None: + # Fetch the question + async with aiohttp.ClientSession() as session: + async with session.get("https://opentdb.com/api.php?amount=1") as response: + j = await response.json() + # Parse the question + if j["response_code"] != 0: + raise ExternalError(f"OpenTDB returned {j['response_code']} response_code") + question = j["results"][0] + text = f'❓ [b]{question["category"]} - {question["difficulty"].capitalize()}[/b]\n' \ + f'{question["question"]}' + # Prepare answers + correct_answer: str = question["correct_answer"] + wrong_answers: typing.List[str] = question["incorrect_answers"] + answers = [correct_answer, *wrong_answers] + random.shuffle(answers) + # Find the correct index + for index, answer in enumerate(answers): + if answer == correct_answer: + correct_index = index + break + else: + raise ValueError("correct_index not found") + # Add emojis + for index, answer in enumerate(answers): + answers[index] = f"{self._letter_emojis[index]} {answers[index]}" + # Create the question id + question_id = uuid.uuid4() + self.answerers[question_id] = {} + + # Create the correct and wrong functions + async def correct(data: CommandData): + answerer_ = await data.get_author(error_if_none=True) + try: + self.answerers[question_id][answerer_] = True + except KeyError: + raise KeyboardExpiredError("Question time ran out.") + return "🆗 Hai risposto alla domanda. Ora aspetta un attimo per i risultati!" + + async def wrong(data: CommandData): + answerer_ = await data.get_author(error_if_none=True) + try: + self.answerers[question_id][answerer_] = False + except KeyError: + raise KeyboardExpiredError("Question time ran out.") + return "🆗 Hai risposto alla domanda. Ora aspetta un attimo per i risultati!" + + # Add question + keyboard = {} + for index, answer in enumerate(answers): + if index == correct_index: + keyboard[answer] = correct + else: + keyboard[answer] = wrong + await data.keyboard(text, keyboard) + await asyncio.sleep(self._answer_time) + results = f"❗️ Tempo scaduto!\n" \ + f"La risposta corretta era [b]{answers[correct_index]}[/b]!\n\n" + for answerer in self.answerers[question_id]: + if self.answerers[question_id][answerer]: + results += self._correct_emoji + else: + results += self._wrong_emoji + results += f" {answerer}" + await data.reply(results) diff --git a/royalnet/error.py b/royalnet/error.py index 3681123f..2a6a75e8 100644 --- a/royalnet/error.py +++ b/royalnet/error.py @@ -49,3 +49,7 @@ class FileTooBigError(Exception): class CurrentlyDisabledError(Exception): """This feature is temporarely disabled and is not available right now.""" + + +class KeyboardExpiredError(Exception): + """A special type of exception that can be raised in keyboard handlers to mark a specific keyboard as expired.""" diff --git a/royalnet/royalgames.py b/royalnet/royalgames.py index 030c7448..41d4ed7c 100644 --- a/royalnet/royalgames.py +++ b/royalnet/royalgames.py @@ -19,14 +19,12 @@ stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{m log.addHandler(stream_handler) - sentry_dsn = os.environ.get("SENTRY_DSN") # noinspection PyUnreachableCode if __debug__: commands = [ - DebugErrorCommand, - DebugKeyboardCommand + ] log.setLevel(logging.DEBUG) else: @@ -49,7 +47,8 @@ else: SummonCommand, VideochannelCommand, DnditemCommand, - DndspellCommand + DndspellCommand, + TriviaCommand ] log.setLevel(logging.INFO)