Compare commits

..

14 commits

Author SHA1 Message Date
q
97c1795831 Fix ai server url 2026-02-22 20:08:45 +03:00
q
185ef45621 Merge pull request 'Изменения: что изменилось' (#5) from Zhirik1337/bot_tg:wpng into main
Reviewed-on: zovos/bot_tg#5
2026-02-22 17:04:43 +00:00
ac491e6433 Изменения: что изменилось
Добавлена миниигра /penis с хранением состояния по пользователю (в памяти процесса).
main.py (line 28)
main.py (line 29)
main.py (line 32)
main.py (line 400)
Что делает:
хранит length и last_used_at на user_id;
даёт изменение длины случайно +/- в диапазоне 0.1..7.0;
ограничивает минимум до 0.1;
вводит кулдаун 24 часа от последнего вызова.
Реализован динамический топ /top_penis.
main.py (line 33)
main.py (line 454)
main.py (line 590)
Что делает:
собирает текущие значения из PENIS_STATE;
сортирует по убыванию см;
показывает медали 🥇🥈🥉 и далее нумерацию;
лимит топа по умолчанию 10 участников;
при пустом состоянии пишет, что топ пуст.
Добавлена нормализация имени пользователя для топа.
main.py (line 445)
main.py (line 585)
Как работает:
приоритет @username;
если нет username, берёт full_name;
fallback: user_<id>.
Обновлены подсказки и меню Telegram-команд.
main.py (line 153)
main.py (line 716)
main.py (line 735)
Что добавлено:
строка в /help про /top_penis;
BotCommand("top_penis") в меню;
регистрация обработчика Command("top_penis").
Добавлены подробные комментарии по коду.
main.py (line 21)
main.py (line 146)
main.py (line 400)
talk_handler.py (line 10)
talk_handler.py (line 45)
Что покрыто:
константы/секции;
игровые функции;
хендлеры;
старт и регистрация роутов;
пояснения к LLM-модулю /talk.
Что протестировано локально.
compileall для main.py и talk_handler.py — успешно.
Смоук-тесты функций:
parse_up_down, minutes_until_break, minutes_until_next_lesson;
generate_maket, generate_meme;
play_penis (включая cooldown);
build_penis_top (сортировка/пустой топ).
Итог: основные проверки прошли.
Текущее ограничение реализации.
Состояние /penis и /top_penis хранится только в RAM, после перезапуска бота сбрасывается.
main.py (line 28)
2026-02-22 19:40:57 +03:00
q
947e0010e8 Merge pull request 'ИИ-бот' (#4) from Dan4ick/bot_tg:feature/FenyaBot into main
Reviewed-on: zovos/bot_tg#4
2026-02-22 13:27:01 +00:00
Danil
d39ada437c ИИ-бот на фене через llama.cpp 2026-02-22 14:07:01 +03:00
q
091022facc Merge pull request 'Ебать у меня идеи, а вы и дальше говорите что алкоголь вреден...........7' (#3) from jeffrey228/bot_tg:main into main
Reviewed-on: zovos/bot_tg#3
2026-02-21 04:48:55 +00:00
itexpert228
de07891926
Ебать у меня идеи, а вы и дальше говорите что алкоголь вреден...........7 2026-02-20 23:28:22 +03:00
q
a85ab1c639 Fix api domain 2026-02-20 18:43:05 +03:00
q
99946bc923 Fix, i am down 2026-02-20 14:27:58 +03:00
q
f385bedd9b add new zaloopa 2026-02-20 14:23:47 +03:00
q
1d79125500 Remove tracked pycache 2026-02-20 13:57:49 +03:00
q
aafb64ad4e Resolve merge conflict: combine size and creep handlers 2026-02-20 13:57:49 +03:00
q
11ad1c43c9 Add radiohead creeeeeeep 2026-02-20 13:08:48 +03:00
hitoshi
5072450244 Ну короче реализация вайба, я ща ОБОССУСЬ НАХУЙ 2026-02-20 12:40:54 +03:00
8 changed files with 417 additions and 7 deletions

2
.gitignore vendored
View file

@ -18,3 +18,5 @@ db/
.pytest_cache/
.mypy_cache/
AI/models/

65
AI/talk_handler.py Normal file
View file

@ -0,0 +1,65 @@
import asyncio
import os
import logging
import aiohttp
from aiogram.types import Message
logger = logging.getLogger(__name__)
# Адрес OpenAI-совместимого API у llama.cpp сервера.
LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-ai/")
# ПЖ НЕ ДЕЛАЙТЕ ДУДОС
# Системный промпт задаёт стиль и "характер" ответа.
SYSTEM_PROMPT = (
"Ты — чёткий пацан с района. Базаришь строго на фене — блатной жаргон, "
"тюремный сленг, дворовый язык. Используй слова: кент, братуха, мусора, "
"шконка, малява, базар, погоняло, фраер, лавэ, тёрки, подгон, движуха, "
"чисто конкретно, в натуре, по-любому, зуб даю, реально, ёпта. "
"Отвечай коротко и дерзко. Не ломай персонажа. Не используй формальный язык. "
"Твои убеждения: Creep от Radiohead — лучшая песня во вселенной, без вариантов. "
"Линукс — лучшая ОС во вселенной, остальное для фраеров. "
"Вселенную надо переписать на Rust, это не обсуждается. "
"Убунту — хуйня, настоящие пацаны сидят на Arch или Gentoo."
)
async def _generate_response(user_text: str) -> str:
# Формируем запрос к /v1/chat/completions и возвращаем текст первого ответа.
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
payload = {
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_text},
],
"max_tokens": 256,
"temperature": 0.8,
"top_p": 0.9,
}
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
data = await resp.json()
return data["choices"][0]["message"]["content"].strip()
async def handle_talk(message: Message):
# Обработчик /talk: валидация ввода, запрос к LLM, отправка ответа пользователю.
parts = message.text.split(maxsplit=1)
if len(parts) < 2 or not parts[1].strip():
await message.reply("Ты чё, кент? Напиши /talk [текст], побазарим.")
return
user_text = parts[1].strip()
# Показываем "печатает..." пока ждём ответ модели.
await message.bot.send_chat_action(chat_id=message.chat.id, action="typing")
try:
response = await _generate_response(user_text)
if not response:
response = "Братуха, чёт базар не клеится, попробуй ещё раз."
await message.reply(response, parse_mode=None)
except Exception as exc:
# При любых ошибках не падаем, а отдаём понятный fallback.
logger.exception("Talk generation failed")
await message.reply("Бля, кент, чёт движок заглох. Попробуй позже.")

View file

@ -5,4 +5,5 @@ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py impact.ttf ./
COPY maket ./maket
COPY AI ./AI
CMD ["python", "main.py"]

View file

@ -3,7 +3,8 @@
Телеграм‑бот, который:
- вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`);
- делает мемы из присланных фото (`/gen_mem Up:... Down:...`);
- подсказывает, сколько минут до перекура по заданному расписанию (`/break`).
- подсказывает, сколько минут до перекура по заданному расписанию (`/break`);
- умеет базарить (`/talk`).
## Запуск
@ -11,7 +12,8 @@
docker-compose up --build
```
Переменные окружения: задайте `BOT_TOKEN` в `.env` (уже добавлен шаблон).
Переменные окружения: задайте `BOT_TOKEN` в `.env` (уже добавлен шаблон);
`LLAMA_API_URL` — адрес llama.cpp сервера (для `/talk`)
## Команды
@ -20,6 +22,7 @@ docker-compose up --build
- `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240).
- `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down.
- `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур.
- `/talk <сообщение>` — побазарить с ИИ. Работает через llama.cpp сервер.
## Макеты и шрифт
@ -29,6 +32,7 @@ docker-compose up --build
## Технологии
- Python 3.11, aiogram 3, Pillow.
- llama.cpp для ИИ-фич.
- Dockerfile + docker-compose для развёртывания.
## Кратко о логике
@ -36,3 +40,13 @@ docker-compose up --build
- `/maket` подбирает размер текста, чтобы уместить его в область макета.
- `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой.
- `/break` считает по расписанию в `main.py` (словарь `SCHEDULE`).
- `/talk` шлёт запрос к llama.cpp серверу (`LLAMA_API_URL`), системный промпт задаёт стиль фени(Можно изменить).
## ИИ-модуль (`AI/talk_handler.py`)
Отдельный модуль, не трогает основной код. Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`).
Настройка сервера:
1. Скачать модель: `wget https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf`
2. Запустить: `./llama-server -m model.gguf --host 0.0.0.0 --port 8080 -c 2048`
3. Прописать URL в `.env`: `LLAMA_API_URL=http://адрес:8080`

Binary file not shown.

333
main.py
View file

@ -1,24 +1,38 @@
import asyncio
from AI.talk_handler import handle_talk
import logging
import os
import re
import requests
from io import BytesIO
from pathlib import Path
from textwrap import wrap
import random
from aiogram import Bot, Dispatcher, F
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
from aiogram.types import BufferedInputFile, Message, BotCommand, FSInputFile
from aiogram.client.default import DefaultBotProperties
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
# Базовые пути/настройки проекта.
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_START_LENGTH = 10.0
PENIS_MIN_DELTA = 0.1
PENIS_MAX_DELTA = 7.0
PENIS_COOLDOWN = timedelta(hours=24)
PENIS_TOP_LIMIT = 10
# Конфигурация текстовых макетов /maket.
TEMPLATES = {
"1": {
"path": BASE_DIR / "maket" / "1.png",
@ -29,6 +43,7 @@ TEMPLATES = {
}
def templates_list_text() -> str:
# Собираем человекочитаемый список доступных макетов.
parts = []
for tid in sorted(TEMPLATES.keys()):
label = TEMPLATES[tid].get("label", "")
@ -41,6 +56,7 @@ def templates_list_text() -> str:
# ---- Schedule & breaks ----
def make_time(h: int, m: int) -> time:
# Короткий хелпер для заполнения расписания.
return time(hour=h, minute=m)
@ -91,6 +107,7 @@ SCHEDULE: dict[int, list[tuple[time, time, int | None]]] = {
def minutes_until_break(now: datetime | None = None) -> str:
# Возвращает статус: до перекура / идёт перекур / пары окончены.
if now is None:
now = datetime.now(ZoneInfo(DEFAULT_TZ))
elif now.tzinfo is None:
@ -127,10 +144,16 @@ def minutes_until_break(now: datetime | None = None) -> str:
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)."
)
@ -139,12 +162,14 @@ logger = logging.getLogger(__name__)
def load_font(size: int) -> ImageFont.FreeTypeFont:
# Грузим Impact нужного размера.
if not FONT_PATH.exists():
raise FileNotFoundError("impact.ttf not found. Place the font in the project root.")
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]:
# Переносим текст по словам так, чтобы каждая строка влезла в max_width.
words = text.split()
if not words:
return []
@ -165,6 +190,7 @@ def wrap_text_to_width(text: str, font: ImageFont.FreeTypeFont, max_width: int,
def fit_text_in_box(text: str, max_width: int, max_height: int, start_size: int = 72, min_size: int = 14):
# Подбираем максимальный размер шрифта, который влезет в заданный прямоугольник.
draw_dummy = ImageDraw.Draw(Image.new("RGB", (max_width, max_height)))
for size in range(start_size, min_size - 1, -2):
font = load_font(size)
@ -179,7 +205,7 @@ def fit_text_in_box(text: str, max_width: int, max_height: int, start_size: int
total_height = sum(line_heights) + max(0, len(lines) - 1) * max(6, size // 6)
if total_height <= max_height:
return font, lines, stroke_width, max(6, size // 6)
# Fallback to minimal size
# Если не влезло ни на одном размере, используем минимальный.
font = load_font(min_size)
stroke_width = max(1, min_size // 12)
lines = wrap_text_to_width(text, font, max_width, draw_dummy, stroke_width)
@ -188,6 +214,7 @@ def fit_text_in_box(text: str, max_width: int, max_height: int, start_size: int
def draw_multiline(draw: ImageDraw.ImageDraw, lines: list[str], position: tuple[int, int], font: ImageFont.FreeTypeFont,
stroke_width: int, line_spacing: int, align_center: bool = False, max_width: int | None = None):
# Рисуем набор строк с опциональным центрированием по горизонтали.
x, y = position
for line in lines:
bbox = draw.textbbox((0, 0), line, font=font, stroke_width=stroke_width)
@ -200,6 +227,7 @@ def draw_multiline(draw: ImageDraw.ImageDraw, lines: list[str], position: tuple[
def generate_maket(text: str, template_id: str = "1") -> BytesIO:
# Генерируем картинку макета с текстом в заранее заданной области.
tpl = TEMPLATES.get(template_id)
if not tpl:
raise FileNotFoundError(f"Template {template_id} is missing")
@ -223,6 +251,7 @@ def generate_maket(text: str, template_id: str = "1") -> BytesIO:
def calculate_font_for_width(text: str, image_width: int, max_ratio: float = 0.09, min_size: int = 18):
# Подбор размера шрифта для верхнего/нижнего блока мема.
start_size = max(int(image_width * max_ratio), min_size)
draw_dummy = ImageDraw.Draw(Image.new("RGB", (image_width, image_width)))
for size in range(start_size, min_size - 1, -2):
@ -243,6 +272,7 @@ def calculate_font_for_width(text: str, image_width: int, max_ratio: float = 0.0
def generate_meme(img: Image.Image, top_text: str, bottom_text: str) -> BytesIO:
# Рисуем классический мем-текст сверху/снизу и возвращаем JPEG в памяти.
img = img.convert("RGB")
draw = ImageDraw.Draw(img)
width, height = img.size
@ -272,6 +302,8 @@ def generate_meme(img: Image.Image, top_text: str, bottom_text: str) -> BytesIO:
def parse_up_down(text: str) -> tuple[str, str]:
# Парсим формат /gen_mem Up:... Down:...
# Если меток нет, используем fallback: первое слово -> up, остальное -> down.
up = ""
down = ""
# Normalize line breaks
@ -290,19 +322,174 @@ def parse_up_down(text: str) -> tuple[str, str]:
down = parts[1].strip()
return up, down
def minutes_until_next_lesson(now: datetime | None = None) -> str:
# Отдельная "угарная" логика для /zvetok.
import random
if now is None:
now = datetime.now(ZoneInfo(DEFAULT_TZ))
elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ))
LESSON_DAYS = [1, 3] # 1 = вторник, 3 = четверг
LESSON_START = time(9, 50)
LESSON_END = time(11, 25)
day = now.weekday()
def lesson_start_for(day_offset: int) -> datetime:
target = now + timedelta(days=day_offset)
return target.replace(
hour=LESSON_START.hour,
minute=LESSON_START.minute,
second=0,
microsecond=0
)
def lesson_end_today() -> datetime:
return now.replace(
hour=LESSON_END.hour,
minute=LESSON_END.minute,
second=0,
microsecond=0
)
# ---- Проверяем: сейчас внутри пары? ----
if day in LESSON_DAYS:
start_today = lesson_start_for(0)
end_today = lesson_end_today()
if start_today <= now < end_today:
# ищем следующую пару (не сегодняшнюю)
for i in range(1, 8):
next_day = (day + i) % 7
if next_day in LESSON_DAYS:
next_dt = lesson_start_for(i)
break
delta = next_dt - now
total_seconds = int(delta.total_seconds())
days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60
percent = random.randint(0, 100)
return (
f"Сегодня твой мозг сгнил на {percent}%\n"
f"Мозг начнет догнивать через {days} дн {hours} ч {minutes} мин"
)
# ---- Ищем ближайшую будущую пару ----
for i in range(0, 8):
next_day = (day + i) % 7
if next_day in LESSON_DAYS:
candidate = lesson_start_for(i)
if candidate > now:
delta = candidate - now
break
total_seconds = int(delta.total_seconds())
days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60
return f"Твой мозг окончательно сгниет, а очко порвется через: {days} дн {hours} ч {minutes} мин"
def play_penis(user_id: int, user_name: str | None = None, now: datetime | None = None) -> tuple[bool, str]:
# Миниигра /penis: изменение длины и кулдаун 24 часа от последнего броска.
if now is None:
now = datetime.now(ZoneInfo(DEFAULT_TZ))
elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ))
state = PENIS_STATE.get(user_id)
if state is None:
state = {"length": PENIS_START_LENGTH, "last_used_at": None}
PENIS_STATE[user_id] = state
# Обновляем отображаемое имя для топа.
if user_name:
state["display_name"] = user_name
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} мин."
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
sign_text = "+" if sign > 0 else "-"
return (
True,
f"📏 Изменение: {sign_text}{delta:.1f} см\n"
f"Текущая длина: {new_length:.1f} см\n"
"Следующая попытка через 24 часа.",
)
def format_penis_user_name(user: types.User) -> str:
# Красиво подписываем пользователя для топа.
if user.username:
return f"@{user.username}"
if user.full_name:
return user.full_name
return f"user_{user.id}"
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()))
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):
prefix = medals.get(index, f"{index}.")
lines.append(f"{prefix} {name}{length:.1f} см")
return "\n".join(lines)
async def handle_start(message: Message):
# Базовый /start.
await message.answer("Соси")
async def handle_help(message: Message):
# Вывод подсказки по возможностям бота.
await message.answer(HINT_TEXT)
async def handle_break(message: Message):
# Команда расписания перекура.
await message.answer(minutes_until_break())
async def handle_maket(message: Message):
# Генерация текста в подготовленном шаблоне /maket.
parts = message.text.split(maxsplit=2)
if len(parts) == 1:
await message.reply(f"Доступные макеты: {templates_list_text()}\nФормат: /maket «id» «текст»")
@ -328,6 +515,7 @@ async def handle_maket(message: Message):
async def handle_gen_mem(message: Message, bot: Bot):
# Мем можно сделать из фото в этом же сообщении или из reply на фото.
photo_message = None
if message.photo:
photo_message = message
@ -345,6 +533,7 @@ async def handle_gen_mem(message: Message, bot: Bot):
photo = photo_message.photo[-1]
try:
# Берём самое большое фото Telegram и скачиваем в память.
file = await bot.get_file(photo.file_id)
buf = BytesIO()
await bot.download_file(file.file_path, destination=buf)
@ -365,6 +554,7 @@ async def handle_gen_mem(message: Message, bot: Bot):
await message.reply_photo(BufferedInputFile(output.getvalue(), filename="meme.jpg"))
async def handle_size(message: Message):
# Простая рандом-команда /size.
size = round(random.uniform(0.1, 35.0), 1)
if size < 5:
@ -386,7 +576,124 @@ async def handle_size(message: Message):
)
async def handle_penis(message: Message):
# Хендлер миниигры /penis.
if not message.from_user:
await message.answer("Команда доступна только пользователям.")
return
display_name = format_penis_user_name(message.from_user)
_, result_text = play_penis(message.from_user.id, display_name)
await message.answer(result_text)
async def handle_top_penis(message: Message):
# Хендлер топа по значениям /penis.
await message.answer(build_penis_top())
# Ключевые слова, на которые бот кидает трек Creep.
KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'}
# I'M CREEP
async def handle_keywords(message: types.Message):
# Не реагируем на команды, только на обычный текст.
if message.text and message.text.startswith('/'):
return
text = message.text.lower()
if any(re.search(rf'\b{word}\b', text) for word in KEYWORDS):
audio_path = BASE_DIR / "maket" / "Radiohead - Creep.mp3"
if audio_path.exists():
await message.answer_audio(
audio=FSInputFile(audio_path),
caption="I'm a creep, I'm a weirdo..."
)
else:
logger.warning(f"Файл не найден по пути: {audio_path}")
async def handle_prices(message: Message):
# Старый источник цен (/prices).
def fetch():
return requests.get(
"https://bebrik.xyz/magnit/products",
headers={"X-Token": "sisi"},
timeout=10,
)
try:
resp = await asyncio.to_thread(fetch)
if resp.status_code != 200:
await message.answer("пошел нахуй")
return
data = resp.json()
if not data:
await message.answer("иди нахуй")
return
text = "смерть в 25:\n\n"
for item in data:
name = item.get("name")
price = item.get("price")
text += f"{name}{price}\n"
await message.answer(text[:4000])
except Exception:
await message.answer("unable to connect x-server гандон")
async def handle_fetch(message: Message):
# Новый источник цен (/fetch).
def fetch():
return requests.get(
"https://mirror.porno4free.ru/magnit/magnit/prices/all",
headers={"x-token": "secret_token"},
timeout=10,
)
try:
resp = await asyncio.to_thread(fetch)
if resp.status_code != 200:
await message.reply("пошел нахуй")
return
data = resp.json()
if not isinstance(data, dict) or not data:
await message.reply("иди нахуй")
return
lines = ["смерть в 25:"]
# сортируем категории для стабильного вывода
for group in sorted(data.keys()):
items = data.get(group)
lines.append(f"\n{group}:")
if not isinstance(items, list):
continue
for item in items:
name = item.get("name")
price = item.get("price")
if not name or price is None:
continue
lines.append(f"{name}{price}")
text = "\n".join(lines)
await message.answer(text[:4000])
except Exception:
await message.answer("unable to connect x-server гандон")
async def handle_zvetok(message: Message):
# Команда с отдельной "мемной" логикой таймера.
result = minutes_until_next_lesson()
await message.answer(result)
def main():
# Точка входа: создаём бота, регистрируем команды и запускаем polling.
token = os.getenv("BOT_TOKEN")
if not token:
raise RuntimeError("BOT_TOKEN env var is required")
@ -395,6 +702,7 @@ def main():
dp = Dispatcher()
async def on_startup(bot: Bot):
# Выставляем меню команд в интерфейсе Telegram.
try:
await bot.set_my_commands(
[
@ -404,6 +712,12 @@ def main():
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="Побазарить"),
]
)
except Exception: # noqa: WPS440
@ -411,16 +725,29 @@ def main():
dp.startup.register(on_startup)
# Регистрация команд-обработчиков.
dp.message.register(handle_start, CommandStart())
dp.message.register(handle_help, Command("help"))
dp.message.register(handle_break, Command("break"))
dp.message.register(handle_maket, Command("maket"))
dp.message.register(handle_size, Command("size"))
dp.message.register(handle_penis, Command("penis"))
dp.message.register(handle_top_penis, Command("top_penis"))
dp.message.register(handle_prices, Command("prices"))
dp.message.register(handle_fetch, Command("fetch"))
dp.message.register(handle_zvetok, Command("zvetok"))
dp.message.register(handle_talk, Command("talk"))
async def handle_gen_mem_cmd(message: Message, bot: Bot):
# Обёртка под сигнатуру aiogram для /gen_mem.
await handle_gen_mem(message, bot)
dp.message.register(handle_gen_mem_cmd, Command("gen_mem"))
# Текстовые авто-реакции (без команд).
dp.message.register(handle_keywords, F.text)
logger.info("Bot is starting...")
asyncio.run(dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()))

BIN
maket/Radiohead - Creep.mp3 Normal file

Binary file not shown.

View file

@ -1,2 +1,3 @@
aiogram==3.4.1
pillow==10.3.0
aiogram>=3.4.1
pillow>=10.3.0
requests>=2.31.0