From 3e133adf636852c76bd977e8ff1c42ddc7d6c3ad Mon Sep 17 00:00:00 2001 From: itqop Date: Mon, 28 Apr 2025 18:21:04 +0300 Subject: [PATCH] Fix errors --- .gitignore | 2 + bot/database/db.py | 10 ++++- bot/handlers/game.py | 62 ++++++++++++++++++++---------- bot/handlers/start.py | 26 +++++++++---- bot/handlers/stats.py | 29 +++++++++----- bot/handlers/text.py | 10 +++++ bot/keyboards/reply_keyboards.py | 23 +++++++++++ bot/main.py | 14 +++++-- bot/middlewares/auth_middleware.py | 20 +++++----- bot/utils/helpers.py | 11 +++++- 10 files changed, 153 insertions(+), 54 deletions(-) create mode 100644 bot/handlers/text.py create mode 100644 bot/keyboards/reply_keyboards.py diff --git a/.gitignore b/.gitignore index 3188cd4..7052d55 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,5 @@ dmypy.json Thumbs.db ehthumbs.db Desktop.ini + +data/ \ No newline at end of file diff --git a/bot/database/db.py b/bot/database/db.py index 3eca991..33ce31f 100644 --- a/bot/database/db.py +++ b/bot/database/db.py @@ -1,11 +1,14 @@ import aiosqlite from typing import List, Optional from datetime import date +import os +import logging from bot.config import settings from .models import User, Game, Streak, GameChoice DATABASE_URL = settings.database_name +os.makedirs(os.path.dirname(DATABASE_URL), exist_ok=True) async def init_db(): """Инициализирует базу данных и создает таблицы, если их нет.""" @@ -58,7 +61,12 @@ async def add_user(telegram_id: int, username: Optional[str]) -> Optional[User]: (telegram_id, username) ) await db.commit() - user_id = (await db.execute("SELECT last_insert_rowid()")).fetchone()[0] + cursor = await db.execute("SELECT last_insert_rowid()") + row = await cursor.fetchone() + if not row: + logging.error(f"Could not retrieve last_insert_rowid after inserting user {telegram_id}") + return None + user_id = row[0] await ensure_streak_record(user_id) diff --git a/bot/handlers/game.py b/bot/handlers/game.py index 82c562b..e2b886c 100644 --- a/bot/handlers/game.py +++ b/bot/handlers/game.py @@ -1,9 +1,9 @@ import logging from datetime import date, timedelta -from typing import Optional, Literal +from typing import Optional, Literal, Dict, Any from aiogram import Router, types, F, Bot -from aiogram.filters import Command +from aiogram.filters import Command, or_f from bot.database.models import User as DbUser, Game as DbGame, GameChoice from bot.database.db import ( @@ -16,6 +16,7 @@ from bot.database.db import ( get_game_on_date ) from bot.keyboards.game_keyboard import get_game_choice_keyboard, GameChoiceCallback +from bot.keyboards.reply_keyboards import PLAY_BUTTON_TEXT from bot.utils.game_logic import determine_winner, CHOICE_NAMES_RU from bot.utils.helpers import get_partner @@ -106,30 +107,40 @@ async def check_and_resolve_yesterdays_game(user_id: int, partner_id: int, bot: return True -@router.message(Command("play")) -async def handle_play(message: types.Message, db_user: DbUser, bot: Bot): +@router.message(or_f(Command("play"), F.text == PLAY_BUTTON_TEXT)) +async def handle_play( + message: types.Message, + bot: Bot, + **kwargs +): """Обработчик команды /play. Проверяет вчерашнюю игру и предлагает сделать ход.""" - partner = await get_partner(db_user.id) + user_db_obj: Optional[DbUser] = kwargs.get('user_db_obj') + if not user_db_obj: + logging.error(f"Error in handle_play: user_db_obj not found in kwargs for user {message.from_user.id}") + await message.answer("Произошла ошибка при получении ваших данных.") + return + + partner = await get_partner(user_db_obj.id) if not partner: await message.answer("Для игры нужен партнер. Дождитесь, пока второй пользователь присоединится.") return try: - await check_and_resolve_yesterdays_game(db_user.id, partner.id, bot) + await check_and_resolve_yesterdays_game(user_db_obj.id, partner.id, bot) except Exception as e: - logging.exception(f"Error checking/resolving yesterday's game for {db_user.id} and {partner.id}") + logging.exception(f"Error checking/resolving yesterday's game for {user_db_obj.id} and {partner.id}") - game = await create_or_get_today_game(db_user.id, partner.id) + game = await create_or_get_today_game(user_db_obj.id, partner.id) if not game: await message.answer("Не удалось начать игру. Попробуйте позже.") - logging.error(f"Failed to create/get game for users {db_user.id} and {partner.id}") + logging.error(f"Failed to create/get game for users {user_db_obj.id} and {partner.id}") return if game.is_finished: await message.answer("Вы уже сыграли сегодня! Приходите завтра ❤️") return - current_player_choice = game.player1_choice if game.player1_id == db_user.id else game.player2_choice + current_player_choice = game.player1_choice if game.player1_id == user_db_obj.id else game.player2_choice if current_player_choice: choice_name_ru = CHOICE_NAMES_RU.get(current_player_choice, current_player_choice) @@ -138,29 +149,38 @@ async def handle_play(message: types.Message, db_user: DbUser, bot: Bot): await message.answer("Кто больше любит сегодня? 😉 Сделайте свой выбор:", reply_markup=get_game_choice_keyboard()) - @router.callback_query(GameChoiceCallback.filter()) async def handle_game_choice( callback: types.CallbackQuery, callback_data: GameChoiceCallback, - db_user: DbUser, - bot: Bot + bot: Bot, + **kwargs ): """Обработчик нажатия на кнопку выбора хода.""" + user_db_obj: Optional[DbUser] = kwargs.get('user_db_obj') + if not user_db_obj: + logging.error(f"Error in handle_game_choice: user_db_obj not found in kwargs for user {callback.from_user.id}") + await callback.answer("Произошла ошибка при получении ваших данных.", show_alert=True) + try: + await callback.message.edit_text("Произошла ошибка при обработке вашего выбора.") + except Exception: + pass + return + choice: GameChoice = callback_data.choice choice_name_ru = CHOICE_NAMES_RU.get(choice, choice) - partner = await get_partner(db_user.id) + partner = await get_partner(user_db_obj.id) if not partner: await callback.answer("Ошибка: не найден партнер.", show_alert=True) await callback.message.edit_text("Не удалось обработать ваш ход: партнер не найден.") return - game = await create_or_get_today_game(db_user.id, partner.id) + game = await create_or_get_today_game(user_db_obj.id, partner.id) if not game: await callback.answer("Ошибка: не найдена текущая игра.", show_alert=True) await callback.message.edit_text("Не удалось обработать ваш ход: игра не найдена.") - logging.error(f"Game not found for users {db_user.id} and {partner.id} during callback") + logging.error(f"Game not found for users {user_db_obj.id} and {partner.id} during callback") return if game.is_finished: @@ -168,7 +188,7 @@ async def handle_game_choice( await callback.message.edit_text("Вы опоздали, игра на сегодня уже закончилась!" ) return - player_field = "player1_choice" if game.player1_id == db_user.id else "player2_choice" + player_field = "player1_choice" if game.player1_id == user_db_obj.id else "player2_choice" current_choice = getattr(game, player_field) if current_choice is not None: current_choice_ru = CHOICE_NAMES_RU.get(current_choice, current_choice) @@ -176,11 +196,11 @@ async def handle_game_choice( await callback.message.edit_text(f"Вы уже выбрали: {current_choice_ru}\nОжидаем ход партнера... ✨") return - updated = await update_game_choice(game.id, db_user.id, choice) + updated = await update_game_choice(game.id, user_db_obj.id, choice) if not updated: await callback.answer("Ошибка: не удалось сохранить ваш выбор.", show_alert=True) await callback.message.edit_text("Произошла ошибка при сохранении вашего хода. Попробуйте еще раз.") - logging.error(f"Failed to update choice for game {game.id}, user {db_user.id}") + logging.error(f"Failed to update choice for game {game.id}, user {user_db_obj.id}") return await callback.answer(f"Вы выбрали: {choice_name_ru}") @@ -204,7 +224,7 @@ async def handle_game_choice( if not p1_user or not p2_user: logging.error(f"Could not find user data for game {updated_game.id}") - await bot.send_message(db_user.telegram_id, "Ошибка: не удалось получить данные игроков для завершения игры.") + await bot.send_message(user_db_obj.telegram_id, "Ошибка: не удалось получить данные игроков для завершения игры.") await bot.send_message(partner.telegram_id, "Ошибка: не удалось получить данные игроков для завершения игры.") return @@ -239,7 +259,7 @@ async def handle_game_choice( else: partner_user = await get_user_by_id(partner.id) if partner_user: - current_user_name = db_user.username or f"Игрок {db_user.telegram_id}" + current_user_name = user_db_obj.username or f"Игрок {user_db_obj.telegram_id}" try: await bot.send_message( partner_user.telegram_id, diff --git a/bot/handlers/start.py b/bot/handlers/start.py index c6b63cb..1e19ecb 100644 --- a/bot/handlers/start.py +++ b/bot/handlers/start.py @@ -1,18 +1,30 @@ -from aiogram import Router, types +from aiogram import Router, types, F from aiogram.filters import CommandStart from bot.database.models import User as DbUser +from typing import Optional +import logging +# Импортируем новую клавиатуру +from bot.keyboards.reply_keyboards import get_main_menu_keyboard router = Router() @router.message(CommandStart()) -async def handle_start(message: types.Message, db_user: DbUser): +async def handle_start(message: types.Message, **kwargs): """Обработчик команды /start для аутентифицированных пользователей.""" - username = db_user.username or f"Пользователь {db_user.telegram_id}" + user_db_obj: Optional[DbUser] = kwargs.get('user_db_obj') + if not user_db_obj: + logging.error(f"Error in handle_start: user_db_obj not found in kwargs for user {message.from_user.id}") + await message.answer("Произошла ошибка при получении ваших данных.") + return + + username = user_db_obj.username or f"Пользователь {user_db_obj.telegram_id}" + # Отправляем приветствие и клавиатуру await message.answer( f"Привет, {username}! ✨\n\n" - f"Это бот 'Кто больше любит'. Каждый день вы со своей парой можете сыграть в Камень-Ножницы-Бумага, чтобы определить, кто любит больше! 😉\n\n" - f"Чтобы сделать ход, используй кнопки ниже (они появятся, когда придет время игры).\n" - f"Для просмотра статистики используй команду /stats.\n\n" - f"Удачи! ❤️" + f"Это бот 'Кто больше любит'. Используй кнопки ниже, чтобы играть или смотреть статистику! 😉", + reply_markup=get_main_menu_keyboard() ) + +# TODO: Возможно, убрать подробное описание правил из /start, +# так как теперь есть кнопки для основных действий. diff --git a/bot/handlers/stats.py b/bot/handlers/stats.py index dd274a0..5cdcaf9 100644 --- a/bot/handlers/stats.py +++ b/bot/handlers/stats.py @@ -1,6 +1,8 @@ import logging +import typing from aiogram import Router, types -from aiogram.filters import Command +from aiogram.filters import Command, or_f +from aiogram import F from bot.database.models import User as DbUser from bot.database.db import ( @@ -11,31 +13,38 @@ from bot.database.db import ( get_user_by_id ) from bot.utils.helpers import get_partner +from bot.keyboards.reply_keyboards import STATS_BUTTON_TEXT router = Router() -@router.message(Command("stats")) -async def handle_stats(message: types.Message, db_user: DbUser): +@router.message(or_f(Command("stats"), F.text == STATS_BUTTON_TEXT)) +async def handle_stats(message: types.Message, **kwargs): """Обработчик команды /stats. Выводит статистику игры.""" - partner = await get_partner(db_user.id) + user_db_obj: typing.Optional[DbUser] = kwargs.get('user_db_obj') + if not user_db_obj: + logging.error(f"Error in handle_stats: user_db_obj not found in kwargs for user {message.from_user.id}") + await message.answer("Произошла ошибка при получении ваших данных.") + return + + partner = await get_partner(user_db_obj.id) if not partner: await message.answer("Не могу показать статистику, так как не найден партнер.") return try: - total_games = await get_total_games_count(db_user.id, partner.id) - user_wins = await get_wins_count(db_user.id) + total_games = await get_total_games_count(user_db_obj.id, partner.id) + user_wins = await get_wins_count(user_db_obj.id) partner_wins = await get_wins_count(partner.id) - draws = await get_draw_count(db_user.id, partner.id) - user_streak = await get_streak(db_user.id) + draws = await get_draw_count(user_db_obj.id, partner.id) + user_streak = await get_streak(user_db_obj.id) partner_streak = await get_streak(partner.id) except Exception as e: - logging.exception(f"Error fetching stats for user {db_user.id} and partner {partner.id}") + logging.exception(f"Error fetching stats for user {user_db_obj.id} and partner {partner.id}") await message.answer("Произошла ошибка при получении статистики. Попробуйте позже.") return - user_name = db_user.username or f"Игрок {db_user.telegram_id}" + user_name = user_db_obj.username or f"Игрок {user_db_obj.telegram_id}" partner_name = partner.username or f"Игрок {partner.telegram_id}" stats_text = ( diff --git a/bot/handlers/text.py b/bot/handlers/text.py new file mode 100644 index 0000000..bebb241 --- /dev/null +++ b/bot/handlers/text.py @@ -0,0 +1,10 @@ +from aiogram import Router, types, F + +router = Router() + +@router.message(F.text) +async def catch_all_messages(message: types.Message): + """Ловит все текстовые сообщения без явных команд.""" + if message.text.startswith("/"): + return + pass \ No newline at end of file diff --git a/bot/keyboards/reply_keyboards.py b/bot/keyboards/reply_keyboards.py new file mode 100644 index 0000000..f684be8 --- /dev/null +++ b/bot/keyboards/reply_keyboards.py @@ -0,0 +1,23 @@ +# bot/keyboards/reply_keyboards.py + +from aiogram.types import ReplyKeyboardMarkup, KeyboardButton + +# Определим тексты кнопок как константы для удобства +PLAY_BUTTON_TEXT = "Играть ▶️" +STATS_BUTTON_TEXT = "Статистика 📊" + +def get_main_menu_keyboard() -> ReplyKeyboardMarkup: + """Возвращает основную клавиатуру с кнопками Играть и Статистика.""" + kb = [ + [ + KeyboardButton(text=PLAY_BUTTON_TEXT), + KeyboardButton(text=STATS_BUTTON_TEXT) + ] + ] + keyboard = ReplyKeyboardMarkup( + keyboard=kb, + resize_keyboard=True, # Делает кнопки более компактными + # one_time_keyboard=False # Клавиатура будет постоянной (это значение по умолчанию) + input_field_placeholder="Нажмите Играть или Статистика" + ) + return keyboard \ No newline at end of file diff --git a/bot/main.py b/bot/main.py index e485fa7..017dd85 100644 --- a/bot/main.py +++ b/bot/main.py @@ -4,8 +4,9 @@ import sys from aiogram import Bot, Dispatcher from aiogram.enums import ParseMode +from aiogram.client.default import DefaultBotProperties -from bot.handlers import start, game, stats +from bot.handlers import start, game, stats, text from bot.middlewares.auth_middleware import AuthMiddleware, load_initial_users from bot.database.db import init_db from bot.config import settings @@ -16,15 +17,20 @@ async def main() -> None: await load_initial_users() - bot = Bot(token=settings.bot_token.get_secret_value(), parse_mode=ParseMode.HTML) + bot = Bot( + token=settings.bot_token.get_secret_value(), + default=DefaultBotProperties(parse_mode=ParseMode.HTML) + ) dp = Dispatcher() - dp.update.outer_middleware(AuthMiddleware()) + dp.message.middleware(AuthMiddleware()) + dp.callback_query.middleware(AuthMiddleware()) + dp.include_router(start.router) dp.include_router(game.router) dp.include_router(stats.router) - # TODO: Раскомментировать и добавить роутеры, когда они будут готовы + dp.include_router(text.router) logging.info("Starting bot...") await bot.delete_webhook(drop_pending_updates=True) diff --git a/bot/middlewares/auth_middleware.py b/bot/middlewares/auth_middleware.py index 93291d7..6bd73c3 100644 --- a/bot/middlewares/auth_middleware.py +++ b/bot/middlewares/auth_middleware.py @@ -2,7 +2,7 @@ import logging from typing import Callable, Dict, Any, Awaitable, Set from aiogram import BaseMiddleware -from aiogram.types import Message, TelegramObject, User as AiogramUser +from aiogram.types import Message, CallbackQuery, TelegramObject, User as AiogramUser from bot.config import settings from bot.database.db import get_all_users, add_user, get_user_by_telegram_id @@ -36,7 +36,7 @@ class AuthMiddleware(BaseMiddleware): if not initial_users_loaded: await load_initial_users() - if not isinstance(event, Message): + if not isinstance(event, (Message, CallbackQuery)): return await handler(event, data) aiogram_user: AiogramUser = data.get('event_from_user') @@ -48,29 +48,31 @@ class AuthMiddleware(BaseMiddleware): if telegram_id in authenticated_user_ids: db_user = await get_user_by_telegram_id(telegram_id) - if db_user: - data['db_user'] = db_user + if db_user: + data['user_db_obj'] = db_user + logging.info(f"AuthMiddleware: Found authenticated user {telegram_id}. Passing user_db_obj to handler. Data keys: {list(data.keys())}") return await handler(event, data) else: logging.warning(f"User {telegram_id} is in authenticated_user_ids but not found in DB.") - return + return if event.text and event.text == settings.secret_password.get_secret_value(): if len(authenticated_user_ids) < 2: db_user = await add_user(telegram_id, aiogram_user.username) if db_user: authenticated_user_ids.add(telegram_id) - data['db_user'] = db_user + data['user_db_obj'] = db_user logging.info(f"User {telegram_id} ({aiogram_user.username}) authenticated successfully. Total users: {len(authenticated_user_ids)}") - return + await event.answer(f"Добро пожаловать, {db_user.username or f'Пользователь {db_user.telegram_id}'}! Вы успешно аутентифицированы. Используйте /start для начала.") + return else: logging.error(f"Failed to add user {telegram_id} to DB after password check.") await event.answer("Произошла ошибка при регистрации. Попробуйте позже.") - return + return else: logging.info(f"Authentication attempt blocked for user {telegram_id}. Limit of 2 users reached.") await event.answer("Извините, бот уже используется двумя пользователями.") - return + return else: if event.text != settings.secret_password.get_secret_value(): await event.answer("Для начала работы введите секретный пароль.") diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py index f025055..7b030fd 100644 --- a/bot/utils/helpers.py +++ b/bot/utils/helpers.py @@ -1,7 +1,10 @@ -from typing import Optional +from typing import Optional, Dict, Any + +# from aiogram.types import TelegramObject # Больше не нужен from bot.database.db import get_all_users from bot.database.models import User as DbUser +# import logging # Убираем импорт, если он больше нигде не используется async def get_partner(current_user_id: int) -> Optional[DbUser]: """Находит второго зарегистрированного пользователя (партнера).""" @@ -11,4 +14,8 @@ async def get_partner(current_user_id: int) -> Optional[DbUser]: for user in users: if user.id is not None and user.id != current_user_id: return user - return None \ No newline at end of file + return None + +# УДАЛЯЕМ ФУНКЦИЮ ЗАВИСИМОСТИ +# async def get_current_db_user(data: Dict[str, Any]) -> DbUser: +# ... \ No newline at end of file