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
6 changed files with 333 additions and 187 deletions

View file

@ -3,7 +3,7 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /app WORKDIR /app
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r 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 maket ./maket
COPY AI ./AI COPY AI ./AI
CMD ["python", "main.py"] CMD ["python", "main.py"]

105
config.py
View file

@ -1,105 +0,0 @@
# ВСЕ КОНСТАНТНЫЕ ЗНАЧЕНИЯ
import os
from pathlib import Path
from datetime import timedelta, time
def make_time(h: int, m: int) -> time:
return time(hour=h, minute=m)
# Базовые пути
BASE_DIR = Path(__file__).resolve().parent
FONT_PATH = BASE_DIR / "impact.ttf"
DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow")
# Состояние мини-игры
PENIS_STATE: dict[int, dict[str, object]] = {}
PENIS_START_LENGTH = 10.0
PENIS_MIN_DELTA = 0.1
PENIS_MAX_DELTA = 7.0
PENIS_COOLDOWN = timedelta(hours=24)
PENIS_TOP_LIMIT = 10
# Твои шаблоны и тот самый огромный кортеж
TEMPLATES = {
"1": {
"path": BASE_DIR / "maket" / "1.png",
"anchor": (104, 240),
"padding": 20,
"label": "Владимир Владимирович Путин самый лучший президент",
},
# Сюда вставь весь остальной огромный массив
}
SCHEDULE: dict[int, list[tuple[time, time, int | None]]] = {
0: [ # Monday
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
(make_time(11, 55), make_time(12, 40), 5),
(make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), 30),
(make_time(15, 50), make_time(16, 35), 5),
(make_time(16, 40), make_time(17, 25), None),
],
1: [ # Tuesday
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
(make_time(11, 55), make_time(12, 40), 5),
(make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), None),
],
2: [], # Wednesday — кайфуем дома
3: [ # Thursday
(make_time(8, 0), make_time(8, 45), 5),
(make_time(8, 50), make_time(9, 35), 15),
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
(make_time(11, 55), make_time(12, 40), 5),
(make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), 30),
(make_time(15, 50), make_time(16, 35), 5),
(make_time(16, 40), make_time(17, 25), None),
],
4: [ # Friday
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
(make_time(11, 55), make_time(12, 40), 5),
(make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), None),
],
5: [], # Saturday — выходной
6: [], # Sunday — выходной
}
# Ключевые слова, на которые бот кидает трек Creep.
KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'}
def templates_list_text() -> str:
parts = []
for tid in sorted(TEMPLATES.keys()):
label = TEMPLATES[tid].get("label", "")
if label:
parts.append(f"{tid}{label}")
else:
parts.append(tid)
return ", ".join(parts) if parts else "нет макетов"
HINT_TEXT = (
# Общая подсказка по командам.
"Команды:\n"
f"• /maket «id» «текст» — доступные: {templates_list_text()} (1: 737x414, точка 104,240).\n"
"• /gen_mem Up:верх Down:низ — отправь с фото (как подпись) или ответом на фото.\n"
"• /break — узнать, сколько минут до перекура.\n"
"• /penis — миниигра: +/ от 0.1 до 7 см, раз в 24 часа.\n"
"• /top_penis — динамический топ по текущей длине.\n"
"• /talk [сообщение] — побазарить с ИИ на фене.\n"
"• /prices — старый способ цен.\n"
"• /fetch — свежие цены с x-server.\n"
"Подсказка: в группах команда должна быть слитно с ботом (/gen_mem@username)."
)

View file

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

353
main.py
View file

