From e56a11461b01b4fa90de2872b673dbaab4ea79f8 Mon Sep 17 00:00:00 2001 From: wpng1337 Date: Sun, 22 Feb 2026 20:33:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F:=20=D1=87=D1=82=D0=BE=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB=D0=BE=D1=81=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена и доработана миниигра /penis Логика рандома +/- 0.1..7.0, ограничение минимума до 0.1, cooldown 24ч. Файлы/места: main.py (line 420), main.py (line 579). Добавлена команда /top_penis Динамический топ по текущим значениям см. Формат с медалями и лимитом топа. Файлы/места: main.py (line 505), main.py (line 590). Хранение данных /penis и /top_penis переведено с RAM на SQLite Убрана in-memory модель, добавлена БД penis_stats.sqlite3. Добавлена инициализация таблицы penis_stats. Файлы/места: main.py (line 27), main.py (line 47), main.py (line 761). Логика записи в БД исправлена под твое требование Для нового пользователя: INSERT. Для существующего: UPDATE (без новых строк). Файл/место: main.py (line 468). В топ добавлено нормальное имя пользователя Берётся @username, иначе full_name, иначе user_. Файл/место: main.py (line 496). Команды добавлены в help и Telegram command menu В /help добавлены /penis и /top_penis. В set_my_commands есть обе команды. Файлы/места: main.py (line 152), main.py (line 778), main.py (line 783). Улучшено отображение команд через / Команды выставляются для scope: default all private chats all group chats Файл/место: main.py (line 790), main.py (line 796). Добавлены подробные комментарии в код По основным блокам main.py и talk_handler.py. Файлы: main.py, talk_handler.py. Проверки, которые прогонялись Компиляция: talk_handler.py. Локальные тесты: insert/update/cooldown /penis; сортировка /top_penis; запись полей в SQLite; smoke по генерации мемов и тайм-функциям. --- main.py | 182 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 128 insertions(+), 54 deletions(-) diff --git a/main.py b/main.py index d591d18..e105864 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 @@ -23,13 +32,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. @@ -42,6 +51,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 = [] @@ -403,35 +431,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 ( @@ -453,23 +509,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) @@ -698,28 +763,37 @@ 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: - await bot.set_my_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="Побазарить"), - ] + 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(), ) + for scope in scopes: + await bot.set_my_commands(commands, scope=scope) except Exception: # noqa: WPS440 logger.warning("Не удалось выставить команды, но бот продолжит работу.")