Compare commits

...

9 commits

Author SHA1 Message Date
q
b9c9ddb4bf fix docker 2026-02-22 21:05:22 +03:00
q
0d0967001a fix old staff 2026-02-22 21:02:36 +03:00
q
1c27e84dc1 Fix ZAVISIMOSTIgit add .git add . 2026-02-22 20:57:12 +03:00
q
62aa560886 Fix docker for db 2026-02-22 20:56:15 +03:00
q
957d46b735 Merge branch 'Zhirik1337-wpng' 2026-02-22 20:53:46 +03:00
q
8ea9c50564 Merge pull request 'СВО ЕБАТЬ' (#7) from jeffrey228/bot_tg:main into main
Reviewed-on: zovos/bot_tg#7
2026-02-22 17:47:48 +00:00
q
434ae86e55 Merge branch 'main' into wpng 2026-02-22 17:47:02 +00:00
itexpert228
caebebf3cf
СВО ЕБАТЬ 2026-02-22 20:40:51 +03:00
e56a11461b Изменения: что изменилось
Добавлена и доработана миниигра /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_<id>.
Файл/место: 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 по генерации мемов и тайм-функциям.
2026-02-22 20:33:27 +03:00
5 changed files with 206 additions and 56 deletions

View file

@ -3,7 +3,7 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py impact.ttf ./
COPY main.py impact.ttf zparser.py ./
COPY maket ./maket
COPY AI ./AI
CMD ["python", "main.py"]

View file

@ -3,6 +3,8 @@ version: "3.9"
services:
bot:
build: .
volumes:
- ./db:/db
env_file:
- .env
restart: unless-stopped

164
main.py
View file

@ -8,28 +8,38 @@ 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
from zoneinfo import ZoneInfo
from zparser import get_military_data
# Базовые пути/настройки проекта.
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 = "/db/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 +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 = []
@ -403,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
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)
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 +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)
@ -692,20 +758,36 @@ async def handle_zvetok(message: Message):
await message.answer(result)
async def handle_svodka(message: Message):
channel = "creamy_caprice"
data = get_military_data(channel, depth=5)
if data["status"] == "success":
response = (
f"в период с {data['dates']} хохлы в среднем проебывали по {data['pace']} км² в сутки, "
f"а за день ВС РФ лишала чубатых в среднем по {data['total']} км²"
)
await message.answer(response)
else:
await message.answer(f"Хохлы ответят за это...: {data['message']}")
def main():
# Точка входа: создаём бота, регистрируем команды и запускаем polling.
token = os.getenv("BOT_TOKEN")
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(
[
commands = [
BotCommand(command="start", description="Приветствие"),
BotCommand(command="help", description="Подсказки по командам"),
BotCommand(command="maket", description="Вписать текст в макет 1"),
@ -718,8 +800,16 @@ def main():
BotCommand(command="fetch", description="цены (новый источник)"),
BotCommand(command="zvetok", description="Цветянский бля"),
BotCommand(command="talk", description="Побазарить"),
BotCommand(command="svodka", 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("Не удалось выставить команды, но бот продолжит работу.")
@ -737,7 +827,7 @@ def main():
dp.message.register(handle_fetch, Command("fetch"))
dp.message.register(handle_zvetok, Command("zvetok"))
dp.message.register(handle_talk, Command("talk"))
dp.message.register(handle_svodka, Command("svodka"))
async def handle_gen_mem_cmd(message: Message, bot: Bot):

View file

@ -1,3 +1,6 @@
aiogram>=3.4.1
pillow>=10.3.0
requests>=2.31.0
beautifulsoup4
lxml
pysqlite3-binary>=0.5.3

55
zparser.py Normal file
View file

@ -0,0 +1,55 @@
import requests
from bs4 import BeautifulSoup
import re
import time
def get_military_data(channel_username, depth=5):
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'}
last_id = None
for i in range(depth):
url = f"https://t.me/s/{channel_username}"
if last_id:
url += f"?before={last_id}"
try:
response = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
messages = soup.find_all('div', class_='tgme_widget_message')
if not messages:
break
first_msg_data = messages[0].get('data-post')
if first_msg_data:
last_id = first_msg_data.split('/')[-1]
for msg in reversed(messages):
text_elem = msg.find('div', class_='tgme_widget_message_text')
if not text_elem:
continue
text = " ".join(text_elem.get_text(separator=' ').split())
text_lower = text.lower()
if "темп" in text_lower and "продвижения" in text_lower:
# Улучшенная регулярка для дат (ловит и "с 14 по 17", и "с 14 февраля по 17 февраля")
dates_match = re.search(r"с (.*?)(?=\.|\s+темп)", text_lower)
# Твои регулярки для цифр — отличные
pace_match = re.search(r"темп[^+-]*([+-]\d+[.,]\d+)", text_lower)
total_match = re.search(r"общее продвижение\s*(\d+[.,]\d+)", text_lower)
if pace_match:
return {
"status": "success",
"dates": dates_match.group(1).strip() if dates_match else "не указаны",
"pace": pace_match.group(1).replace(',', '.'),
"total": total_match.group(1).replace(',', '.') if total_match else "0",
"post_id": msg.get('data-post').split('/')[-1]
}
time.sleep(1)
except Exception as e:
print(f"Ошибка на итерации {i}: {e}")
continue
return {"status": "error", "message": "Сводка не найдена в последних постах"}