@ -1,40 +1,155 @@
# Сделал чуть компактнее и читабельнее import asyncio
import asyncio, logging, os, re, requests, random from AI.talk_handler import handle_talk
import logging
import os
import re
import requests
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from textwrap import wrap from textwrap import wrap
from datetime import datetime, time, timedelta import random
from zoneinfo import ZoneInfo 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 AI.talk_handler import handle_talk from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
from zparser import get_military_data
# Конфигурационный файл, куда я вынес все константы # Базовые пути/настройки проекта.
import config BASE_DIR = Path(__file__).parent
FONT_PATH = BASE_DIR / "impact.ttf"
DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow")
# Данные миниигры /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.
TEMPLATES = {
"1": {
"path": BASE_DIR / "maket" / "1.png",
"anchor": (104, 240),
"padding": 20, # right/bottom padding from edges
"label": "Владимир Владимирович Путин самый лучший президент",
},
}
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 = []
for tid in sorted(TEMPLATES.keys()):
label = TEMPLATES[tid].get("label", "")
if label:
parts.append(f"{tid}{label}")
else:
parts.append(tid)
return ", ".join(parts) if parts else "нет макетов"
# ---- Schedule & breaks ---- # ---- Schedule & breaks ----
def make_time(h: int, m: int) -> time:
# Короткий хелпер для заполнения расписания.
return time(hour=h, minute=m)
# Schedule format: list of (lesson_start, lesson_end, break_minutes_after)
SCHEDULE: dict[int, list[tuple[time, time, int | None]]] = {
0: [ # Monday
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
(make_time(11, 55), make_time(12, 40), 5),
(make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), 30),
(make_time(15, 50), make_time(16, 35), 5),
(make_time(16, 40), make_time(17, 25), None),
],
1: [ # Tuesday
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
(make_time(11, 55), make_time(12, 40), 5),
(make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), None),
],
2: [], # Wednesday — кайфуем дома
3: [ # Thursday
(make_time(8, 0), make_time(8, 45), 5),
(make_time(8, 50), make_time(9, 35), 15),
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
(make_time(11, 55), make_time(12, 40), 5),
(make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), 30),
(make_time(15, 50), make_time(16, 35), 5),
(make_time(16, 40), make_time(17, 25), None),
],
4: [ # Friday
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
(make_time(11, 55), make_time(12, 40), 5),
(make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), None),
],
5: [], # Saturday — выходной
6: [], # Sunday — выходной
}
def minutes_until_break(now: datetime | None = None) -> str: def minutes_until_break(now: datetime | None = None) -> str:
# Возвращает статус: до перекура / идёт перекур / пары окончены. # Возвращает статус: до перекура / идёт перекур / пары окончены.
if now is None: if now is None:
now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) now = datetime.now(ZoneInfo(DEFAULT_TZ))
elif now.tzinfo is None: elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ)) now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ))
day = now.weekday() day = now.weekday()
today_SCHEDULE = config.SCHEDULE.get(day, []) today_schedule = SCHEDULE.get(day, [])
if not today_SCHEDULE: if not today_schedule:
return "Сегодня кайфуем дома, перекур не нужен." return "Сегодня кайфуем дома, перекур не нужен."
def dt_for(t: time) -> datetime: def dt_for(t: time) -> datetime:
return now.replace(hour=t.hour, minute=t.minute, second=0, microsecond=0) return now.replace(hour=t.hour, minute=t.minute, second=0, microsecond=0)
for start_t, end_t, br_minutes in today_SCHEDULE: for start_t, end_t, br_minutes in today_schedule:
start_dt = dt_for(start_t) start_dt = dt_for(start_t)
end_dt = dt_for(end_t) end_dt = dt_for(end_t)
@ -57,6 +172,19 @@ def minutes_until_break(now: datetime | None = None) -> str:
return "Пары закончились, перекур только завтра." return "Пары закончились, перекур только завтра."
HINT_TEXT = (
# Общая подсказка по командам.
"Команды:\n"
f"• /maket «id» «текст» — доступные: {templates_list_text()} (1: 737x414, точка 104,240).\n"
"• /gen_mem Up:верх Down:низ — отправь с фото (как подпись) или ответом на фото.\n"
"• /break — узнать, сколько минут до перекура.\n"
"• /penis — миниигра: +/ от 0.1 до 7 см, раз в 24 часа.\n"
"• /top_penis — динамический топ по текущей длине.\n"
"• /talk [сообщение] — побазарить с ИИ на фене.\n"
"• /prices — старый способ цен.\n"
"• /fetch — свежие цены с x-server.\n"
"Подсказка: в группах команда должна быть слитно с ботом (/gen_mem@username)."
)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -64,9 +192,9 @@ logger = logging.getLogger(__name__)
def load_font(size: int) -> ImageFont.FreeTypeFont: def load_font(size: int) -> ImageFont.FreeTypeFont:
# Грузим Impact нужного размера. # Грузим Impact нужного размера.
if not config.FONT_PATH.exists(): if not FONT_PATH.exists():
raise FileNotFoundError("impact.ttf not found. Place the font in the project root.") raise FileNotFoundError("impact.ttf not found. Place the font in the project root.")
return ImageFont.truetype(str(config.FONT_PATH), size=size) return ImageFont.truetype(str(FONT_PATH), size=size)
def wrap_text_to_width(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.ImageDraw, stroke_width: int) -> list[str]: def wrap_text_to_width(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.ImageDraw, stroke_width: int) -> list[str]:
@ -129,7 +257,7 @@ def draw_multiline(draw: ImageDraw.ImageDraw, lines: list[str], position: tuple[
def generate_maket(text: str, template_id: str = "1") -> BytesIO: def generate_maket(text: str, template_id: str = "1") -> BytesIO:
# Генерируем картинку макета с текстом в заранее заданной области. # Генерируем картинку макета с текстом в заранее заданной области.
tpl = config.TEMPLATES.get(template_id) tpl = TEMPLATES.get(template_id)
if not tpl: if not tpl:
raise FileNotFoundError(f"Template {template_id} is missing") raise FileNotFoundError(f"Template {template_id} is missing")
template_path = tpl["path"] template_path = tpl["path"]
@ -228,9 +356,9 @@ def minutes_until_next_lesson(now: datetime | None = None) -> str:
import random import random
if now is None: if now is None:
now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) now = datetime.now(ZoneInfo(DEFAULT_TZ))
elif now.tzinfo is None: elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ)) now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ))
LESSON_DAYS = [1, 3] # 1 = вторник, 3 = четверг LESSON_DAYS = [1, 3] # 1 = вторник, 3 = четверг
LESSON_START = time(9, 50) LESSON_START = time(9, 50)
@ -301,38 +429,66 @@ def minutes_until_next_lesson(now: datetime | None = None) -> str:
def play_penis(user_id: int, user_name: str | None = None, now: datetime | None = None) -> tuple[bool, str]: def play_penis(user_id: int, user_name: str | None = None, now: datetime | None = None) -> tuple[bool, str]:
# Миниигра /penis: изменение длины и кулдаун 24 часа от последнего броска. # Миниигра /penis: изменение длины и кулдаун 24 часа от последнего броска.
if now is None: if now is None:
now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) now = datetime.now(ZoneInfo(DEFAULT_TZ))
elif now.tzinfo is None: elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(config.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 = config.PENIS_STATE.get(user_id) try:
if state is None: with sqlite3.connect(PENIS_DB_PATH) as conn:
state = {"length": config.PENIS_START_LENGTH, "last_used_at": None} conn.row_factory = sqlite3.Row
config.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 + config.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", config.PENIS_START_LENGTH)) delta = round(random.uniform(PENIS_MIN_DELTA, PENIS_MAX_DELTA), 1)
delta = round(random.uniform(config.PENIS_MIN_DELTA, config.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 (
@ -352,25 +508,34 @@ def format_penis_user_name(user: types.User) -> str:
return f"user_{user.id}" return f"user_{user.id}"
def build_penis_top(limit: int = config.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 config.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)
@ -382,7 +547,7 @@ async def handle_start(message: Message):
async def handle_help(message: Message): async def handle_help(message: Message):
# Вывод подсказки по возможностям бота. # Вывод подсказки по возможностям бота.
await message.answer(config.HINT_TEXT) await message.answer(HINT_TEXT)
async def handle_break(message: Message): async def handle_break(message: Message):
# Команда расписания перекура. # Команда расписания перекура.
@ -393,14 +558,14 @@ async def handle_maket(message: Message):
# Генерация текста в подготовленном шаблоне /maket. # Генерация текста в подготовленном шаблоне /maket.
parts = message.text.split(maxsplit=2) parts = message.text.split(maxsplit=2)
if len(parts) == 1: if len(parts) == 1:
await message.reply(f"Доступные макеты: {config.templates_list_text()}\nФормат: /maket «id» «текст»") await message.reply(f"Доступные макеты: {templates_list_text()}\nФормат: /maket «id» «текст»")
return return
if len(parts) < 3: if len(parts) < 3:
await message.reply(f"Формат: /maket «id» «текст»\nДоступные макеты: {config.templates_list_text()}\n\n" + config.HINT_TEXT) await message.reply(f"Формат: /maket «id» «текст»\nДоступные макеты: {templates_list_text()}\n\n" + HINT_TEXT)
return return
template_id = parts[1] template_id = parts[1]
if template_id not in config.TEMPLATES: if template_id not in TEMPLATES:
await message.reply(f"Такого макета нет. Доступные: {config.templates_list_text()}") await message.reply(f"Такого макета нет. Доступные: {templates_list_text()}")
return return
text = parts[2].strip() text = parts[2].strip()
if not text: if not text:
@ -493,7 +658,9 @@ async def handle_top_penis(message: Message):
await message.answer(build_penis_top()) await message.answer(build_penis_top())
# Смотреть config.py # Ключевые слова, на которые бот кидает трек Creep.
KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'}
# I'M CREEP
async def handle_keywords(message: types.Message): async def handle_keywords(message: types.Message):
# Не реагируем на команды, только на обычный текст. # Не реагируем на команды, только на обычный текст.
@ -502,8 +669,8 @@ async def handle_keywords(message: types.Message):
text = message.text.lower() text = message.text.lower()
if any(re.search(rf'\b{word}\b', text) for word in config.KEYWORDS): if any(re.search(rf'\b{word}\b', text) for word in KEYWORDS):
audio_path = config.BASE_DIR / "maket" / "Radiohead - Creep.mp3" audio_path = BASE_DIR / "maket" / "Radiohead - Creep.mp3"
if audio_path.exists(): if audio_path.exists():
await message.answer_audio( await message.answer_audio(
@ -591,34 +758,58 @@ async def handle_zvetok(message: Message):
await message.answer(result) 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(): def main():
# Точка входа: создаём бота, регистрируем команды и запускаем polling. # Точка входа: создаём бота, регистрируем команды и запускаем polling.
token = os.getenv("BOT_TOKEN") token = os.getenv("BOT_TOKEN")
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:
await bot.set_my_commands( commands = [
[ BotCommand(command="start", description="Приветствие"),
BotCommand(command="start", description="Приветствие"), BotCommand(command="help", description="Подсказки по командам"),
BotCommand(command="help", description="Подсказки по командам"), BotCommand(command="maket", description="Вписать текст в макет 1"),
BotCommand(command="maket", description="Вписать текст в макет 1"), BotCommand(command="gen_mem", description="Мем из фото (ответом)"),
BotCommand(command="gen_mem", description="Мем из фото (ответом)"), BotCommand(command="break", description="Сколько минут до перекура"),
BotCommand(command="break", description="Сколько минут до перекура"), BotCommand(command="size", description="хуй"),
BotCommand(command="size", description="хуй"), BotCommand(command="penis", description="миниигра 1 раз в 24ч"),
BotCommand(command="penis", description="миниигра 1 раз в 24ч"), BotCommand(command="top_penis", description="топ по длине"),
BotCommand(command="top_penis", description="топ по длине"), BotCommand(command="prices", description="цены x-server"),
BotCommand(command="prices", description="цены x-server"), BotCommand(command="fetch", description="цены (новый источник)"),
BotCommand(command="fetch", description="цены (новый источник)"), BotCommand(command="zvetok", description="Цветянский бля"),
BotCommand(command="zvetok", description="Цветянский бля"), BotCommand(command="talk", 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 except Exception: # noqa: WPS440
logger.warning("Не удалось выставить команды, но бот продолжит работу.") logger.warning("Не удалось выставить команды, но бот продолжит работу.")
@ -636,7 +827,7 @@ def main():
dp.message.register(handle_fetch, Command("fetch")) dp.message.register(handle_fetch, Command("fetch"))
dp.message.register(handle_zvetok, Command("zvetok")) dp.message.register(handle_zvetok, Command("zvetok"))
dp.message.register(handle_talk, Command("talk")) dp.message.register(handle_talk, Command("talk"))
dp.message.register(handle_svodka, Command("svodka"))
async def handle_gen_mem_cmd(message: Message, bot: Bot): async def handle_gen_mem_cmd(message: Message, bot: Bot):

View file

@ -1,3 +1,6 @@
aiogram>=3.4.1 aiogram>=3.4.1
pillow>=10.3.0 pillow>=10.3.0
requests>=2.31.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": "Сводка не найдена в последних постах"}