Fix errors

This commit is contained in:
itqop 2025-04-28 18:21:04 +03:00
parent 5dd2c798e5
commit 3e133adf63
10 changed files with 153 additions and 54 deletions

2
.gitignore vendored
View File

@ -86,3 +86,5 @@ dmypy.json
Thumbs.db
ehthumbs.db
Desktop.ini
data/

View File

@ -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)

View File

@ -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,

View File

@ -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,
# так как теперь есть кнопки для основных действий.

View File

@ -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 = (

10
bot/handlers/text.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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("Для начала работы введите секретный пароль.")

View File

@ -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
return None
# УДАЛЯЕМ ФУНКЦИЮ ЗАВИСИМОСТИ
# async def get_current_db_user(data: Dict[str, Any]) -> DbUser:
# ...