initial
This commit is contained in:
parent
24fa5a928c
commit
5dd2c798e5
|
@ -0,0 +1,88 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
.env/
|
||||||
|
.venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Django stuff
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Pycharm
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# SQL databases
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Mac OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
|
@ -0,0 +1,12 @@
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["python", "-m", "bot.main"]
|
|
@ -0,0 +1 @@
|
||||||
|
# Mark directories as Python packages
|
|
@ -0,0 +1,10 @@
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
from pydantic import SecretStr
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
bot_token: SecretStr
|
||||||
|
secret_password: SecretStr
|
||||||
|
database_name: str = "data/love_bot.db"
|
||||||
|
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', extra='ignore')
|
||||||
|
|
||||||
|
settings = Settings()
|
|
@ -0,0 +1 @@
|
||||||
|
# Mark directories as Python packages
|
|
@ -0,0 +1,318 @@
|
||||||
|
import aiosqlite
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from bot.config import settings
|
||||||
|
from .models import User, Game, Streak, GameChoice
|
||||||
|
|
||||||
|
DATABASE_URL = settings.database_name
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
"""Инициализирует базу данных и создает таблицы, если их нет."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
telegram_id INTEGER UNIQUE NOT NULL,
|
||||||
|
username TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS games (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
game_date DATE NOT NULL,
|
||||||
|
player1_id INTEGER NOT NULL,
|
||||||
|
player1_choice TEXT CHECK(player1_choice IN ('rock', 'scissors', 'paper')),
|
||||||
|
player2_id INTEGER NOT NULL,
|
||||||
|
player2_choice TEXT CHECK(player2_choice IN ('rock', 'scissors', 'paper')),
|
||||||
|
winner_id INTEGER,
|
||||||
|
is_finished BOOLEAN DEFAULT FALSE,
|
||||||
|
FOREIGN KEY (player1_id) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (player2_id) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (winner_id) REFERENCES users (id),
|
||||||
|
UNIQUE (game_date, player1_id, player2_id) -- Гарантирует одну игру в день для пары
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS streaks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER UNIQUE NOT NULL,
|
||||||
|
current_streak INTEGER DEFAULT 0,
|
||||||
|
max_streak INTEGER DEFAULT 0,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
async def add_user(telegram_id: int, username: Optional[str]) -> Optional[User]:
|
||||||
|
"""Добавляет нового пользователя, если его еще нет. Возвращает объект User."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute("SELECT id, telegram_id, username FROM users WHERE telegram_id = ?", (telegram_id,)) as cursor:
|
||||||
|
user_row = await cursor.fetchone()
|
||||||
|
if user_row:
|
||||||
|
return User(id=user_row[0], telegram_id=user_row[1], username=user_row[2])
|
||||||
|
|
||||||
|
try:
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO users (telegram_id, username) VALUES (?, ?)",
|
||||||
|
(telegram_id, username)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
user_id = (await db.execute("SELECT last_insert_rowid()")).fetchone()[0]
|
||||||
|
|
||||||
|
await ensure_streak_record(user_id)
|
||||||
|
|
||||||
|
return User(id=user_id, telegram_id=telegram_id, username=username)
|
||||||
|
except aiosqlite.IntegrityError:
|
||||||
|
async with db.execute("SELECT id, telegram_id, username FROM users WHERE telegram_id = ?", (telegram_id,)) as cursor:
|
||||||
|
user_row = await cursor.fetchone()
|
||||||
|
if user_row:
|
||||||
|
return User(id=user_row[0], telegram_id=user_row[1], username=user_row[2])
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_user_by_telegram_id(telegram_id: int) -> Optional[User]:
|
||||||
|
"""Ищет пользователя по его Telegram ID."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute("SELECT id, telegram_id, username FROM users WHERE telegram_id = ?", (telegram_id,)) as cursor:
|
||||||
|
user_row = await cursor.fetchone()
|
||||||
|
if user_row:
|
||||||
|
return User(id=user_row[0], telegram_id=user_row[1], username=user_row[2])
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_user_by_id(user_id: int) -> Optional[User]:
|
||||||
|
"""Ищет пользователя по его ID в базе данных."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute("SELECT id, telegram_id, username FROM users WHERE id = ?", (user_id,)) as cursor:
|
||||||
|
user_row = await cursor.fetchone()
|
||||||
|
if user_row:
|
||||||
|
return User(id=user_row[0], telegram_id=user_row[1], username=user_row[2])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_users() -> List[User]:
|
||||||
|
"""Возвращает список всех зарегистрированных пользователей."""
|
||||||
|
users = []
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute("SELECT id, telegram_id, username FROM users") as cursor:
|
||||||
|
async for row in cursor:
|
||||||
|
users.append(User(id=row[0], telegram_id=row[1], username=row[2]))
|
||||||
|
return users
|
||||||
|
|
||||||
|
# --- Функции для работы со стриками (заглушка ensure_streak_record добавлена для add_user) ---
|
||||||
|
|
||||||
|
async def ensure_streak_record(user_id: int) -> Streak:
|
||||||
|
"""
|
||||||
|
Гарантирует наличие записи о стриках для пользователя.
|
||||||
|
Если записи нет, создает ее с нулевыми значениями.
|
||||||
|
Возвращает объект Streak.
|
||||||
|
"""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute("SELECT id, user_id, current_streak, max_streak FROM streaks WHERE user_id = ?", (user_id,)) as cursor:
|
||||||
|
streak_row = await cursor.fetchone()
|
||||||
|
if streak_row:
|
||||||
|
return Streak(id=streak_row[0], user_id=streak_row[1], current_streak=streak_row[2], max_streak=streak_row[3])
|
||||||
|
else:
|
||||||
|
await db.execute("INSERT INTO streaks (user_id) VALUES (?)", (user_id,))
|
||||||
|
await db.commit()
|
||||||
|
async with db.execute("SELECT id, user_id, current_streak, max_streak FROM streaks WHERE user_id = ?", (user_id,)) as new_cursor:
|
||||||
|
new_streak_row = await new_cursor.fetchone()
|
||||||
|
if new_streak_row:
|
||||||
|
return Streak(id=new_streak_row[0], user_id=new_streak_row[1], current_streak=new_streak_row[2], max_streak=new_streak_row[3])
|
||||||
|
else:
|
||||||
|
raise Exception(f"Could not create or find streak record for user_id {user_id}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Функции для работы с играми ---
|
||||||
|
|
||||||
|
def _row_to_game(row: Optional[tuple]) -> Optional[Game]:
|
||||||
|
"""Вспомогательная функция для преобразования строки базы данных в объект Game."""
|
||||||
|
if row:
|
||||||
|
return Game(
|
||||||
|
id=row[0],
|
||||||
|
game_date=date.fromisoformat(row[1]),
|
||||||
|
player1_id=row[2],
|
||||||
|
player1_choice=row[3],
|
||||||
|
player2_id=row[4],
|
||||||
|
player2_choice=row[5],
|
||||||
|
winner_id=row[6],
|
||||||
|
is_finished=bool(row[7])
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def create_or_get_today_game(player1_id: int, player2_id: int) -> Optional[Game]:
|
||||||
|
"""
|
||||||
|
Находит или создает игру для указанной пары игроков на СЕГОДНЯШНЮЮ дату.
|
||||||
|
Возвращает объект Game или None в случае ошибки.
|
||||||
|
Гарантирует, что player1_id < player2_id для уникальности записи.
|
||||||
|
"""
|
||||||
|
today = date.today()
|
||||||
|
p1, p2 = sorted((player1_id, player2_id))
|
||||||
|
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute(
|
||||||
|
"""SELECT id, game_date, player1_id, player1_choice, player2_id, player2_choice, winner_id, is_finished
|
||||||
|
FROM games
|
||||||
|
WHERE game_date = ? AND player1_id = ? AND player2_id = ?""",
|
||||||
|
(today, p1, p2)
|
||||||
|
) as cursor:
|
||||||
|
game_row = await cursor.fetchone()
|
||||||
|
if game_row:
|
||||||
|
return _row_to_game(game_row)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await db.execute(
|
||||||
|
"""INSERT INTO games (game_date, player1_id, player2_id) VALUES (?, ?, ?)""",
|
||||||
|
(today, p1, p2)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
async with db.execute(
|
||||||
|
"""SELECT id, game_date, player1_id, player1_choice, player2_id, player2_choice, winner_id, is_finished
|
||||||
|
FROM games
|
||||||
|
WHERE game_date = ? AND player1_id = ? AND player2_id = ?""",
|
||||||
|
(today, p1, p2)
|
||||||
|
) as new_cursor:
|
||||||
|
new_game_row = await new_cursor.fetchone()
|
||||||
|
return _row_to_game(new_game_row)
|
||||||
|
except aiosqlite.IntegrityError:
|
||||||
|
async with db.execute(
|
||||||
|
"""SELECT id, game_date, player1_id, player1_choice, player2_id, player2_choice, winner_id, is_finished
|
||||||
|
FROM games
|
||||||
|
WHERE game_date = ? AND player1_id = ? AND player2_id = ?""",
|
||||||
|
(today, p1, p2)
|
||||||
|
) as cursor:
|
||||||
|
game_row = await cursor.fetchone()
|
||||||
|
return _row_to_game(game_row)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating/getting today's game: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def update_game_choice(game_id: int, player_id: int, choice: GameChoice) -> bool:
|
||||||
|
"""Обновляет выбор игрока в указанной игре."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
game = await get_game_by_id(game_id)
|
||||||
|
if not game:
|
||||||
|
return False
|
||||||
|
|
||||||
|
column_to_update = None
|
||||||
|
if game.player1_id == player_id:
|
||||||
|
column_to_update = "player1_choice"
|
||||||
|
elif game.player2_id == player_id:
|
||||||
|
column_to_update = "player2_choice"
|
||||||
|
|
||||||
|
if not column_to_update:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
await db.execute(
|
||||||
|
f"UPDATE games SET {column_to_update} = ? WHERE id = ?",
|
||||||
|
(choice, game_id)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating game choice: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def get_game_by_id(game_id: int) -> Optional[Game]:
|
||||||
|
"""Получает игру по её ID."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute(
|
||||||
|
"""SELECT id, game_date, player1_id, player1_choice, player2_id, player2_choice, winner_id, is_finished
|
||||||
|
FROM games
|
||||||
|
WHERE id = ?""",
|
||||||
|
(game_id,)
|
||||||
|
) as cursor:
|
||||||
|
game_row = await cursor.fetchone()
|
||||||
|
return _row_to_game(game_row)
|
||||||
|
|
||||||
|
|
||||||
|
async def finish_game(game_id: int, winner_id: Optional[int]) -> bool:
|
||||||
|
"""Отмечает игру как завершенную и указывает победителя (или None для ничьи)."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
try:
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE games SET winner_id = ?, is_finished = TRUE WHERE id = ?",
|
||||||
|
(winner_id, game_id)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error finishing game: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_game_on_date(player1_id: int, player2_id: int, game_date: date) -> Optional[Game]:
|
||||||
|
"""Получает игру для пары игроков на указанную дату."""
|
||||||
|
p1, p2 = sorted((player1_id, player2_id))
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute(
|
||||||
|
"""SELECT id, game_date, player1_id, player1_choice, player2_id, player2_choice, winner_id, is_finished
|
||||||
|
FROM games
|
||||||
|
WHERE game_date = ? AND player1_id = ? AND player2_id = ?""",
|
||||||
|
(game_date, p1, p2)
|
||||||
|
) as cursor:
|
||||||
|
game_row = await cursor.fetchone()
|
||||||
|
return _row_to_game(game_row)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Функции для работы со стриками ---
|
||||||
|
|
||||||
|
async def get_streak(user_id: int) -> Optional[Streak]:
|
||||||
|
"""Получает текущую и максимальную серию побед для пользователя."""
|
||||||
|
return await ensure_streak_record(user_id)
|
||||||
|
|
||||||
|
async def update_streak(user_id: int, win: bool):
|
||||||
|
"""Обновляет серию побед пользователя."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
current_streak_data = await get_streak(user_id)
|
||||||
|
if not current_streak_data:
|
||||||
|
print(f"Error: Streak record not found for user {user_id} during update.")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_streak = current_streak_data.current_streak
|
||||||
|
max_streak = current_streak_data.max_streak
|
||||||
|
|
||||||
|
if win:
|
||||||
|
current_streak += 1
|
||||||
|
if current_streak > max_streak:
|
||||||
|
max_streak = current_streak
|
||||||
|
else:
|
||||||
|
current_streak = 0
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE streaks SET current_streak = ?, max_streak = ? WHERE user_id = ?",
|
||||||
|
(current_streak, max_streak, user_id)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
# --- Функции для статистики ---
|
||||||
|
|
||||||
|
async def get_wins_count(user_id: int) -> int:
|
||||||
|
"""Возвращает общее количество побед пользователя."""
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute("SELECT COUNT(*) FROM games WHERE winner_id = ? AND is_finished = TRUE", (user_id,)) as cursor:
|
||||||
|
result = await cursor.fetchone()
|
||||||
|
return result[0] if result else 0
|
||||||
|
|
||||||
|
async def get_total_games_count(player1_id: int, player2_id: int) -> int:
|
||||||
|
"""Возвращает общее количество сыгранных (завершенных) игр между двумя пользователями."""
|
||||||
|
p1, p2 = sorted((player1_id, player2_id))
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute(
|
||||||
|
"SELECT COUNT(*) FROM games WHERE player1_id = ? AND player2_id = ? AND is_finished = TRUE",
|
||||||
|
(p1, p2)
|
||||||
|
) as cursor:
|
||||||
|
result = await cursor.fetchone()
|
||||||
|
return result[0] if result else 0
|
||||||
|
|
||||||
|
async def get_draw_count(player1_id: int, player2_id: int) -> int:
|
||||||
|
"""Возвращает общее количество ничьих между двумя пользователями."""
|
||||||
|
p1, p2 = sorted((player1_id, player2_id))
|
||||||
|
async with aiosqlite.connect(DATABASE_URL) as db:
|
||||||
|
async with db.execute(
|
||||||
|
"SELECT COUNT(*) FROM games WHERE player1_id = ? AND player2_id = ? AND is_finished = TRUE AND winner_id IS NULL",
|
||||||
|
(p1, p2)
|
||||||
|
) as cursor:
|
||||||
|
result = await cursor.fetchone()
|
||||||
|
return result[0] if result else 0
|
|
@ -0,0 +1,26 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date
|
||||||
|
from typing import Optional, Literal
|
||||||
|
|
||||||
|
GameChoice = Literal["rock", "scissors", "paper"]
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
id: Optional[int] = Field(default=None, description="Primary key")
|
||||||
|
telegram_id: int = Field(description="Telegram User ID")
|
||||||
|
username: Optional[str] = Field(default=None, description="Telegram Username")
|
||||||
|
|
||||||
|
class Game(BaseModel):
|
||||||
|
id: Optional[int] = Field(default=None, description="Primary key")
|
||||||
|
game_date: date = Field(description="Date of the game")
|
||||||
|
player1_id: int = Field(description="Foreign key to users table")
|
||||||
|
player1_choice: Optional[GameChoice] = Field(default=None)
|
||||||
|
player2_id: int = Field(description="Foreign key to users table")
|
||||||
|
player2_choice: Optional[GameChoice] = Field(default=None)
|
||||||
|
winner_id: Optional[int] = Field(default=None, description="Foreign key to users table, NULL if draw")
|
||||||
|
is_finished: bool = Field(default=False)
|
||||||
|
|
||||||
|
class Streak(BaseModel):
|
||||||
|
id: Optional[int] = Field(default=None, description="Primary key")
|
||||||
|
user_id: int = Field(description="Foreign key to users table")
|
||||||
|
current_streak: int = Field(default=0)
|
||||||
|
max_streak: int = Field(default=0)
|
|
@ -0,0 +1 @@
|
||||||
|
# Mark directories as Python packages
|
|
@ -0,0 +1,251 @@
|
||||||
|
import logging
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from typing import Optional, Literal
|
||||||
|
|
||||||
|
from aiogram import Router, types, F, Bot
|
||||||
|
from aiogram.filters import Command
|
||||||
|
|
||||||
|
from bot.database.models import User as DbUser, Game as DbGame, GameChoice
|
||||||
|
from bot.database.db import (
|
||||||
|
create_or_get_today_game,
|
||||||
|
update_game_choice,
|
||||||
|
get_user_by_id,
|
||||||
|
finish_game,
|
||||||
|
update_streak,
|
||||||
|
get_game_by_id,
|
||||||
|
get_game_on_date
|
||||||
|
)
|
||||||
|
from bot.keyboards.game_keyboard import get_game_choice_keyboard, GameChoiceCallback
|
||||||
|
from bot.utils.game_logic import determine_winner, CHOICE_NAMES_RU
|
||||||
|
from bot.utils.helpers import get_partner
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
async def check_and_resolve_yesterdays_game(user_id: int, partner_id: int, bot: Bot) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет наличие незавершенной игры за вчерашний день и завершает ее по правилам.
|
||||||
|
Возвращает True, если игра была найдена и обработана, иначе False.
|
||||||
|
"""
|
||||||
|
yesterday = date.today() - timedelta(days=1)
|
||||||
|
game = await get_game_on_date(user_id, partner_id, yesterday)
|
||||||
|
|
||||||
|
if not game or game.is_finished:
|
||||||
|
return False
|
||||||
|
|
||||||
|
logging.info(f"Found unfinished game from yesterday ({yesterday}) for users {user_id} and {partner_id}. Resolving...")
|
||||||
|
|
||||||
|
p1_id = game.player1_id
|
||||||
|
p2_id = game.player2_id
|
||||||
|
p1_choice = game.player1_choice
|
||||||
|
p2_choice = game.player2_choice
|
||||||
|
|
||||||
|
p1_user = await get_user_by_id(p1_id)
|
||||||
|
p2_user = await get_user_by_id(p2_id)
|
||||||
|
|
||||||
|
if not p1_user or not p2_user:
|
||||||
|
logging.error(f"Could not find user data for yesterday's game {game.id}")
|
||||||
|
await finish_game(game.id, None)
|
||||||
|
return True
|
||||||
|
|
||||||
|
p1_name = p1_user.username or f"Игрок {p1_user.telegram_id}"
|
||||||
|
p2_name = p2_user.username or f"Игрок {p2_user.telegram_id}"
|
||||||
|
|
||||||
|
winner_db_id: Optional[int] = None
|
||||||
|
result_message: str = ""
|
||||||
|
p1_win: bool = False
|
||||||
|
p2_win: bool = False
|
||||||
|
|
||||||
|
if p1_choice and not p2_choice:
|
||||||
|
winner_db_id = p1_id
|
||||||
|
p1_choice_ru = CHOICE_NAMES_RU.get(p1_choice, p1_choice)
|
||||||
|
result_message = (
|
||||||
|
f"⏳ Вчерашняя игра ({yesterday}):\n"
|
||||||
|
f"<b>{p1_name}</b> ({p1_choice_ru}) побеждает! 🎉\n"
|
||||||
|
f"<i>({p2_name} не сделал(а) свой ход)</i>"
|
||||||
|
)
|
||||||
|
p1_win = True
|
||||||
|
p2_win = False
|
||||||
|
logging.info(f"Yesterday's game {game.id}: Player 1 ({p1_id}) wins by default.")
|
||||||
|
|
||||||
|
elif not p1_choice and p2_choice:
|
||||||
|
winner_db_id = p2_id
|
||||||
|
p2_choice_ru = CHOICE_NAMES_RU.get(p2_choice, p2_choice)
|
||||||
|
result_message = (
|
||||||
|
f"⏳ Вчерашняя игра ({yesterday}):\n"
|
||||||
|
f"<b>{p2_name}</b> ({p2_choice_ru}) побеждает! 🎉\n"
|
||||||
|
f"<i>({p1_name} не сделал(а) свой ход)</i>"
|
||||||
|
)
|
||||||
|
p1_win = False
|
||||||
|
p2_win = True
|
||||||
|
logging.info(f"Yesterday's game {game.id}: Player 2 ({p2_id}) wins by default.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
winner_db_id = None
|
||||||
|
result_message = (
|
||||||
|
f"⏳ Вчерашняя игра ({yesterday}):\n"
|
||||||
|
f"Ничья! Игра аннулирована, так как не все сделали ход. 🤷♀️🤷♂️"
|
||||||
|
)
|
||||||
|
p1_win = False
|
||||||
|
p2_win = False
|
||||||
|
logging.info(f"Yesterday's game {game.id}: Draw (at least one player did not choose).")
|
||||||
|
|
||||||
|
await finish_game(game.id, winner_db_id)
|
||||||
|
|
||||||
|
await update_streak(p1_id, win=p1_win)
|
||||||
|
await update_streak(p2_id, win=p2_win)
|
||||||
|
|
||||||
|
full_result_message = result_message + "\n\nНачинаем игру на сегодня! /play"
|
||||||
|
try:
|
||||||
|
await bot.send_message(p1_user.telegram_id, full_result_message, parse_mode="HTML")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send yesterday result to user {p1_user.telegram_id}: {e}")
|
||||||
|
try:
|
||||||
|
await bot.send_message(p2_user.telegram_id, full_result_message, parse_mode="HTML")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send yesterday result to user {p2_user.telegram_id}: {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@router.message(Command("play"))
|
||||||
|
async def handle_play(message: types.Message, db_user: DbUser, bot: Bot):
|
||||||
|
"""Обработчик команды /play. Проверяет вчерашнюю игру и предлагает сделать ход."""
|
||||||
|
partner = await get_partner(db_user.id)
|
||||||
|
if not partner:
|
||||||
|
await message.answer("Для игры нужен партнер. Дождитесь, пока второй пользователь присоединится.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await check_and_resolve_yesterdays_game(db_user.id, partner.id, bot)
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Error checking/resolving yesterday's game for {db_user.id} and {partner.id}")
|
||||||
|
|
||||||
|
game = await create_or_get_today_game(db_user.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}")
|
||||||
|
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
|
||||||
|
|
||||||
|
if current_player_choice:
|
||||||
|
choice_name_ru = CHOICE_NAMES_RU.get(current_player_choice, current_player_choice)
|
||||||
|
await message.answer(f"Вы уже сделали свой ход сегодня ({choice_name_ru}). Ожидаем хода партнера! 😉")
|
||||||
|
return
|
||||||
|
|
||||||
|
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
|
||||||
|
):
|
||||||
|
"""Обработчик нажатия на кнопку выбора хода."""
|
||||||
|
choice: GameChoice = callback_data.choice
|
||||||
|
choice_name_ru = CHOICE_NAMES_RU.get(choice, choice)
|
||||||
|
|
||||||
|
partner = await get_partner(db_user.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)
|
||||||
|
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")
|
||||||
|
return
|
||||||
|
|
||||||
|
if game.is_finished:
|
||||||
|
await callback.answer("Игра уже завершена.", show_alert=True)
|
||||||
|
await callback.message.edit_text("Вы опоздали, игра на сегодня уже закончилась!" )
|
||||||
|
return
|
||||||
|
|
||||||
|
player_field = "player1_choice" if game.player1_id == db_user.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)
|
||||||
|
await callback.answer("Вы уже сделали свой ход.", show_alert=True)
|
||||||
|
await callback.message.edit_text(f"Вы уже выбрали: {current_choice_ru}\nОжидаем ход партнера... ✨")
|
||||||
|
return
|
||||||
|
|
||||||
|
updated = await update_game_choice(game.id, db_user.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}")
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.answer(f"Вы выбрали: {choice_name_ru}")
|
||||||
|
await callback.message.edit_text(f"Вы выбрали: {choice_name_ru}\nОжидаем ход партнера... ✨")
|
||||||
|
|
||||||
|
updated_game = await get_game_by_id(game.id)
|
||||||
|
if not updated_game:
|
||||||
|
logging.error(f"Failed to fetch updated game {game.id} after choice update.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if updated_game.player1_choice and updated_game.player2_choice:
|
||||||
|
winner_relation = determine_winner(updated_game.player1_choice, updated_game.player2_choice)
|
||||||
|
|
||||||
|
winner_db_id: Optional[int] = None
|
||||||
|
result_text = ""
|
||||||
|
|
||||||
|
p1_choice_ru = CHOICE_NAMES_RU[updated_game.player1_choice]
|
||||||
|
p2_choice_ru = CHOICE_NAMES_RU[updated_game.player2_choice]
|
||||||
|
p1_user = await get_user_by_id(updated_game.player1_id)
|
||||||
|
p2_user = await get_user_by_id(updated_game.player2_id)
|
||||||
|
|
||||||
|
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(partner.telegram_id, "Ошибка: не удалось получить данные игроков для завершения игры.")
|
||||||
|
return
|
||||||
|
|
||||||
|
p1_name = p1_user.username or f"Игрок {p1_user.telegram_id}"
|
||||||
|
p2_name = p2_user.username or f"Игрок {p2_user.telegram_id}"
|
||||||
|
|
||||||
|
if winner_relation == 1:
|
||||||
|
winner_db_id = updated_game.player1_id
|
||||||
|
result_text = f"<b>{p1_name}</b> ({p1_choice_ru}) побеждает <b>{p2_name}</b> ({p2_choice_ru})!\n{p1_name} сегодня любит больше! ❤️"
|
||||||
|
elif winner_relation == 2:
|
||||||
|
winner_db_id = updated_game.player2_id
|
||||||
|
result_text = f"<b>{p2_name}</b> ({p2_choice_ru}) побеждает <b>{p1_name}</b> ({p1_choice_ru})!\n{p2_name} сегодня любит больше! ❤️"
|
||||||
|
else:
|
||||||
|
result_text = f"Ничья! Оба выбрали {p1_choice_ru}.\nСегодня вы любите друг друга одинаково сильно! 🥰"
|
||||||
|
|
||||||
|
await finish_game(updated_game.id, winner_db_id)
|
||||||
|
|
||||||
|
await update_streak(updated_game.player1_id, win=(winner_relation == 1))
|
||||||
|
await update_streak(updated_game.player2_id, win=(winner_relation == 2))
|
||||||
|
|
||||||
|
final_message = f"Игра за {updated_game.game_date.strftime('%d.%m.%Y')} завершена! 🎉\n\n{result_text}\n\nПосмотреть статистику: /stats"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.send_message(p1_user.telegram_id, final_message, parse_mode="HTML")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send result to user {p1_user.telegram_id}: {e}")
|
||||||
|
try:
|
||||||
|
await bot.send_message(p2_user.telegram_id, final_message, parse_mode="HTML")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send result to user {p2_user.telegram_id}: {e}")
|
||||||
|
|
||||||
|
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}"
|
||||||
|
try:
|
||||||
|
await bot.send_message(
|
||||||
|
partner_user.telegram_id,
|
||||||
|
f"Ваш партнер, {current_user_name}, сделал свой ход! 😉\nТеперь ваша очередь решить, кто любит больше! Используйте команду /play, чтобы сделать ход."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to notify partner {partner_user.telegram_id}: {e}")
|
||||||
|
else:
|
||||||
|
logging.error(f"Could not find partner user data for notification (partner_id: {partner.id})")
|
|
@ -0,0 +1,18 @@
|
||||||
|
from aiogram import Router, types
|
||||||
|
from aiogram.filters import CommandStart
|
||||||
|
|
||||||
|
from bot.database.models import User as DbUser
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
@router.message(CommandStart())
|
||||||
|
async def handle_start(message: types.Message, db_user: DbUser):
|
||||||
|
"""Обработчик команды /start для аутентифицированных пользователей."""
|
||||||
|
username = db_user.username or f"Пользователь {db_user.telegram_id}"
|
||||||
|
await message.answer(
|
||||||
|
f"Привет, {username}! ✨\n\n"
|
||||||
|
f"Это бот 'Кто больше любит'. Каждый день вы со своей парой можете сыграть в Камень-Ножницы-Бумага, чтобы определить, кто любит больше! 😉\n\n"
|
||||||
|
f"Чтобы сделать ход, используй кнопки ниже (они появятся, когда придет время игры).\n"
|
||||||
|
f"Для просмотра статистики используй команду /stats.\n\n"
|
||||||
|
f"Удачи! ❤️"
|
||||||
|
)
|
|
@ -0,0 +1,58 @@
|
||||||
|
import logging
|
||||||
|
from aiogram import Router, types
|
||||||
|
from aiogram.filters import Command
|
||||||
|
|
||||||
|
from bot.database.models import User as DbUser
|
||||||
|
from bot.database.db import (
|
||||||
|
get_wins_count,
|
||||||
|
get_total_games_count,
|
||||||
|
get_draw_count,
|
||||||
|
get_streak,
|
||||||
|
get_user_by_id
|
||||||
|
)
|
||||||
|
from bot.utils.helpers import get_partner
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
@router.message(Command("stats"))
|
||||||
|
async def handle_stats(message: types.Message, db_user: DbUser):
|
||||||
|
"""Обработчик команды /stats. Выводит статистику игры."""
|
||||||
|
partner = await get_partner(db_user.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)
|
||||||
|
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)
|
||||||
|
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}")
|
||||||
|
await message.answer("Произошла ошибка при получении статистики. Попробуйте позже.")
|
||||||
|
return
|
||||||
|
|
||||||
|
user_name = db_user.username or f"Игрок {db_user.telegram_id}"
|
||||||
|
partner_name = partner.username or f"Игрок {partner.telegram_id}"
|
||||||
|
|
||||||
|
stats_text = (
|
||||||
|
f"📊 <b>Статистика \"Кто больше любит?\"</b> ❤️\n\n"
|
||||||
|
f"<b>Всего сыграно игр:</b> {total_games}\n"
|
||||||
|
f"<i>(Завершенных игр между вами)</i>\n\n"
|
||||||
|
f"🏆 <b>Победы:</b>\n"
|
||||||
|
f" - {user_name}: {user_wins}\n"
|
||||||
|
f" - {partner_name}: {partner_wins}\n"
|
||||||
|
f" - Ничьи: {draws}\n\n"
|
||||||
|
f"🔥 <b>Серии побед (стрики):</b>\n"
|
||||||
|
f" - {user_name}:\n"
|
||||||
|
f" - Текущий: {user_streak.current_streak if user_streak else 0}\n"
|
||||||
|
f" - Макс.: {user_streak.max_streak if user_streak else 0}\n"
|
||||||
|
f" - {partner_name}:\n"
|
||||||
|
f" - Текущий: {partner_streak.current_streak if partner_streak else 0}\n"
|
||||||
|
f" - Макс.: {partner_streak.max_streak if partner_streak else 0}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await message.answer(stats_text, parse_mode="HTML")
|
|
@ -0,0 +1 @@
|
||||||
|
# Mark directories as Python packages
|
|
@ -0,0 +1,27 @@
|
||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
from aiogram.filters.callback_data import CallbackData
|
||||||
|
|
||||||
|
class GameChoiceCallback(CallbackData, prefix="game"):
|
||||||
|
choice: str
|
||||||
|
|
||||||
|
|
||||||
|
def get_game_choice_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
"""Возвращает инлайн-клавиатуру с кнопками выбора хода."""
|
||||||
|
buttons = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🗿 Камень",
|
||||||
|
callback_data=GameChoiceCallback(choice="rock").pack()
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="✂️ Ножницы",
|
||||||
|
callback_data=GameChoiceCallback(choice="scissors").pack()
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="📄 Бумага",
|
||||||
|
callback_data=GameChoiceCallback(choice="paper").pack()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
return keyboard
|
|
@ -0,0 +1,40 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
|
||||||
|
from bot.handlers import start, game, stats
|
||||||
|
from bot.middlewares.auth_middleware import AuthMiddleware, load_initial_users
|
||||||
|
from bot.database.db import init_db
|
||||||
|
from bot.config import settings
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
await init_db()
|
||||||
|
logging.info("Database initialized.")
|
||||||
|
|
||||||
|
await load_initial_users()
|
||||||
|
|
||||||
|
bot = Bot(token=settings.bot_token.get_secret_value(), parse_mode=ParseMode.HTML)
|
||||||
|
dp = Dispatcher()
|
||||||
|
|
||||||
|
dp.update.outer_middleware(AuthMiddleware())
|
||||||
|
|
||||||
|
dp.include_router(start.router)
|
||||||
|
dp.include_router(game.router)
|
||||||
|
dp.include_router(stats.router)
|
||||||
|
# TODO: Раскомментировать и добавить роутеры, когда они будут готовы
|
||||||
|
|
||||||
|
logging.info("Starting bot...")
|
||||||
|
await bot.delete_webhook(drop_pending_updates=True)
|
||||||
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
logging.info("Bot stopped.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Bot encountered an error:")
|
|
@ -0,0 +1 @@
|
||||||
|
# Mark directories as Python packages
|
|
@ -0,0 +1,77 @@
|
||||||
|
import logging
|
||||||
|
from typing import Callable, Dict, Any, Awaitable, Set
|
||||||
|
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import Message, TelegramObject, User as AiogramUser
|
||||||
|
|
||||||
|
from bot.config import settings
|
||||||
|
from bot.database.db import get_all_users, add_user, get_user_by_telegram_id
|
||||||
|
from bot.database.models import User as DbUser
|
||||||
|
|
||||||
|
authenticated_user_ids: Set[int] = set()
|
||||||
|
|
||||||
|
initial_users_loaded = False
|
||||||
|
|
||||||
|
async def load_initial_users():
|
||||||
|
"""Loads existing user IDs from the DB into memory on startup."""
|
||||||
|
global authenticated_user_ids, initial_users_loaded
|
||||||
|
if not initial_users_loaded:
|
||||||
|
logging.info("Loading initial users from database...")
|
||||||
|
existing_users = await get_all_users()
|
||||||
|
for user in existing_users:
|
||||||
|
authenticated_user_ids.add(user.telegram_id)
|
||||||
|
if len(authenticated_user_ids) >= 2:
|
||||||
|
break
|
||||||
|
initial_users_loaded = True
|
||||||
|
logging.info(f"Loaded {len(authenticated_user_ids)} users: {authenticated_user_ids}")
|
||||||
|
|
||||||
|
class AuthMiddleware(BaseMiddleware):
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
|
||||||
|
if not initial_users_loaded:
|
||||||
|
await load_initial_users()
|
||||||
|
|
||||||
|
if not isinstance(event, Message):
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
aiogram_user: AiogramUser = data.get('event_from_user')
|
||||||
|
|
||||||
|
if not aiogram_user:
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
telegram_id = aiogram_user.id
|
||||||
|
|
||||||
|
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
|
||||||
|
return await handler(event, data)
|
||||||
|
else:
|
||||||
|
logging.warning(f"User {telegram_id} is in authenticated_user_ids but not found in DB.")
|
||||||
|
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
|
||||||
|
logging.info(f"User {telegram_id} ({aiogram_user.username}) authenticated successfully. Total users: {len(authenticated_user_ids)}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logging.error(f"Failed to add user {telegram_id} to DB after password check.")
|
||||||
|
await event.answer("Произошла ошибка при регистрации. Попробуйте позже.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logging.info(f"Authentication attempt blocked for user {telegram_id}. Limit of 2 users reached.")
|
||||||
|
await event.answer("Извините, бот уже используется двумя пользователями.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if event.text != settings.secret_password.get_secret_value():
|
||||||
|
await event.answer("Для начала работы введите секретный пароль.")
|
||||||
|
return
|
|
@ -0,0 +1 @@
|
||||||
|
# Mark directories as Python packages
|
|
@ -0,0 +1,38 @@
|
||||||
|
from typing import Optional, Literal
|
||||||
|
from bot.database.models import GameChoice, User
|
||||||
|
|
||||||
|
WIN_CONDITIONS: dict[GameChoice, GameChoice] = {
|
||||||
|
"rock": "scissors",
|
||||||
|
"scissors": "paper",
|
||||||
|
"paper": "rock",
|
||||||
|
}
|
||||||
|
|
||||||
|
CHOICE_NAMES_RU: dict[GameChoice, str] = {
|
||||||
|
"rock": "Камень 🗿",
|
||||||
|
"scissors": "Ножницы ✂️",
|
||||||
|
"paper": "Бумага 📄",
|
||||||
|
}
|
||||||
|
|
||||||
|
def determine_winner(choice1: Optional[GameChoice], choice2: Optional[GameChoice]) -> Optional[Literal[1, 2]]:
|
||||||
|
"""
|
||||||
|
Определяет победителя игры Камень-Ножницы-Бумага.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
choice1: Выбор первого игрока.
|
||||||
|
choice2: Выбор второго игрока.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
1, если победил игрок 1.
|
||||||
|
2, если победил игрок 2.
|
||||||
|
None, если ничья или кто-то не сделал выбор.
|
||||||
|
"""
|
||||||
|
if not choice1 or not choice2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if choice1 == choice2:
|
||||||
|
return None # Ничья
|
||||||
|
|
||||||
|
if WIN_CONDITIONS.get(choice1) == choice2:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 2
|
|
@ -0,0 +1,14 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from bot.database.db import get_all_users
|
||||||
|
from bot.database.models import User as DbUser
|
||||||
|
|
||||||
|
async def get_partner(current_user_id: int) -> Optional[DbUser]:
|
||||||
|
"""Находит второго зарегистрированного пользователя (партнера)."""
|
||||||
|
users = await get_all_users()
|
||||||
|
if len(users) != 2:
|
||||||
|
return None
|
||||||
|
for user in users:
|
||||||
|
if user.id is not None and user.id != current_user_id:
|
||||||
|
return user
|
||||||
|
return None
|
|
@ -0,0 +1,14 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
love_bot:
|
||||||
|
build: .
|
||||||
|
container_name: love_bot_app
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- sqlite_data:/app/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sqlite_data:
|
|
@ -0,0 +1,5 @@
|
||||||
|
aiogram>=3.4.1,<4.0.0
|
||||||
|
pydantic>=2.7.1,<3.0.0
|
||||||
|
python-dotenv>=1.0.1,<2.0.0
|
||||||
|
aiosqlite>=0.19.0,<1.0.0
|
||||||
|
pydantic-settings>=2.2.1,<3.0.0
|
Loading…
Reference in New Issue