diff --git a/main.py b/main.py index 739060d..310da01 100644 --- a/main.py +++ b/main.py @@ -8,11 +8,20 @@ from io import BytesIO from pathlib import Path from textwrap import wrap import random +import sqlite3 from aiogram import Bot, Dispatcher, F, types from aiogram.enums import ParseMode from aiogram.filters import Command, CommandStart -from aiogram.types import BufferedInputFile, Message, BotCommand, FSInputFile +from aiogram.types import ( + BufferedInputFile, + Message, + BotCommand, + FSInputFile, + BotCommandScopeDefault, + BotCommandScopeAllPrivateChats, + BotCommandScopeAllGroupChats, +) from aiogram.client.default import DefaultBotProperties from PIL import Image, ImageDraw, ImageFont from datetime import datetime, time, timedelta @@ -24,13 +33,13 @@ BASE_DIR = Path(__file__).parent FONT_PATH = BASE_DIR / "impact.ttf" DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow") -# In-memory состояние миниигры /penis. -# Важно: после перезапуска процесса всё обнуляется. -PENIS_STATE: dict[int, dict[str, object]] = {} +# Данные миниигры /penis храним в SQLite, чтобы не держать их в RAM. +PENIS_DB_PATH = BASE_DIR / "penis_stats.sqlite3" PENIS_START_LENGTH = 10.0 PENIS_MIN_DELTA = 0.1 PENIS_MAX_DELTA = 7.0 PENIS_COOLDOWN = timedelta(hours=24) +PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds()) PENIS_TOP_LIMIT = 10 # Конфигурация текстовых макетов /maket. @@ -43,6 +52,25 @@ TEMPLATES = { }, } + +def init_penis_db() -> None: + # Инициализация таблицы для миниигры /penis. + with sqlite3.connect(PENIS_DB_PATH) as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS penis_stats ( + user_id INTEGER PRIMARY KEY, + display_name TEXT NOT NULL, + length REAL NOT NULL, + last_used_ts INTEGER + ) + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_penis_stats_length ON penis_stats(length DESC)" + ) + conn.commit() + def templates_list_text() -> str: # Собираем человекочитаемый список доступных макетов. parts = [] @@ -404,35 +432,63 @@ def play_penis(user_id: int, user_name: str | None = None, now: datetime | None now = datetime.now(ZoneInfo(DEFAULT_TZ)) elif now.tzinfo is None: now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ)) + now_ts = int(now.timestamp()) + display_name = user_name.strip() if user_name and user_name.strip() else f"user_{user_id}" - state = PENIS_STATE.get(user_id) - if state is None: - state = {"length": PENIS_START_LENGTH, "last_used_at": None} - PENIS_STATE[user_id] = state + try: + with sqlite3.connect(PENIS_DB_PATH) as conn: + conn.row_factory = sqlite3.Row + row = conn.execute( + "SELECT length, last_used_ts, display_name FROM penis_stats WHERE user_id = ?", + (user_id,), + ).fetchone() - # Обновляем отображаемое имя для топа. - if user_name: - state["display_name"] = user_name + if row is None: + current_length = PENIS_START_LENGTH + last_used_ts = None + stored_name = None + else: + current_length = float(row["length"]) + last_used_ts = row["last_used_ts"] + stored_name = row["display_name"] + if not user_name and isinstance(stored_name, str) and stored_name.strip(): + display_name = stored_name.strip() - last_used_at = state.get("last_used_at") - if isinstance(last_used_at, datetime): - next_time = last_used_at + PENIS_COOLDOWN - if now < next_time: - # Если рано, считаем оставшееся время до следующей попытки. - left = next_time - now - total_seconds = int(left.total_seconds()) - hours = total_seconds // 3600 - minutes = (total_seconds % 3600) // 60 - return False, f"Сегодня уже кидал. Попробуй через {hours} ч {minutes} мин." + if last_used_ts is not None: + next_ts = int(last_used_ts) + PENIS_COOLDOWN_SECONDS + if now_ts < next_ts: + # Даже при кулдауне обновляем имя, чтобы топ показывал актуальный ник. + if row is not None and row["display_name"] != display_name: + conn.execute( + "UPDATE penis_stats SET display_name = ? WHERE user_id = ?", + (display_name, user_id), + ) + left_seconds = next_ts - now_ts + hours = left_seconds // 3600 + minutes = (left_seconds % 3600) // 60 + conn.commit() + return False, f"Сегодня уже кидал. Попробуй через {hours} ч {minutes} мин." - current_length = float(state.get("length", PENIS_START_LENGTH)) - delta = round(random.uniform(PENIS_MIN_DELTA, PENIS_MAX_DELTA), 1) - sign = random.choice([-1, 1]) - # Не даём длине уйти в отрицательные значения. - new_length = round(max(0.1, current_length + sign * delta), 1) + delta = round(random.uniform(PENIS_MIN_DELTA, PENIS_MAX_DELTA), 1) + sign = random.choice([-1, 1]) + # Не даём длине уйти в отрицательные значения. + new_length = round(max(0.1, current_length + sign * delta), 1) - state["length"] = new_length - state["last_used_at"] = now + # Для нового пользователя создаём строку, для существующего — обновляем. + if row is None: + conn.execute( + "INSERT INTO penis_stats(user_id, display_name, length, last_used_ts) VALUES(?, ?, ?, ?)", + (user_id, display_name, new_length, now_ts), + ) + else: + conn.execute( + "UPDATE penis_stats SET display_name = ?, length = ?, last_used_ts = ? WHERE user_id = ?", + (display_name, new_length, now_ts, user_id), + ) + conn.commit() + except sqlite3.Error: + logger.exception("Failed to process /penis") + return False, "Ошибка БД. Попробуй позже." sign_text = "+" if sign > 0 else "-" return ( @@ -454,23 +510,32 @@ def format_penis_user_name(user: types.User) -> str: def build_penis_top(limit: int = PENIS_TOP_LIMIT) -> str: # Формируем динамический топ по текущим значениям /penis. - rows: list[tuple[float, str]] = [] - for user_id, state in PENIS_STATE.items(): - length = state.get("length") - if not isinstance(length, (int, float)): - continue - display_name = state.get("display_name") - if not isinstance(display_name, str) or not display_name.strip(): - display_name = f"user_{user_id}" - rows.append((float(length), display_name.strip())) + try: + with sqlite3.connect(PENIS_DB_PATH) as conn: + conn.row_factory = sqlite3.Row + rows = conn.execute( + """ + SELECT user_id, display_name, length + FROM penis_stats + ORDER BY length DESC, user_id ASC + LIMIT ? + """, + (limit,), + ).fetchall() + except sqlite3.Error: + logger.exception("Failed to build /top_penis") + return "Не удалось загрузить топ. Ошибка БД." if not rows: return "🏆 Топ пуст. Сначала пусть кто-то крутнет /penis." - rows.sort(key=lambda item: item[0], reverse=True) medals = {1: "🥇", 2: "🥈", 3: "🥉"} lines = ["🏆 Топ по длине:"] - for index, (length, name) in enumerate(rows[:limit], start=1): + for index, row in enumerate(rows, start=1): + user_id = int(row["user_id"]) + length = float(row["length"]) + name = row["display_name"] if isinstance(row["display_name"], str) else f"user_{user_id}" + name = name.strip() if name.strip() else f"user_{user_id}" prefix = medals.get(index, f"{index}.") lines.append(f"{prefix} {name} — {length:.1f} см") return "\n".join(lines) @@ -713,12 +778,16 @@ def main(): if not token: raise RuntimeError("BOT_TOKEN env var is required") + # Готовим SQLite-таблицу для /penis и /top_penis. + init_penis_db() + bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) dp = Dispatcher() async def on_startup(bot: Bot): # Выставляем меню команд в интерфейсе Telegram. try: +<<<<<<< HEAD await bot.set_my_commands( [ BotCommand(command="start", description="Приветствие"), @@ -736,7 +805,30 @@ def main(): BotCommand(command="svodka", description="СВО: итоги"), ] +======= + commands = [ + BotCommand(command="start", description="Приветствие"), + BotCommand(command="help", description="Подсказки по командам"), + BotCommand(command="maket", description="Вписать текст в макет 1"), + BotCommand(command="gen_mem", description="Мем из фото (ответом)"), + BotCommand(command="break", description="Сколько минут до перекура"), + BotCommand(command="size", description="хуй"), + BotCommand(command="penis", description="миниигра 1 раз в 24ч"), + BotCommand(command="top_penis", description="топ по длине"), + BotCommand(command="prices", description="цены x-server"), + BotCommand(command="fetch", description="цены (новый источник)"), + BotCommand(command="zvetok", description="Цветянский бля"), + BotCommand(command="talk", description="Побазарить"), + ] + # Явно задаём команды для default/private/group, чтобы меню через "/" появлялось стабильно. + scopes = ( + BotCommandScopeDefault(), + BotCommandScopeAllPrivateChats(), + BotCommandScopeAllGroupChats(), +>>>>>>> Zhirik1337-wpng ) + for scope in scopes: + await bot.set_my_commands(commands, scope=scope) except Exception: # noqa: WPS440 logger.warning("Не удалось выставить команды, но бот продолжит работу.")