Merge branch 'Zhirik1337-wpng'

This commit is contained in:
q 2026-02-22 20:53:46 +03:00
commit 957d46b735

170
main.py
View file

@ -8,11 +8,20 @@ from io import BytesIO
from pathlib import Path from pathlib import Path
from textwrap import wrap from textwrap import wrap
import random import random
import sqlite3
from aiogram import Bot, Dispatcher, F, types from aiogram import Bot, Dispatcher, F, types
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.filters import Command, CommandStart 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 aiogram.client.default import DefaultBotProperties
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
@ -24,13 +33,13 @@ BASE_DIR = Path(__file__).parent
FONT_PATH = BASE_DIR / "impact.ttf" FONT_PATH = BASE_DIR / "impact.ttf"
DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow") DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow")
# In-memory состояние миниигры /penis. # Данные миниигры /penis храним в SQLite, чтобы не держать их в RAM.
# Важно: после перезапуска процесса всё обнуляется. PENIS_DB_PATH = BASE_DIR / "penis_stats.sqlite3"
PENIS_STATE: dict[int, dict[str, object]] = {}
PENIS_START_LENGTH = 10.0 PENIS_START_LENGTH = 10.0
PENIS_MIN_DELTA = 0.1 PENIS_MIN_DELTA = 0.1
PENIS_MAX_DELTA = 7.0 PENIS_MAX_DELTA = 7.0
PENIS_COOLDOWN = timedelta(hours=24) PENIS_COOLDOWN = timedelta(hours=24)
PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds())
PENIS_TOP_LIMIT = 10 PENIS_TOP_LIMIT = 10
# Конфигурация текстовых макетов /maket. # Конфигурация текстовых макетов /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: def templates_list_text() -> str:
# Собираем человекочитаемый список доступных макетов. # Собираем человекочитаемый список доступных макетов.
parts = [] 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)) now = datetime.now(ZoneInfo(DEFAULT_TZ))
elif now.tzinfo is None: elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ)) 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) try:
if state is None: with sqlite3.connect(PENIS_DB_PATH) as conn:
state = {"length": PENIS_START_LENGTH, "last_used_at": None} conn.row_factory = sqlite3.Row
PENIS_STATE[user_id] = state row = conn.execute(
"SELECT length, last_used_ts, display_name FROM penis_stats WHERE user_id = ?",
(user_id,),
).fetchone()
# Обновляем отображаемое имя для топа. if row is None:
if user_name: current_length = PENIS_START_LENGTH
state["display_name"] = user_name 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 last_used_ts is not None:
if isinstance(last_used_at, datetime): next_ts = int(last_used_ts) + PENIS_COOLDOWN_SECONDS
next_time = last_used_at + PENIS_COOLDOWN if now_ts < next_ts:
if now < next_time: # Даже при кулдауне обновляем имя, чтобы топ показывал актуальный ник.
# Если рано, считаем оставшееся время до следующей попытки. if row is not None and row["display_name"] != display_name:
left = next_time - now conn.execute(
total_seconds = int(left.total_seconds()) "UPDATE penis_stats SET display_name = ? WHERE user_id = ?",
hours = total_seconds // 3600 (display_name, user_id),
minutes = (total_seconds % 3600) // 60 )
return False, f"Сегодня уже кидал. Попробуй через {hours} ч {minutes} мин." 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)
delta = round(random.uniform(PENIS_MIN_DELTA, PENIS_MAX_DELTA), 1) sign = random.choice([-1, 1])
sign = random.choice([-1, 1]) # Не даём длине уйти в отрицательные значения.
# Не даём длине уйти в отрицательные значения. new_length = round(max(0.1, current_length + sign * delta), 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 "-" sign_text = "+" if sign > 0 else "-"
return ( return (
@ -454,23 +510,32 @@ def format_penis_user_name(user: types.User) -> str:
def build_penis_top(limit: int = PENIS_TOP_LIMIT) -> str: def build_penis_top(limit: int = PENIS_TOP_LIMIT) -> str:
# Формируем динамический топ по текущим значениям /penis. # Формируем динамический топ по текущим значениям /penis.
rows: list[tuple[float, str]] = [] try:
for user_id, state in PENIS_STATE.items(): with sqlite3.connect(PENIS_DB_PATH) as conn:
length = state.get("length") conn.row_factory = sqlite3.Row
if not isinstance(length, (int, float)): rows = conn.execute(
continue """
display_name = state.get("display_name") SELECT user_id, display_name, length
if not isinstance(display_name, str) or not display_name.strip(): FROM penis_stats
display_name = f"user_{user_id}" ORDER BY length DESC, user_id ASC
rows.append((float(length), display_name.strip())) LIMIT ?
""",
(limit,),
).fetchall()
except sqlite3.Error:
logger.exception("Failed to build /top_penis")
return "Не удалось загрузить топ. Ошибка БД."
if not rows: if not rows:
return "🏆 Топ пуст. Сначала пусть кто-то крутнет /penis." return "🏆 Топ пуст. Сначала пусть кто-то крутнет /penis."
rows.sort(key=lambda item: item[0], reverse=True)
medals = {1: "🥇", 2: "🥈", 3: "🥉"} medals = {1: "🥇", 2: "🥈", 3: "🥉"}
lines = ["🏆 Топ по длине:"] 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}.") prefix = medals.get(index, f"{index}.")
lines.append(f"{prefix} {name}{length:.1f} см") lines.append(f"{prefix} {name}{length:.1f} см")
return "\n".join(lines) return "\n".join(lines)
@ -713,12 +778,16 @@ def main():
if not token: if not token:
raise RuntimeError("BOT_TOKEN env var is required") 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)) bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher() dp = Dispatcher()
async def on_startup(bot: Bot): async def on_startup(bot: Bot):
# Выставляем меню команд в интерфейсе Telegram. # Выставляем меню команд в интерфейсе Telegram.
try: try:
<<<<<<< HEAD
await bot.set_my_commands( await bot.set_my_commands(
[ [
BotCommand(command="start", description="Приветствие"), BotCommand(command="start", description="Приветствие"),
@ -736,7 +805,30 @@ def main():
BotCommand(command="svodka", description="СВО: итоги"), 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 except Exception: # noqa: WPS440
logger.warning("Не удалось выставить команды, но бот продолжит работу.") logger.warning("Не удалось выставить команды, но бот продолжит работу.")