diff --git a/src/bot/handlers/parse_handler.py b/src/bot/handlers/parse_handler.py new file mode 100644 index 0000000..be29d34 --- /dev/null +++ b/src/bot/handlers/parse_handler.py @@ -0,0 +1,62 @@ +import base64 + +import requests +from codenames.game.color import CardColor +from telegram import PhotoSize +from the_spymaster_util.logger import get_logger + +from bot.handlers.base import EventHandler +from bot.models import BadMessageError, BotState + +log = get_logger(__name__) + + +class ParseHandler(EventHandler): + def handle(self): + self.send_text("πŸ—ΊοΈ Please send me a picture of the map:") + return BotState.PARSE_MAP + + +class ParseMapHandler(EventHandler): + def handle(self): + photos = self.update.message.photo + if not photos: + raise BadMessageError("No photo found in message") + log.info(f"Got {len(photos)} photos, downloading the largest one") + photo_meta = _get_largest_photo(photos) + photo_ptr = photo_meta.get_file() + photo_bytes = photo_ptr.download_as_bytearray() + photo_base64 = base64.b64encode(photo_bytes).decode("utf-8") + log.info("Downloaded and encoded photo") + response = requests.get( + url="http://localhost:5000/parse-color-map", json={"map_image_b64": photo_base64}, timeout=15 + ) + response.raise_for_status() + response_json = response.json() + map_colors = response_json.get("map_colors") + card_colors = [CardColor(color) for color in map_colors] + self._send_as_emoji_table(card_colors) + self.send_text("🧩 Please send me a picture of the board:") + return BotState.PARSE_MAP + + def _send_as_emoji_table(self, card_colors: list[CardColor]): + result = "πŸ” Parsed: \n" + for i in range(0, len(card_colors), 5): + row = card_colors[i : i + 5] + row_emojis = " ".join(card.emoji for card in row) + result += f"{row_emojis}\n" + self.send_text(result) + + +def _get_largest_photo(photos: list[PhotoSize]) -> PhotoSize: + return max(photos, key=lambda photo: photo.file_size) + + +class ParseBoardHandler(EventHandler): + def handle(self): + photo = self.update.message.photo + if not photo: + raise BadMessageError("No photo found in message") + + self.send_text("OK: I got the board. Let's play!") + return BotState.ENTRY diff --git a/src/bot/models.py b/src/bot/models.py index 4ee3c59..cc3080b 100644 --- a/src/bot/models.py +++ b/src/bot/models.py @@ -1,4 +1,4 @@ -from enum import IntEnum, auto +from enum import IntEnum from typing import Optional from codenames.game.color import CardColor, TeamColor @@ -34,13 +34,15 @@ class BadMessageError(Exception): class BotState(IntEnum): - ENTRY = auto() - CONFIG_LANGUAGE = auto() - CONFIG_SOLVER = auto() - CONFIG_DIFFICULTY = auto() - CONFIG_MODEL = auto() - CONTINUE_GET_ID = auto() - PLAYING = auto() + ENTRY = 0 + CONFIG_LANGUAGE = 10 + CONFIG_SOLVER = 11 + CONFIG_DIFFICULTY = 12 + CONFIG_MODEL = 13 + CONTINUE_GET_ID = 20 + PLAYING = 30 + PARSE_MAP = 40 + PARSE_BOARD = 41 class GameConfig(BaseModel): # Move to backend api? diff --git a/src/bot/the_spymaster_bot.py b/src/bot/the_spymaster_bot.py index 7a72048..18d3a87 100644 --- a/src/bot/the_spymaster_bot.py +++ b/src/bot/the_spymaster_bot.py @@ -34,6 +34,7 @@ StartEventHandler, TestingHandler, ) +from bot.handlers.parse_handler import ParseBoardHandler, ParseHandler, ParseMapHandler from bot.models import AVAILABLE_MODELS, BotState from persistence.dynamo_db_persistence import DynamoDbPersistence @@ -77,24 +78,34 @@ def parse_update(self, update: dict) -> Optional[Update]: def _construct_updater(self): # pylint: disable=too-many-locals log.info("Setting up bot...") + # Start start_handler = CommandHandler("start", self.generate_callback(StartEventHandler)) custom_handler = CommandHandler("custom", self.generate_callback(CustomHandler)) + # Config config_language_handler = MessageHandler(Filters.text, self.generate_callback(ConfigLanguageHandler)) config_solver_handler = MessageHandler(Filters.text, self.generate_callback(ConfigSolverHandler)) config_difficulty_handler = MessageHandler(Filters.text, self.generate_callback(ConfigDifficultyHandler)) config_model_handler = MessageHandler(Filters.text, self.generate_callback(ConfigModelHandler)) - continue_game_handler = CommandHandler("continue", self.generate_callback(ContinueHandler)) - continue_get_id_handler = MessageHandler(Filters.text, self.generate_callback(ContinueGetIdHandler)) - fallback_handler = CommandHandler("quit", self.generate_callback(FallbackHandler)) - help_message_handler = CommandHandler("help", self.generate_callback(HelpMessageHandler)) - get_sessions_handler = CommandHandler("sessions", self.generate_callback(GetSessionsHandler)) - load_models_handler = CommandHandler("load_models", self.generate_callback(LoadModelsHandler)) - testing_handler = CommandHandler("test", self.generate_callback(TestingHandler)) + # Game process_message_handler = MessageHandler( Filters.text & ~Filters.command, self.generate_callback(ProcessMessageHandler) ) next_move_handler = CommandHandler("next_move", self.generate_callback(NextMoveHandler)) + # Parsing + parse_handler = CommandHandler("parse", self.generate_callback(ParseHandler)) + parse_map_handler = MessageHandler(Filters.photo, self.generate_callback(ParseMapHandler)) + parse_board_handler = MessageHandler(Filters.photo, self.generate_callback(ParseBoardHandler)) + # Util + fallback_handler = CommandHandler("quit", self.generate_callback(FallbackHandler)) + help_message_handler = CommandHandler("help", self.generate_callback(HelpMessageHandler)) error_handler = self.generate_callback(ErrorHandler) + # Internal + load_models_handler = CommandHandler("load_models", self.generate_callback(LoadModelsHandler)) + testing_handler = CommandHandler("test", self.generate_callback(TestingHandler)) + # Not supported + continue_game_handler = CommandHandler("continue", self.generate_callback(ContinueHandler)) + continue_get_id_handler = MessageHandler(Filters.text, self.generate_callback(ContinueGetIdHandler)) + get_sessions_handler = CommandHandler("sessions", self.generate_callback(GetSessionsHandler)) conv_handler = ConversationHandler( name="main", @@ -107,6 +118,7 @@ def _construct_updater(self): # pylint: disable=too-many-locals testing_handler, get_sessions_handler, continue_game_handler, + parse_handler, ], states={ BotState.CONFIG_LANGUAGE: [config_language_handler], @@ -115,6 +127,8 @@ def _construct_updater(self): # pylint: disable=too-many-locals BotState.CONFIG_MODEL: [config_model_handler, fallback_handler], BotState.CONTINUE_GET_ID: [continue_get_id_handler], BotState.PLAYING: [process_message_handler], + BotState.PARSE_MAP: [parse_map_handler, fallback_handler], + BotState.PARSE_BOARD: [parse_board_handler, fallback_handler], }, fallbacks=[fallback_handler], allow_reentry=True, diff --git a/src/main.py b/src/main.py index df8a99f..53bb2f5 100644 --- a/src/main.py +++ b/src/main.py @@ -2,7 +2,6 @@ from bot.config import configure_logging, get_config from bot.the_spymaster_bot import TheSpymasterBot -from lambda_handler import handle def main(): @@ -18,6 +17,8 @@ def main(): def example_event(): + from lambda_handler import handle # pylint: disable=import-outside-toplevel + telegram_update = { "update_id": 617241338, "message": { @@ -34,6 +35,8 @@ def example_event(): def example_warmup(): + from lambda_handler import handle # pylint: disable=import-outside-toplevel + update = {"action": "warmup"} event = {"body": json.dumps(update)} handle(event) diff --git a/src/settings.toml b/src/settings.toml index ab4f35f..757b846 100644 --- a/src/settings.toml +++ b/src/settings.toml @@ -21,10 +21,11 @@ should_load_ssm_parameters = false std_formatter = "simple" bot_log_level = "DEBUG" persistence_db_table_name = "the-spymaster-bot-dev-persistence-table" +base_parser_url = "http://localhost:5000" [dev] env_verbose_name = "Dev" -base_backend_url = "https://backend.dev.303707.xyz" +base_parser_url = "https://parser.dev.303707.xyz" [stage] env_verbose_name = "Staging" @@ -32,6 +33,7 @@ env_verbose_name = "Staging" [prod] env_verbose_name = "Production" base_backend_url = "https://backend.303707.xyz" +base_parser_url = "https://parser.303707.xyz" [rsbpi] env_verbose_name = "Raspberry Pi"