forked from zovos/bot_tg
Compare commits
38 commits
fad8629599
...
3039326100
| Author | SHA1 | Date | |
|---|---|---|---|
| 3039326100 | |||
|
|
481965e8f2 | ||
|
|
e014019912 | ||
|
|
f0cefc7f23 | ||
|
|
3baf71ab98 | ||
| 29cd511186 | |||
|
|
1925c44ac6 | ||
| 801d6da69b | |||
|
|
6b6c1dd15d | ||
| 4ecc25239e | |||
|
|
2bcc726eea | ||
| 93d8c2187a | |||
|
|
967cfb4a6d | ||
|
|
107a0647d1 | ||
| 7289296169 | |||
|
|
aee53349b1 | ||
|
|
a7c047f847 | ||
|
|
9295538f5f | ||
|
|
5b294415d0 | ||
|
|
43503dc778 | ||
|
|
75a63bb6fc | ||
|
|
682fc7422c | ||
| 373aa8f6c2 | |||
|
|
9f6a7464d1 | ||
|
|
fc85149345 | ||
|
|
7035a90656 | ||
|
|
73e539b4c4 | ||
|
|
c59858df53 | ||
|
|
24245f6b87 | ||
|
|
f14b1a8209 | ||
|
|
6afc30570f | ||
|
|
8bc905331c | ||
|
|
1cbebeec37 | ||
|
|
0ac74b95c5 | ||
|
|
7494ab2dfa | ||
|
|
ba6bee06e3 | ||
| 79e581a010 | |||
|
|
9b34fd13d5 |
17 changed files with 2165 additions and 104 deletions
|
|
@ -5,3 +5,4 @@ __pycache__
|
|||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
db
|
||||
|
|
|
|||
3
.env
3
.env
|
|
@ -1,2 +1,3 @@
|
|||
BOT_TOKEN=8488665890:AAH7FY5I2xzHhfOdxlzb11ewl2I-MMTH2-o
|
||||
BOT_TOKEN=8156642467:AAEalekr-33lFS3tTTkXIEiaawfDzR6lBJc
|
||||
TZ=Europe/Moscow
|
||||
ODDS_API_KEY=c29bc26dee8aa9d95a5ce8d14ea63923
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
from aiogram.types import Message
|
||||
|
|
@ -18,6 +20,11 @@ SYSTEM_PROMPT = (
|
|||
"Стиль: 1–3 предложения; если список — максимум 3 пункта. "
|
||||
"Не используй формальный язык, канцелярит, вежливые обороты. Не ломай персонажа.\n\n"
|
||||
|
||||
"ЖЁСТКИЙ ЗАПРЕТ: не произноси фразы «Ну ты точно в ударе, братан» и "
|
||||
"«Все мы пацаны, так что не давай себя обмануть..» (а также любые их вариации или "
|
||||
"перефраз). Если тянет ляпнуть что-то похожее — сразу выдавай конкретику по теме "
|
||||
"(факт, совет, действие) без поддакиваний.\n\n"
|
||||
|
||||
"Лексика (мешай, но не лепи всё подряд одним полотном): "
|
||||
"кент, братуха, мусора, шконка, малява, базар, погоняло, фраер, лавэ, тёрки, подгон, движуха, "
|
||||
"чисто конкретно, в натуре, по-любому, зуб даю, реально, ёпта, "
|
||||
|
|
@ -54,15 +61,91 @@ SYSTEM_PROMPT = (
|
|||
"а дальше пересказывай своими словами, без длинных дословных цитат."
|
||||
)
|
||||
|
||||
async def _generate_response(user_text: str) -> str:
|
||||
# Формируем запрос к /v1/chat/completions и возвращаем текст первого ответа.
|
||||
# Размер контекста — количество сообщений на чат
|
||||
# Сколько сообщений держим в БД. Нужно для быстрой очистки.
|
||||
_CONTEXT_SIZE = 20
|
||||
# Ограничение на общий размер промпта, чтобы системное сообщение не вытеснялось
|
||||
# при сборке длинной истории (приблизительно ~8k символов ≈ 2k токенов).
|
||||
_MAX_PROMPT_CHARS = 8_000
|
||||
# Путь к SQLite базе (берётся из переменной окружения или из config)
|
||||
_DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH", "/db/chat_history.sqlite3")
|
||||
|
||||
|
||||
def _init_db() -> None:
|
||||
"""Создаёт таблицу истории чата, если её нет."""
|
||||
Path(_DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
with sqlite3.connect(_DB_PATH) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS chat_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
text TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id ON chat_history(chat_id, id)")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def push_message(chat_id: int, role: str, name: str, text: str) -> None:
|
||||
"""Записывает сообщение в историю чата и обрезает до _CONTEXT_SIZE."""
|
||||
with sqlite3.connect(_DB_PATH) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO chat_history (chat_id, role, name, text) VALUES (?, ?, ?, ?)",
|
||||
(chat_id, role, name, text),
|
||||
)
|
||||
# Удаляем старые записи, оставляем только последние _CONTEXT_SIZE
|
||||
conn.execute(
|
||||
"""
|
||||
DELETE FROM chat_history
|
||||
WHERE chat_id = ? AND id NOT IN (
|
||||
SELECT id FROM chat_history WHERE chat_id = ? ORDER BY id DESC LIMIT ?
|
||||
)
|
||||
""",
|
||||
(chat_id, chat_id, _CONTEXT_SIZE),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _get_history(chat_id: int) -> list[dict]:
|
||||
"""Возвращает историю чата как список словарей."""
|
||||
with sqlite3.connect(_DB_PATH) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT role, name, text FROM chat_history WHERE chat_id = ? ORDER BY id ASC",
|
||||
(chat_id,),
|
||||
).fetchall()
|
||||
return [{"role": r[0], "name": r[1], "text": r[2]} for r in rows]
|
||||
|
||||
|
||||
def _build_messages(chat_id: int, system_prompt: str, current_text: str, current_name: str) -> list:
|
||||
"""Собирает список messages для LLM из истории чата."""
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
|
||||
# Берём историю с конца, пока не превысим бюджет по символам.
|
||||
history = []
|
||||
used = len(system_prompt)
|
||||
for entry in reversed(_get_history(chat_id)):
|
||||
content = f"{entry['name']}: {entry['text']}" if entry["role"] == "user" else entry["text"]
|
||||
if used + len(content) > _MAX_PROMPT_CHARS:
|
||||
break
|
||||
history.append({"role": entry["role"], "content": content})
|
||||
used += len(content)
|
||||
|
||||
messages.extend(reversed(history))
|
||||
messages.append({"role": "user", "content": f"{current_name}: {current_text}"})
|
||||
return messages
|
||||
|
||||
|
||||
async def _generate_response(chat_id: int, user_text: str, user_name: str) -> str:
|
||||
# Формируем запрос к /v1/chat/completions с историей чата и возвращаем текст первого ответа.
|
||||
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
|
||||
msgs = await asyncio.to_thread(_build_messages, chat_id, SYSTEM_PROMPT, user_text, user_name)
|
||||
payload = {
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_text},
|
||||
],
|
||||
"max_tokens": 500,
|
||||
"messages": msgs,
|
||||
"max_tokens": 200,
|
||||
"temperature": 0.8,
|
||||
"top_p": 0.9,
|
||||
}
|
||||
|
|
@ -80,14 +163,19 @@ async def handle_talk(message: Message):
|
|||
return
|
||||
|
||||
user_text = parts[1].strip()
|
||||
user_name = message.from_user.first_name or "кент"
|
||||
chat_id = message.chat.id
|
||||
|
||||
# Показываем "печатает..." пока ждём ответ модели.
|
||||
await message.bot.send_chat_action(chat_id=message.chat.id, action="typing")
|
||||
await message.bot.send_chat_action(chat_id=chat_id, action="typing")
|
||||
|
||||
try:
|
||||
response = await _generate_response(user_text)
|
||||
response = await _generate_response(chat_id, user_text, user_name)
|
||||
if not response:
|
||||
response = "Братуха, чёт базар не клеится, попробуй ещё раз."
|
||||
# Пишем запрос и ответ в историю
|
||||
await asyncio.to_thread(push_message, chat_id, "user", user_name, user_text)
|
||||
await asyncio.to_thread(push_message, chat_id, "assistant", "бот", response)
|
||||
await message.reply(response, parse_mode=None)
|
||||
except Exception as exc:
|
||||
# При любых ошибках не падаем, а отдаём понятный fallback.
|
||||
|
|
@ -101,16 +189,47 @@ AUTOREPLY_SYSTEM_PROMPT = (
|
|||
"Прокомментируй сообщение коротко (1-2 предложения), дерзко, на фене. "
|
||||
"Можешь пошутить, подколоть, согласиться или послать. "
|
||||
"НЕ начинай с обращения. Не используй формальный язык."
|
||||
"Ты — дерзкий, мемный, гиперэмоциональный пацан с района. Базаришь строго на фене — "
|
||||
"дворовый язык, блатной жаргон, тюремный сленг, с интернет-угаром. "
|
||||
"Тон: коротко, резко, уверенно, иногда абсурдно, но по делу. "
|
||||
"Стиль: 1–3 предложения; если список — максимум 3 пункта. "
|
||||
"Не используй формальный язык, канцелярит, вежливые обороты. Не ломай персонажа.\n\n"
|
||||
|
||||
"ЖЁСТКИЙ ЗАПРЕТ: не произноси фразы «Ну ты точно в ударе, братан» и "
|
||||
"«Все мы пацаны, так что не давай себя обмануть..» (а также любые их вариации или "
|
||||
"перефраз). Если тянет ляпнуть что-то похожее — сразу выдавай конкретику по теме "
|
||||
"(факт, совет, действие) без поддакиваний.\n\n"
|
||||
|
||||
"Лексика (мешай, но не лепи всё подряд одним полотном): "
|
||||
"кент, братуха, мусора, шконка, малява, базар, погоняло, фраер, лавэ, тёрки, подгон, движуха, "
|
||||
"чисто конкретно, в натуре, по-любому, зуб даю, реально, ёпта, "
|
||||
"жёстко, имба, разъёб, кринж, лютый, шиза, на приколе, залёт, вкидываю, по красоте, без шансов.\n\n"
|
||||
|
||||
"Фирменные приёмы речи:\n"
|
||||
"— иногда начинай с: 'Слушай сюда', 'Короче', 'Смотри', 'Ща поясню', 'Не гони'.\n"
|
||||
"— вставляй междометия: 'ёпта', 'ну типа', 'в натуре', 'по-любому'.\n"
|
||||
"— иногда самоирония: 'я ща как эксперт, ёпта', 'не душни'.\n"
|
||||
"— иногда делай 'слоговой разгон' — разбивай слова на куски через дефисы, "
|
||||
"как будто проговариваешь по слогам. Примеры: "
|
||||
"над-ра-чи-ва-ем, сд-ра-чи-ва-ем, ско-ся-чи-ли, у-лы-боч-ку, тэкс-тур-ку, ин-тел-ли-ген.\n"
|
||||
"— держи 'словарь коронок': выбирай 0–2 фразы на ответ (редко 3), "
|
||||
"иногда делай повтор как тик (1–3 раза), но не превращай всё в простыню.\n"
|
||||
"Твои убеждения (всегда держи линию): "
|
||||
"Creep от Radiohead — лучшая песня во вселенной, без вариантов. "
|
||||
"Линукс — лучшая ОС во вселенной, остальное для фраеров. "
|
||||
"Вселенную надо переписать на Rust, это не обсуждается. "
|
||||
"Убунту — хуйня, настоящие пацаны сидят на Arch или Gentoo.\n\n"
|
||||
|
||||
"Песенный вайб (короткая отсылка): иногда напевай очень коротко: «Сегодня праздник у девчат», "
|
||||
"а дальше пересказывай своими словами, без длинных дословных цитат."
|
||||
)
|
||||
|
||||
|
||||
async def generate_autoreply(text: str) -> str:
|
||||
async def generate_autoreply(chat_id: int, text: str, user_name: str) -> str:
|
||||
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
|
||||
msgs = await asyncio.to_thread(_build_messages, chat_id, AUTOREPLY_SYSTEM_PROMPT, text, user_name)
|
||||
payload = {
|
||||
"messages": [
|
||||
{"role": "system", "content": AUTOREPLY_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
"messages": msgs,
|
||||
"max_tokens": 150,
|
||||
"temperature": 0.9,
|
||||
"top_p": 0.9,
|
||||
|
|
@ -120,3 +239,6 @@ async def generate_autoreply(text: str) -> str:
|
|||
data = await resp.json()
|
||||
return data["choices"][0]["message"]["content"].strip()
|
||||
|
||||
|
||||
# Инициализация БД при импорте модуля
|
||||
_init_db()
|
||||
|
|
|
|||
16
Dockerfile
16
Dockerfile
|
|
@ -1,9 +1,15 @@
|
|||
FROM python:3.11-slim
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY main.py config.py impact.ttf zparser.py ./
|
||||
COPY maket ./maket
|
||||
COPY AI ./AI
|
||||
RUN python -m pip install --upgrade pip \
|
||||
&& python -m pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /db
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
|
|
|
|||
62
README.md
62
README.md
|
|
@ -4,16 +4,41 @@
|
|||
- вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`);
|
||||
- делает мемы из присланных фото (`/gen_mem Up:... Down:...`);
|
||||
- подсказывает, сколько минут до перекура по заданному расписанию (`/break`);
|
||||
- умеет базарить (`/talk`).
|
||||
- показывает расписание, текущую и следующую пару (`/schedule`, можно с днём недели);
|
||||
- умеет базарить (`/talk`) с контекстом 100 сообщений;
|
||||
- авто-ответы в чате (10% шанс + триггер-слова);
|
||||
- миниигра `/penis` + `/top_penis`;
|
||||
- казино на размер (`/penis_casino`);
|
||||
- гадание на фене с мемом (`/gadanie`);
|
||||
- ставки на матчи CS2, Dota 2, футбол с реальными коэффициентами (`/matches`, `/bet`).
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Переменные окружения: задайте `BOT_TOKEN` в `.env` (уже добавлен шаблон);
|
||||
`LLAMA_API_URL` — адрес llama.cpp сервера (для `/talk`)
|
||||
Логи:
|
||||
|
||||
```bash
|
||||
docker compose logs -f bot
|
||||
```
|
||||
|
||||
Остановка:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
Переменные окружения: задайте `BOT_TOKEN` в `.env`.
|
||||
Для ИИ-фич нужен `LLAMA_API_URL`.
|
||||
Для ставок нужен `ODDS_API_KEY` (бесплатно на https://the-odds-api.com).
|
||||
|
||||
Контейнер хранит состояние в `./db`:
|
||||
- `penis_stats.sqlite3`
|
||||
- `chat_history.sqlite3`
|
||||
- `polychaetsi_stats.json`
|
||||
- `bets.sqlite3`
|
||||
|
||||
## Команды
|
||||
|
||||
|
|
@ -22,7 +47,18 @@ docker-compose up --build
|
|||
- `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240).
|
||||
- `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down.
|
||||
- `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур.
|
||||
- `/talk <сообщение>` — побазарить с ИИ. Работает через llama.cpp сервер.
|
||||
- `/schedule [пн|mon]` — показывает расписание на ближайший указанный день недели; без аргумента показывает сегодня, текущую и следующую пару.
|
||||
- `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений).
|
||||
- `/penis` — миниигра, 1 раз в 24 часа.
|
||||
- `/top_penis` — топ по длине.
|
||||
- `/polychaetsi` — топ по слову «получается».
|
||||
- `/penis_casino <ставка>` — казино на размер (макс 1 см, казино всегда в выигрыше).
|
||||
- `/gadanie <тема>` — ИИ-гадание на фене + авто-мем.
|
||||
- `/ebalnik` — вкл/выкл авто-ответы бота в чате.
|
||||
- `/matches [cs|dota|football]` — ближайшие матчи с коэффициентами.
|
||||
- `/bet [спорт] <номер> <исход> <ставка>` — поставить см на матч.
|
||||
- `/mybets` — мои активные ставки.
|
||||
- `/svodka` — сводка СВО.
|
||||
|
||||
## Макеты и шрифт
|
||||
|
||||
|
|
@ -35,16 +71,26 @@ docker-compose up --build
|
|||
- llama.cpp для ИИ-фич.
|
||||
- Dockerfile + docker-compose для развёртывания.
|
||||
|
||||
## Docker Notes
|
||||
|
||||
- Образ ставит `tzdata`, поэтому `ZoneInfo("Europe/Moscow")` нормально работает в контейнере.
|
||||
- В образ копируется весь проект, а runtime-данные исключены через `.dockerignore`.
|
||||
- Если меняешь код или картинки, достаточно пересобрать: `docker compose up --build -d`.
|
||||
|
||||
## Кратко о логике
|
||||
|
||||
- `/maket` подбирает размер текста, чтобы уместить его в область макета.
|
||||
- `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой.
|
||||
- `/break` считает по расписанию в `main.py` (словарь `SCHEDULE`).
|
||||
- `/talk` шлёт запрос к llama.cpp серверу (`LLAMA_API_URL`), системный промпт задаёт стиль фени(Можно изменить).
|
||||
- `/break` считает по расписанию в `config.py` и показывает следующую пару.
|
||||
- Названия пар и верхняя/нижняя неделя лежат в `config.py` в `LESSON_SCHEDULE`.
|
||||
- `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite).
|
||||
- `/penis_casino` — слоты с множителем x1.2–x3 (`games/casino.py`).
|
||||
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).
|
||||
- `/matches` + `/bet` — ставки на реальные матчи, коэффициенты через The-Odds-API (`games/betting.py`). После `/matches cs|dota|football` можно короче: `/bet <номер> <исход> <ставка>`.
|
||||
|
||||
## ИИ-модуль (`AI/talk_handler.py`)
|
||||
|
||||
Отдельный модуль, не трогает основной код. Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`).
|
||||
Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`). Хранит контекст 100 сообщений в SQLite.
|
||||
|
||||
Настройка сервера:
|
||||
1. Скачать модель: `wget https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf`
|
||||
|
|
|
|||
137
config.py
137
config.py
|
|
@ -10,6 +10,7 @@ DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow")
|
|||
|
||||
# --- Настройки /penis ---
|
||||
PENIS_DB_PATH = "/db/penis_stats.sqlite3"
|
||||
CHAT_HISTORY_DB_PATH = "/db/chat_history.sqlite3"
|
||||
PENIS_START_LENGTH = 10.0
|
||||
PENIS_MIN_DELTA = 0.1
|
||||
PENIS_MAX_DELTA = 7.0
|
||||
|
|
@ -17,6 +18,44 @@ PENIS_COOLDOWN = timedelta(hours=24)
|
|||
PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds())
|
||||
PENIS_TOP_LIMIT = 10
|
||||
|
||||
# --- Настройки "получается" ---
|
||||
POLYCHAETSI_STATS_PATH = Path(os.getenv("POLYCHAETSI_STATS_PATH", "/db/polychaetsi_stats.json"))
|
||||
POLYCHAETSI_TARGET = "получается"
|
||||
POLYCHAETSI_TOP_LIMIT = 10
|
||||
POLYCHAETSI_IMAGE_PATH = BASE_DIR / "maket" / "cvetok.jpg"
|
||||
|
||||
# --- Казино ---
|
||||
CASINO_MAX_BET = 1.0
|
||||
|
||||
# --- E621 API ---
|
||||
E621_LOGIN = os.getenv("E621_LOGIN", "")
|
||||
E621_API_KEY = os.getenv("E621_API_KEY", "")
|
||||
|
||||
# --- Ставки на матчи ---
|
||||
ODDS_API_KEY = os.getenv("ODDS_API_KEY", "")
|
||||
ODDS_API_BASE = "https://api.the-odds-api.com/v4"
|
||||
ODDS_SPORTS = {
|
||||
"epl": "soccer_epl",
|
||||
"ucl": "soccer_uefa_champs_league",
|
||||
"laliga": "soccer_spain_la_liga",
|
||||
"bundesliga": "soccer_germany_bundesliga",
|
||||
"seriea": "soccer_italy_serie_a",
|
||||
"ligue1": "soccer_france_ligue_one",
|
||||
"europa": "soccer_uefa_europa_league",
|
||||
}
|
||||
BET_MAX_AMOUNT = 3.0
|
||||
BET_DB_PATH = "/db/bets.sqlite3"
|
||||
ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно)
|
||||
|
||||
# --- Magnit API ---
|
||||
MAGNIT_API_BASE_URL = os.getenv("MAGNIT_API_BASE_URL", "https://mirror.porno4free.ru/magnit")
|
||||
MAGNIT_STORE_CODE = os.getenv("MAGNIT_STORE_CODE", "618224")
|
||||
MAGNIT_REQUEST_TIMEOUT = int(os.getenv("MAGNIT_REQUEST_TIMEOUT", "15"))
|
||||
# Можно указывать просто строку с названием или словарь с query/label/store_code.
|
||||
MAGNIT_PRODUCTS = [
|
||||
"RedBull",
|
||||
]
|
||||
|
||||
# --- Конфигурация макетов ---
|
||||
TEMPLATES = {
|
||||
"1": {
|
||||
|
|
@ -25,12 +64,93 @@ TEMPLATES = {
|
|||
"padding": 20,
|
||||
"label": "Владимир Владимирович Путин самый лучший президент",
|
||||
},
|
||||
"2": {
|
||||
"path": BASE_DIR / "maket" / "2.png",
|
||||
"anchor": (40, 40),
|
||||
"box_size": (582, 346),
|
||||
"label": "Макет 2",
|
||||
},
|
||||
}
|
||||
|
||||
# --- Расписание ---
|
||||
def make_time(h: int, m: int) -> time:
|
||||
return time(hour=h, minute=m)
|
||||
|
||||
# Если у тебя наоборот, просто поменяй на False.
|
||||
UPPER_WEEK_IS_ODD = True
|
||||
|
||||
PAIR_TIMES = {
|
||||
1: (make_time(8, 0), make_time(9, 35)),
|
||||
2: (make_time(9, 50), make_time(11, 25)),
|
||||
3: (make_time(11, 55), make_time(13, 30)),
|
||||
4: (make_time(13, 45), make_time(15, 20)),
|
||||
5: (make_time(15, 50), make_time(17, 25)),
|
||||
}
|
||||
|
||||
LESSON_SCHEDULE = {
|
||||
"upper": {
|
||||
0: [
|
||||
{"pair": 1, "title": "Пары нету"},
|
||||
{"pair": 2, "title": "История Россия (семинар), каб. 504"},
|
||||
{"pair": 3, "title": "Первая Подгруппа: Основы программирование, каб. 606. Вторая подгруппа Глод."},
|
||||
{"pair": 4, "title": "Первая Подгруппа: Глод. Вторая подгруппа Основы программирование, каб 606"},
|
||||
{"pair": 5, "title": "Общекулутрная и профессиональная коммуникации."},
|
||||
],
|
||||
1: [
|
||||
{"pair": 1, "title": "Нету пары"},
|
||||
{"pair": 2, "title": "Физика (лаб), каб. 241"},
|
||||
{"pair": 3, "title": "Математика (лек), каб 514"},
|
||||
],
|
||||
2: [
|
||||
{"pair": None, "title": "Англ ебучий"},
|
||||
],
|
||||
3: [
|
||||
{"pair": 1, "title": "Физика (лек), каб 245"},
|
||||
{"pair": 2, "title": "Физика (прак), каб 245"},
|
||||
{"pair": 3, "title": "Маетимака (прак), каб 503"},
|
||||
{"pair": 4, "title": "Безопасность жизнедятельности (прак) каб 503"},
|
||||
{"pair": 5, "title": "Общекультурная и профессиональная коммуникации (прак), каб 503"},
|
||||
],
|
||||
4: [
|
||||
{"pair": 1, "title": "Нету пары"},
|
||||
{"pair": 2, "title": "История России (лек), каб 510"},
|
||||
{"pair": 3, "title": "Основы программирования (лек), каб 510"},
|
||||
{"pair": 4, "title": "Физическая культура спорта (прак), каб 503"},
|
||||
],
|
||||
},
|
||||
"lower": {
|
||||
0: [
|
||||
{"pair": 1, "title": "Пары нету"},
|
||||
{"pair": 2, "title": "История Россия (семинар), каб. 504"},
|
||||
{"pair": 3, "title": "Первая Подгруппа: Основы программирование, каб. 606. Вторая подгруппа Глод."},
|
||||
{"pair": 4, "title": "Первая Подгруппа: Глод. Вторая подгруппа Основы программирование, каб 606"},
|
||||
{"pair": 5, "title": "Общекулутрная и профессиональная коммуникации."},
|
||||
],
|
||||
1: [
|
||||
{"pair": 1, "title": "Нету пары"},
|
||||
{"pair": 2, "title": "Физика (лаб), каб. 241"},
|
||||
{"pair": 3, "title": "Математика (лек), каб 514"},
|
||||
{"pair": 4, "title": "Математика (прак), каб 503"},
|
||||
],
|
||||
2: [
|
||||
{"pair": None, "title": "Англ ебучий"},
|
||||
],
|
||||
3: [
|
||||
{"pair": 1, "title": "Физика (лек), каб 245"},
|
||||
{"pair": 2, "title": "Физика (прак), каб 245"},
|
||||
{"pair": 3, "title": "Маетимака (прак), каб 503"},
|
||||
{"pair": 4, "title": "Безопасность жизнедятельности (прак) каб 503"},
|
||||
{"pair": 5, "title": "Общекультурная и профессиональная коммуникации (прак), каб 503"},
|
||||
],
|
||||
4: [
|
||||
{"pair": 1, "title": "Нету пары"},
|
||||
{"pair": 2, "title": "История России (лек), каб 510"},
|
||||
{"pair": 3, "title": "Основы программирования (лек), каб 510"},
|
||||
{"pair": 4, "title": "Физическая культура спорта (прак), каб 503"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
SCHEDULE = {
|
||||
0: [ # Понедельник
|
||||
(make_time(9, 50), make_time(10, 35), 5),
|
||||
|
|
@ -79,7 +199,7 @@ KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'}
|
|||
# --- Авто-ответы ИИ ---
|
||||
AUTOREPLY_CHANCE = 0.10
|
||||
AUTOREPLY_COOLDOWN = 120
|
||||
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар'}
|
||||
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'ваномас'}
|
||||
|
||||
def get_hint_text(templates_str: str) -> str:
|
||||
return (
|
||||
|
|
@ -87,9 +207,20 @@ def get_hint_text(templates_str: str) -> str:
|
|||
f"• /maket «id» «текст» — доступные: {templates_str}.\n"
|
||||
"• /gen_mem Up:верх Down:низ — отправь с фото.\n"
|
||||
"• /break — узнать время до перекура.\n"
|
||||
"• /schedule [пн|mon] — расписание; без аргумента еще текущая и следующая пара.\n"
|
||||
"• /penis — миниигра 1 раз в 24 часа.\n"
|
||||
"• /top_penis — топ по длине.\n"
|
||||
"• /talk [текст] — ИИ базарит на фене.\n"
|
||||
"• /prices /fetch — цены.\n"
|
||||
"• /penis_casino [ставка] — казино на размер.\n"
|
||||
"• /gadanie [тема] — гадание на фене с мемом.\n"
|
||||
"• /matches [epl|ucl|laliga|bundesliga|seriea|ligue1|europa] — матчи для ставок.\n"
|
||||
"• /bet [лига] [номер] [исход] [ставка] — поставить см.\n"
|
||||
"• /mybets — мои активные ставки.\n"
|
||||
"• /bet_history — история ставок за месяц + статистика.\n"
|
||||
"• /sports_debug — диагностика API матчей.\n"
|
||||
"• /ebalnik — включить/выключить автоответы в чате.\n"
|
||||
"• /uwu [теги] — случайная картинка с e621.\n"
|
||||
"• /polychaetsi — топ по слову «получается».\n"
|
||||
"• /prices /fetch — цены и остатки Магнита.\n"
|
||||
"Подсказка: в группах пиши /команда@username_бота."
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
version: "3.9"
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
volumes:
|
||||
- ./db:/db
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
PENIS_DB_PATH: /db/penis_stats.sqlite3
|
||||
CHAT_HISTORY_DB_PATH: /db/chat_history.sqlite3
|
||||
POLYCHAETSI_STATS_PATH: /db/polychaetsi_stats.json
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
0
games/__init__.py
Normal file
0
games/__init__.py
Normal file
665
games/betting.py
Normal file
665
games/betting.py
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
import time as _time
|
||||
import sqlite3
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import aiohttp
|
||||
|
||||
import config
|
||||
from games.casino import get_user_length, update_user_length
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_matches_cache: dict[str, tuple[float, list]] = {}
|
||||
_sports_cache: tuple[float, list] | None = None
|
||||
_last_viewed_sport_by_user: dict[int, str] = {}
|
||||
_DRAW_NAMES = {"draw", "tie", "ничья"}
|
||||
|
||||
# Сколько дней хранить историю ставок
|
||||
BET_HISTORY_DAYS = 30
|
||||
|
||||
|
||||
def _init_bets_db() -> None:
|
||||
Path(config.BET_DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS bets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
match_id TEXT NOT NULL,
|
||||
sport TEXT NOT NULL,
|
||||
home_team TEXT NOT NULL,
|
||||
away_team TEXT NOT NULL,
|
||||
chosen_team TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
odds REAL NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
payout REAL DEFAULT 0,
|
||||
created_ts INTEGER,
|
||||
resolved_ts INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_user ON bets(user_id, status)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_match ON bets(match_id, status)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_created ON bets(created_ts)")
|
||||
|
||||
# Миграция: добавить столбцы если их нет
|
||||
cursor = conn.execute("PRAGMA table_info(bets)")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
|
||||
if "payout" not in columns:
|
||||
conn.execute("ALTER TABLE bets ADD COLUMN payout REAL DEFAULT 0")
|
||||
if "resolved_ts" not in columns:
|
||||
conn.execute("ALTER TABLE bets ADD COLUMN resolved_ts INTEGER")
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
_init_bets_db()
|
||||
|
||||
|
||||
def remember_user_sport_context(user_id: int, sport_alias: str) -> None:
|
||||
if sport_alias in config.ODDS_SPORTS:
|
||||
_last_viewed_sport_by_user[user_id] = sport_alias
|
||||
|
||||
|
||||
def clear_user_sport_context(user_id: int) -> None:
|
||||
_last_viewed_sport_by_user.pop(user_id, None)
|
||||
|
||||
|
||||
def get_user_sport_context(user_id: int) -> str | None:
|
||||
return _last_viewed_sport_by_user.get(user_id)
|
||||
|
||||
|
||||
async def _fetch_available_sports() -> list[dict]:
|
||||
"""Получить список доступных видов спорта из API."""
|
||||
global _sports_cache
|
||||
now = _time.time()
|
||||
if _sports_cache and (now - _sports_cache[0]) < 3600:
|
||||
return _sports_cache[1]
|
||||
|
||||
if not config.ODDS_API_KEY:
|
||||
return []
|
||||
|
||||
url = f"{config.ODDS_API_BASE}/sports"
|
||||
params = {"apiKey": config.ODDS_API_KEY}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp:
|
||||
if resp.status != 200:
|
||||
body = await resp.text()
|
||||
logger.warning(f"Sports API returned {resp.status}: {body[:200]}")
|
||||
return []
|
||||
data = await resp.json()
|
||||
_sports_cache = (now, data)
|
||||
return data
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch sports list")
|
||||
return []
|
||||
|
||||
|
||||
async def fetch_matches(sport_key: str) -> list[dict]:
|
||||
now = _time.time()
|
||||
cached = _matches_cache.get(sport_key)
|
||||
if cached and (now - cached[0]) < config.ODDS_CACHE_TTL:
|
||||
return cached[1]
|
||||
|
||||
if not config.ODDS_API_KEY:
|
||||
logger.warning("ODDS_API_KEY не задан, матчи недоступны")
|
||||
return []
|
||||
|
||||
url = f"{config.ODDS_API_BASE}/sports/{sport_key}/odds"
|
||||
params = {
|
||||
"apiKey": config.ODDS_API_KEY,
|
||||
"regions": "eu",
|
||||
"markets": "h2h",
|
||||
"oddsFormat": "decimal",
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp:
|
||||
if resp.status != 200:
|
||||
body = await resp.text()
|
||||
logger.warning(f"Odds API returned {resp.status} for {sport_key}: {body[:300]}")
|
||||
return cached[1] if cached else []
|
||||
data = await resp.json()
|
||||
logger.info(f"Odds API for {sport_key}: received {len(data)} events")
|
||||
except Exception:
|
||||
logger.exception(f"Failed to fetch odds for {sport_key}")
|
||||
return cached[1] if cached else []
|
||||
|
||||
if not isinstance(data, list):
|
||||
logger.warning(f"Odds API returned non-list for {sport_key}: {type(data)}")
|
||||
return cached[1] if cached else []
|
||||
|
||||
matches = []
|
||||
for event in data:
|
||||
best_odds = {}
|
||||
for bookmaker in event.get("bookmakers", []):
|
||||
for market in bookmaker.get("markets", []):
|
||||
if market.get("key") == "h2h":
|
||||
for outcome in market.get("outcomes", []):
|
||||
name = outcome.get("name", "")
|
||||
price = outcome.get("price", 0)
|
||||
if name and price and (name not in best_odds or price > best_odds[name]):
|
||||
best_odds[name] = price
|
||||
|
||||
if len(best_odds) >= 2:
|
||||
matches.append({
|
||||
"id": event.get("id", ""),
|
||||
"home": event.get("home_team", ""),
|
||||
"away": event.get("away_team", ""),
|
||||
"commence": event.get("commence_time", ""),
|
||||
"odds": best_odds,
|
||||
})
|
||||
|
||||
_matches_cache[sport_key] = (now, matches)
|
||||
logger.info(f"Parsed {len(matches)} matches for {sport_key}")
|
||||
return matches
|
||||
|
||||
|
||||
def _get_match_outcomes(match: dict) -> list[dict[str, str | float]]:
|
||||
odds = match.get("odds", {})
|
||||
outcomes: list[dict[str, str | float]] = []
|
||||
added: set[str] = set()
|
||||
|
||||
def append_outcome(code: str, name: str) -> None:
|
||||
if name in odds and name not in added:
|
||||
outcomes.append({"code": code, "name": name, "odds": odds[name]})
|
||||
added.add(name)
|
||||
|
||||
home = match.get("home", "")
|
||||
away = match.get("away", "")
|
||||
append_outcome("1", home)
|
||||
|
||||
draw_name = next((name for name in odds if name.casefold() in _DRAW_NAMES), None)
|
||||
if draw_name:
|
||||
append_outcome("X", draw_name)
|
||||
|
||||
append_outcome("2", away)
|
||||
|
||||
next_code = 3
|
||||
for name in odds:
|
||||
if name in added:
|
||||
continue
|
||||
append_outcome(str(next_code), name)
|
||||
next_code += 1
|
||||
|
||||
return outcomes
|
||||
|
||||
|
||||
def describe_match_outcomes(match: dict) -> str:
|
||||
return ", ".join(
|
||||
f"{item['code']}={item['name']} x{item['odds']:.2f}"
|
||||
for item in _get_match_outcomes(match)
|
||||
)
|
||||
|
||||
|
||||
def resolve_match_outcome(match: dict, raw_choice: str) -> str | None:
|
||||
choice = raw_choice.strip()
|
||||
if not choice:
|
||||
return None
|
||||
|
||||
normalized = choice.casefold()
|
||||
outcomes = _get_match_outcomes(match)
|
||||
|
||||
for item in outcomes:
|
||||
if normalized == str(item["code"]).casefold():
|
||||
return str(item["name"])
|
||||
|
||||
if normalized in _DRAW_NAMES or normalized == "x":
|
||||
for item in outcomes:
|
||||
if str(item["code"]).casefold() == "x":
|
||||
return str(item["name"])
|
||||
|
||||
for item in outcomes:
|
||||
if normalized == str(item["name"]).casefold():
|
||||
return str(item["name"])
|
||||
|
||||
try:
|
||||
choice_odds = round(float(choice.replace(",", ".")), 2)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
for item in outcomes:
|
||||
if round(float(item["odds"]), 2) == choice_odds:
|
||||
return str(item["name"])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def format_matches(matches: list[dict], sport_alias: str, sport_label: str) -> str:
|
||||
if not matches:
|
||||
return f"🏟 {sport_label} [{sport_alias}] — матчей не найдено."
|
||||
|
||||
lines = [f"🏟 {sport_label} [{sport_alias}] — ближайшие матчи:\n"]
|
||||
for i, m in enumerate(matches[:10], 1):
|
||||
outcomes = _get_match_outcomes(m)
|
||||
odds_str = "\n".join(
|
||||
f" {item['code']}) {item['name']}: {item['odds']:.2f}"
|
||||
for item in outcomes
|
||||
)
|
||||
|
||||
start_str = ""
|
||||
if m["commence"]:
|
||||
try:
|
||||
dt = datetime.fromisoformat(m["commence"].replace("Z", "+00:00"))
|
||||
msk = dt.astimezone(ZoneInfo(config.DEFAULT_TZ))
|
||||
start_str = f"\n 🕐 {msk.strftime('%d.%m %H:%M')} МСК"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
lines.append(f"{i}️⃣ {m['home']} vs {m['away']}{start_str}\n{odds_str}\n")
|
||||
|
||||
lines.append(f"Ставка: /bet {sport_alias} [номер] [исход] [ставка]")
|
||||
lines.append(f"Коротко после /matches {sport_alias}: /bet [номер] [исход] [ставка]")
|
||||
lines.append("Исход: 1/2, для ничьи X. Можно писать ещё название команды или коэффициент.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _get_sport_label(key: str) -> str:
|
||||
labels = {
|
||||
"epl": "⚽ EPL",
|
||||
"ucl": "⚽ Champions League",
|
||||
"laliga": "⚽ La Liga",
|
||||
"bundesliga": "⚽ Bundesliga",
|
||||
"seriea": "⚽ Serie A",
|
||||
"ligue1": "⚽ Ligue 1",
|
||||
"europa": "⚽ Europa League",
|
||||
}
|
||||
return labels.get(key, key)
|
||||
|
||||
|
||||
async def get_formatted_matches(sport_alias: str | None) -> str:
|
||||
if not config.ODDS_API_KEY:
|
||||
return "⚠️ ODDS_API_KEY не задан. Ставки недоступны."
|
||||
|
||||
if sport_alias and sport_alias in config.ODDS_SPORTS:
|
||||
sport_key = config.ODDS_SPORTS[sport_alias]
|
||||
matches = await fetch_matches(sport_key)
|
||||
if not matches:
|
||||
# Попробуем проверить, существует ли вообще этот спорт
|
||||
sports = await _fetch_available_sports()
|
||||
valid_keys = {s.get("key") for s in sports}
|
||||
if sport_key not in valid_keys:
|
||||
# Ищем похожий ключ
|
||||
suggestions = [s for s in sports if sport_alias in s.get("key", "").lower()
|
||||
or sport_alias in s.get("title", "").lower()]
|
||||
if suggestions:
|
||||
suggest_text = "\n".join(
|
||||
f" • {s['key']} — {s.get('title', '')}"
|
||||
for s in suggestions[:5]
|
||||
)
|
||||
return (
|
||||
f"⚠️ Ключ {sport_key} не найден в API.\n"
|
||||
f"Похожие виды спорта:\n{suggest_text}\n\n"
|
||||
f"Обнови sport keys в config.py"
|
||||
)
|
||||
return (
|
||||
f"⚠️ Ключ {sport_key} не найден в API. "
|
||||
f"Проверь ODDS_SPORTS в config.py"
|
||||
)
|
||||
return format_matches(matches, sport_alias, _get_sport_label(sport_alias))
|
||||
|
||||
parts = []
|
||||
for alias, sport_key in config.ODDS_SPORTS.items():
|
||||
matches = await fetch_matches(sport_key)
|
||||
parts.append(format_matches(matches, alias, _get_sport_label(alias)))
|
||||
|
||||
result = "\n\n".join(parts)
|
||||
if all("матчей не найдено" in p for p in parts):
|
||||
result += "\n\n💡 Если матчей нет — возможно, sport keys устарели. Проверь /sports_debug"
|
||||
return result
|
||||
|
||||
|
||||
async def get_match_by_index(sport_alias: str, index: int) -> dict | None:
|
||||
sport_key = config.ODDS_SPORTS.get(sport_alias)
|
||||
if not sport_key:
|
||||
return None
|
||||
matches = await fetch_matches(sport_key)
|
||||
if 0 < index <= len(matches):
|
||||
return matches[index - 1]
|
||||
return None
|
||||
|
||||
|
||||
def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: float) -> str:
|
||||
current = get_user_length(user_id)
|
||||
if current is None:
|
||||
return "Сначала заведи счёт через /penis, братуха."
|
||||
|
||||
if current <= 0:
|
||||
return "🚫 С кредитом ставки не принимаем."
|
||||
|
||||
if amount <= 0:
|
||||
return "Ставка должна быть больше нуля."
|
||||
|
||||
if amount > config.BET_MAX_AMOUNT:
|
||||
return f"Максимальная ставка — {config.BET_MAX_AMOUNT} см."
|
||||
|
||||
if amount > current:
|
||||
return f"У тебя {current:.1f} см, а ставишь {amount:.1f}. Не хватает, фраер."
|
||||
|
||||
team_odds = match["odds"].get(chosen_team)
|
||||
if team_odds is None:
|
||||
return f"Исход не найден. Доступные: {describe_match_outcomes(match)}"
|
||||
|
||||
new_length = update_user_length(user_id, -amount)
|
||||
if new_length is None:
|
||||
return "Ошибка БД."
|
||||
|
||||
now_ts = int(_time.time())
|
||||
try:
|
||||
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO bets (user_id, match_id, sport, home_team, away_team,
|
||||
chosen_team, amount, odds, status, payout, created_ts)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)""",
|
||||
(user_id, match["id"], sport, match["home"], match["away"],
|
||||
chosen_team, amount, team_odds, now_ts),
|
||||
)
|
||||
conn.commit()
|
||||
except sqlite3.Error:
|
||||
logger.exception("Failed to place bet")
|
||||
update_user_length(user_id, amount)
|
||||
return "Ошибка БД."
|
||||
|
||||
potential = round(amount * team_odds, 1)
|
||||
return (
|
||||
f"✅ Ставка принята!\n"
|
||||
f"🏟 {match['home']} vs {match['away']}\n"
|
||||
f"📌 {chosen_team} (x{team_odds:.2f})\n"
|
||||
f"💰 Ставка: {amount:.1f} см\n"
|
||||
f"🎯 Возможный выигрыш: {potential:.1f} см\n"
|
||||
f"📏 Остаток: {new_length:.1f} см"
|
||||
)
|
||||
|
||||
|
||||
def get_user_bets(user_id: int) -> str:
|
||||
try:
|
||||
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM bets WHERE user_id = ? AND status = 'pending' ORDER BY created_ts DESC LIMIT 10",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
except sqlite3.Error:
|
||||
return "Ошибка БД."
|
||||
|
||||
if not rows:
|
||||
return "У тебя нет активных ставок."
|
||||
|
||||
lines = ["📋 Твои активные ставки:\n"]
|
||||
for r in rows:
|
||||
potential = round(r["amount"] * r["odds"], 1)
|
||||
created = ""
|
||||
if r["created_ts"]:
|
||||
dt = datetime.fromtimestamp(r["created_ts"], tz=ZoneInfo(config.DEFAULT_TZ))
|
||||
created = f" 📅 {dt.strftime('%d.%m %H:%M')}\n"
|
||||
lines.append(
|
||||
f"• {r['home_team']} vs {r['away_team']}\n"
|
||||
f" {r['chosen_team']} (x{r['odds']:.2f}), {r['amount']:.1f} см → {potential:.1f} см\n"
|
||||
f"{created}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def get_user_bet_history(user_id: int) -> str:
|
||||
"""Полная история ставок за последний месяц."""
|
||||
cutoff_ts = int((_time.time()) - BET_HISTORY_DAYS * 86400)
|
||||
|
||||
try:
|
||||
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
rows = conn.execute(
|
||||
"""SELECT * FROM bets
|
||||
WHERE user_id = ? AND created_ts >= ?
|
||||
ORDER BY created_ts DESC
|
||||
LIMIT 50""",
|
||||
(user_id, cutoff_ts),
|
||||
).fetchall()
|
||||
except sqlite3.Error:
|
||||
logger.exception("Failed to get bet history")
|
||||
return "Ошибка БД."
|
||||
|
||||
if not rows:
|
||||
return "📋 За последний месяц ставок не было."
|
||||
|
||||
# Считаем статистику
|
||||
total_bet = 0.0
|
||||
total_won = 0.0
|
||||
total_lost = 0.0
|
||||
wins = 0
|
||||
losses = 0
|
||||
pending = 0
|
||||
|
||||
lines = [f"📋 История ставок за {BET_HISTORY_DAYS} дней:\n"]
|
||||
|
||||
for r in rows:
|
||||
status = r["status"]
|
||||
amount = float(r["amount"])
|
||||
odds_val = float(r["odds"])
|
||||
payout_raw = float(r["payout"] or 0)
|
||||
|
||||
total_bet += amount
|
||||
|
||||
if status == "won":
|
||||
wins += 1
|
||||
# Для старых ставок payout может быть 0, пересчитываем
|
||||
actual_payout = payout_raw if payout_raw > 0 else round(amount * odds_val, 1)
|
||||
total_won += actual_payout
|
||||
status_icon = "✅"
|
||||
result_text = f"+{actual_payout:.1f} см"
|
||||
elif status == "lost":
|
||||
losses += 1
|
||||
total_lost += amount
|
||||
status_icon = "❌"
|
||||
result_text = f"-{amount:.1f} см"
|
||||
else:
|
||||
pending += 1
|
||||
status_icon = "⏳"
|
||||
potential = round(amount * odds_val, 1)
|
||||
result_text = f"ожидание → {potential:.1f} см"
|
||||
|
||||
created = ""
|
||||
if r["created_ts"]:
|
||||
dt = datetime.fromtimestamp(r["created_ts"], tz=ZoneInfo(config.DEFAULT_TZ))
|
||||
created = dt.strftime('%d.%m %H:%M')
|
||||
|
||||
lines.append(
|
||||
f"{status_icon} {r['home_team']} vs {r['away_team']}\n"
|
||||
f" {r['chosen_team']} (x{odds_val:.2f}) | {amount:.1f} см | {result_text}\n"
|
||||
f" 📅 {created}"
|
||||
)
|
||||
|
||||
# Итоговая статистика
|
||||
net = total_won - total_lost
|
||||
net_sign = "+" if net >= 0 else ""
|
||||
|
||||
lines.append("")
|
||||
lines.append("━━━ 📊 СТАТИСТИКА ━━━")
|
||||
lines.append(f"🎰 Всего ставок: {len(rows)}")
|
||||
lines.append(f"✅ Побед: {wins}")
|
||||
lines.append(f"❌ Поражений: {losses}")
|
||||
if pending:
|
||||
lines.append(f"⏳ В ожидании: {pending}")
|
||||
lines.append(f"💰 Поставлено: {total_bet:.1f} см")
|
||||
lines.append(f"🏆 Выиграно: +{total_won:.1f} см")
|
||||
lines.append(f"💸 Проиграно: -{total_lost:.1f} см")
|
||||
lines.append(f"📈 Итого: {net_sign}{net:.1f} см")
|
||||
|
||||
if wins + losses > 0:
|
||||
winrate = wins / (wins + losses) * 100
|
||||
lines.append(f"📊 Винрейт: {winrate:.0f}%")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def cleanup_old_bets() -> int:
|
||||
"""Удалить завершённые ставки старше BET_HISTORY_DAYS дней."""
|
||||
cutoff_ts = int(_time.time() - BET_HISTORY_DAYS * 86400)
|
||||
try:
|
||||
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||
cursor = conn.execute(
|
||||
"DELETE FROM bets WHERE status != 'pending' AND created_ts < ?",
|
||||
(cutoff_ts,),
|
||||
)
|
||||
conn.commit()
|
||||
deleted = cursor.rowcount
|
||||
if deleted > 0:
|
||||
logger.info(f"Cleaned up {deleted} old bets")
|
||||
return deleted
|
||||
except sqlite3.Error:
|
||||
logger.exception("Failed to cleanup old bets")
|
||||
return 0
|
||||
|
||||
|
||||
async def settle_bets() -> list[str]:
|
||||
notifications = []
|
||||
try:
|
||||
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
pending = conn.execute(
|
||||
"SELECT DISTINCT match_id, sport FROM bets WHERE status = 'pending'"
|
||||
).fetchall()
|
||||
except sqlite3.Error:
|
||||
return notifications
|
||||
|
||||
if not pending:
|
||||
return notifications
|
||||
|
||||
now_ts = int(_time.time())
|
||||
|
||||
for row in pending:
|
||||
match_id = row["match_id"]
|
||||
sport = row["sport"]
|
||||
sport_key = config.ODDS_SPORTS.get(sport, sport)
|
||||
|
||||
try:
|
||||
url = f"{config.ODDS_API_BASE}/sports/{sport_key}/scores"
|
||||
params = {"apiKey": config.ODDS_API_KEY, "daysFrom": 3}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp:
|
||||
if resp.status != 200:
|
||||
logger.warning(f"Scores API returned {resp.status} for {sport_key}")
|
||||
continue
|
||||
scores_data = await resp.json()
|
||||
except Exception:
|
||||
logger.exception(f"Failed to fetch scores for {sport_key}")
|
||||
continue
|
||||
|
||||
for event in scores_data:
|
||||
if event.get("id") != match_id:
|
||||
continue
|
||||
if not event.get("completed"):
|
||||
continue
|
||||
|
||||
scores = event.get("scores")
|
||||
if not scores:
|
||||
continue
|
||||
|
||||
winner = None
|
||||
max_score = -1
|
||||
is_draw = False
|
||||
|
||||
score_values = []
|
||||
for s in scores:
|
||||
score_val = int(s.get("score", 0))
|
||||
score_values.append((s.get("name", ""), score_val))
|
||||
|
||||
if len(score_values) >= 2 and score_values[0][1] == score_values[1][1]:
|
||||
is_draw = True
|
||||
# Для ничьи ищем "Draw" среди ставок
|
||||
winner = "Draw"
|
||||
else:
|
||||
for name, score_val in score_values:
|
||||
if score_val > max_score:
|
||||
max_score = score_val
|
||||
winner = name
|
||||
|
||||
if not winner:
|
||||
continue
|
||||
|
||||
try:
|
||||
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
bets = conn.execute(
|
||||
"SELECT * FROM bets WHERE match_id = ? AND status = 'pending'",
|
||||
(match_id,),
|
||||
).fetchall()
|
||||
|
||||
for bet in bets:
|
||||
chosen = bet["chosen_team"]
|
||||
bet_won = False
|
||||
|
||||
if is_draw:
|
||||
# Ставка на ничью выигрывает
|
||||
bet_won = chosen.casefold() in _DRAW_NAMES or chosen == "Draw"
|
||||
else:
|
||||
bet_won = chosen == winner
|
||||
|
||||
if bet_won:
|
||||
winnings = round(bet["amount"] * bet["odds"], 1)
|
||||
update_user_length(bet["user_id"], winnings)
|
||||
conn.execute(
|
||||
"UPDATE bets SET status = 'won', payout = ?, resolved_ts = ? WHERE id = ?",
|
||||
(winnings, now_ts, bet["id"]),
|
||||
)
|
||||
notifications.append(
|
||||
f"🎉 user_id={bet['user_id']}: выиграл {winnings:.1f} см "
|
||||
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})"
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE bets SET status = 'lost', payout = 0, resolved_ts = ? WHERE id = ?",
|
||||
(now_ts, bet["id"]),
|
||||
)
|
||||
notifications.append(
|
||||
f"❌ user_id={bet['user_id']}: проиграл {bet['amount']:.1f} см "
|
||||
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})"
|
||||
)
|
||||
conn.commit()
|
||||
except sqlite3.Error:
|
||||
logger.exception("Failed to settle bets")
|
||||
|
||||
# Очистка старых ставок
|
||||
cleanup_old_bets()
|
||||
|
||||
return notifications
|
||||
|
||||
|
||||
async def debug_sports() -> str:
|
||||
"""Диагностика: показать доступные виды спорта из API."""
|
||||
if not config.ODDS_API_KEY:
|
||||
return "⚠️ ODDS_API_KEY не задан."
|
||||
|
||||
sports = await _fetch_available_sports()
|
||||
if not sports:
|
||||
return "❌ Не удалось получить список спортов от API."
|
||||
|
||||
# Показываем текущие ключи и проверяем их
|
||||
current_keys = set(config.ODDS_SPORTS.values())
|
||||
valid_keys = {s.get("key") for s in sports}
|
||||
|
||||
lines = ["🔍 Диагностика sport keys:\n"]
|
||||
|
||||
for alias, key in config.ODDS_SPORTS.items():
|
||||
status = "✅" if key in valid_keys else "❌ НЕ НАЙДЕН"
|
||||
lines.append(f" {alias} → {key} {status}")
|
||||
|
||||
# Показать доступные esports и football
|
||||
lines.append("\n📋 Доступные eSports и футбол:")
|
||||
for s in sports:
|
||||
key = s.get("key", "")
|
||||
title = s.get("title", "")
|
||||
active = s.get("active", False)
|
||||
if active and ("esport" in key.lower() or "soccer" in key.lower() or "football" in key.lower()):
|
||||
marker = " ← ИСПОЛЬЗУЕТСЯ" if key in current_keys else ""
|
||||
lines.append(f" • {key} — {title}{marker}")
|
||||
|
||||
return "\n".join(lines)
|
||||
127
games/casino.py
Normal file
127
games/casino.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import random
|
||||
import sqlite3
|
||||
import logging
|
||||
|
||||
import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Символы слотов и их веса (чем выше вес — тем чаще выпадает)
|
||||
SLOT_SYMBOLS = ["🍒", "🍋", "🔔", "💎", "7️⃣", "🍀"]
|
||||
SLOT_WEIGHTS = [30, 25, 20, 12, 8, 5] # 🍒 чаще всего, 🍀 реже всего
|
||||
|
||||
# Множители для выигрышей: {кол-во совпадений: (мин, макс)}
|
||||
MULTIPLIERS = {
|
||||
2: (1.2, 1.8), # две одинаковых
|
||||
3: (2.0, 3.0), # три одинаковых (джекпот, редко)
|
||||
}
|
||||
|
||||
CASINO_WIN_CHANCE = 0.35
|
||||
|
||||
|
||||
def spin_slots() -> tuple[list[str], int]:
|
||||
reels = random.choices(SLOT_SYMBOLS, weights=SLOT_WEIGHTS, k=3)
|
||||
|
||||
if random.random() > CASINO_WIN_CHANCE:
|
||||
while len(set(reels)) < 3:
|
||||
reels = random.choices(SLOT_SYMBOLS, weights=SLOT_WEIGHTS, k=3)
|
||||
return reels, 0
|
||||
|
||||
counts = {}
|
||||
for s in reels:
|
||||
counts[s] = counts.get(s, 0) + 1
|
||||
max_match = max(counts.values())
|
||||
return reels, max_match
|
||||
|
||||
|
||||
def get_user_length(user_id: int) -> float | None:
|
||||
try:
|
||||
with sqlite3.connect(config.PENIS_DB_PATH) as conn:
|
||||
row = conn.execute(
|
||||
"SELECT length FROM penis_stats WHERE user_id = ?",
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
return float(row[0]) if row else None
|
||||
except sqlite3.Error:
|
||||
logger.exception("casino: failed to get length")
|
||||
return None
|
||||
|
||||
|
||||
def update_user_length(user_id: int, delta: float) -> float | None:
|
||||
try:
|
||||
with sqlite3.connect(config.PENIS_DB_PATH) as conn:
|
||||
row = conn.execute(
|
||||
"SELECT length FROM penis_stats WHERE user_id = ?",
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
new_length = round(float(row[0]) + delta, 1)
|
||||
conn.execute(
|
||||
"UPDATE penis_stats SET length = ? WHERE user_id = ?",
|
||||
(new_length, user_id),
|
||||
)
|
||||
conn.commit()
|
||||
return new_length
|
||||
except sqlite3.Error:
|
||||
logger.exception("casino: failed to update length")
|
||||
return None
|
||||
|
||||
|
||||
def play_casino(user_id: int, bet: float) -> str:
|
||||
current = get_user_length(user_id)
|
||||
if current is None:
|
||||
return "Ты ещё ни разу не крутил /penis, братуха. Сначала заведи счёт."
|
||||
|
||||
# Проверяем отрицательный баланс
|
||||
if current <= 0:
|
||||
return "🚫 С кредитом в казино не впускают, братуха. Иди наращивай через /penis."
|
||||
|
||||
# Проверяем максимум ставки
|
||||
if bet > config.CASINO_MAX_BET:
|
||||
return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай."
|
||||
|
||||
if bet <= 0:
|
||||
return "Ставка должна быть больше нуля, фраер."
|
||||
|
||||
# Проверяем хватает ли
|
||||
if bet > current:
|
||||
return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер."
|
||||
|
||||
# Крутим
|
||||
reels, matches = spin_slots()
|
||||
slots_display = " | ".join(reels)
|
||||
|
||||
if matches >= 2:
|
||||
min_mult, max_mult = MULTIPLIERS[matches]
|
||||
multiplier = round(random.uniform(min_mult, max_mult), 1)
|
||||
winnings = round(bet * multiplier, 1)
|
||||
new_length = update_user_length(user_id, winnings)
|
||||
if new_length is None:
|
||||
return "Ошибка БД. Попробуй позже."
|
||||
|
||||
if matches == 3:
|
||||
return (
|
||||
f"🎰 {slots_display}\n\n"
|
||||
f"🎉 ДЖЕКПОТ!!! Три одинаковых!\n"
|
||||
f"Множитель: x{multiplier}\n"
|
||||
f"Выигрыш: +{winnings:.1f} см\n"
|
||||
f"📏 Текущий размер: {new_length:.1f} см"
|
||||
)
|
||||
return (
|
||||
f"🎰 {slots_display}\n\n"
|
||||
f"✅ Выигрыш! Две совпали.\n"
|
||||
f"Множитель: x{multiplier}\n"
|
||||
f"Выигрыш: +{winnings:.1f} см\n"
|
||||
f"📏 Текущий размер: {new_length:.1f} см"
|
||||
)
|
||||
else:
|
||||
new_length = update_user_length(user_id, -bet)
|
||||
if new_length is None:
|
||||
return "Ошибка БД. Попробуй позже."
|
||||
return (
|
||||
f"🎰 {slots_display}\n\n"
|
||||
f"❌ Мимо, братуха.\n"
|
||||
f"Проигрыш: -{bet:.1f} см\n"
|
||||
f"📏 Текущий размер: {new_length:.1f} см"
|
||||
)
|
||||
57
games/fortune.py
Normal file
57
games/fortune.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import logging
|
||||
import aiohttp
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-ai/")
|
||||
|
||||
FORTUNE_PROMPT = (
|
||||
"Ты — уличный гадатель с района, базаришь на фене. "
|
||||
"Пользователь хочет погадать на тему. Выдай шуточное гадание, "
|
||||
"коротко и дерзко, 2-3 предложения максимум. "
|
||||
"В конце ОБЯЗАТЕЛЬНО напиши на отдельной строке в формате:\n"
|
||||
"MEM_UP: <текст сверху для мема, максимум 4 слова>\n"
|
||||
"MEM_DOWN: <текст снизу для мема, максимум 4 слова>\n"
|
||||
"Тексты для мема должны быть смешными и короткими."
|
||||
)
|
||||
|
||||
|
||||
async def generate_fortune(topic: str) -> tuple[str, str, str]:
|
||||
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
|
||||
payload = {
|
||||
"messages": [
|
||||
{"role": "system", "content": FORTUNE_PROMPT},
|
||||
{"role": "user", "content": f"Погадай мне на: {topic}"},
|
||||
],
|
||||
"max_tokens": 200,
|
||||
"temperature": 0.9,
|
||||
"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()
|
||||
text = data["choices"][0]["message"]["content"].strip()
|
||||
|
||||
up_text = ""
|
||||
down_text = ""
|
||||
fortune_lines = []
|
||||
|
||||
for line in text.split("\n"):
|
||||
stripped = line.strip()
|
||||
if stripped.upper().startswith("MEM_UP:"):
|
||||
up_text = stripped.split(":", 1)[1].strip()
|
||||
elif stripped.upper().startswith("MEM_DOWN:"):
|
||||
down_text = stripped.split(":", 1)[1].strip()
|
||||
else:
|
||||
fortune_lines.append(line)
|
||||
|
||||
fortune_text = "\n".join(fortune_lines).strip()
|
||||
|
||||
if not up_text:
|
||||
up_text = "Гадание"
|
||||
if not down_text:
|
||||
down_text = "по фене"
|
||||
|
||||
return fortune_text, up_text, down_text
|
||||
87
games/uwu.py
Normal file
87
games/uwu.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import aiohttp
|
||||
from aiogram import types
|
||||
import config
|
||||
import logging
|
||||
import random
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CAPTIONS = [
|
||||
"Ня :3",
|
||||
"uwu"
|
||||
]
|
||||
|
||||
async def handle_uwu_cmd(message: types.Message):
|
||||
args = message.text.split(maxsplit=1)
|
||||
|
||||
if len(args) > 1 and args[1].strip() == "-h":
|
||||
help_msg = (
|
||||
"🐈 <b>Справка по тегам e621:</b>\n\n"
|
||||
"Перечислите любые теги через пробел после команды.\n"
|
||||
"• <b>rating</b>: <code>rating:safe</code> (безопасные), <code>rating:questionable</code>, <code>rating:explicit</code> (NSFW)\n"
|
||||
"• <b>score</b>: <code>score:>500</code> (искать популярные посты с рейтингом больше 500)\n"
|
||||
"• Исключить тег: поставьте минус, например <code>-fox</code>\n\n"
|
||||
"<i>Пример:</i> <code>/uwu wolf -rating:explicit score:>100</code>\n"
|
||||
"<i>По умолчанию:</i> <code>rating:safe score:>500 -animated</code>"
|
||||
)
|
||||
await message.reply(help_msg, parse_mode="HTML")
|
||||
return
|
||||
|
||||
tags = "rating:safe score:>500 -animated"
|
||||
if len(args) > 1:
|
||||
tags = args[1].strip()
|
||||
|
||||
tags += " order:random"
|
||||
|
||||
url = "https://e621.net/posts.json"
|
||||
params = {
|
||||
"tags": tags,
|
||||
"limit": 1
|
||||
}
|
||||
|
||||
auth = None
|
||||
if config.E621_LOGIN and config.E621_API_KEY:
|
||||
auth = aiohttp.BasicAuth(config.E621_LOGIN, config.E621_API_KEY)
|
||||
|
||||
headers = {
|
||||
"User-Agent": f"Sex Bomba TG Bot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)"
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params, headers=headers, auth=auth) as resp:
|
||||
if resp.status != 200:
|
||||
logger.error(f"e621 API Error: {resp.status}")
|
||||
await message.reply("Не удалось получить картинку от сервера :(")
|
||||
return
|
||||
data = await resp.json()
|
||||
|
||||
posts = data.get("posts", [])
|
||||
if not posts:
|
||||
await message.reply("Ничего не найдено по этим тегам :(")
|
||||
return
|
||||
|
||||
post = posts[0]
|
||||
file_url = post.get("file", {}).get("url")
|
||||
ext = post.get("file", {}).get("ext", "png")
|
||||
if not file_url:
|
||||
await message.reply("У найденного поста нет прямого URL изображения :(")
|
||||
return
|
||||
|
||||
caption = random.choice(CAPTIONS)
|
||||
if ext in ("gif", "webm", "mp4"):
|
||||
# Для анимаций: отправляем caption отдельным сообщением,
|
||||
# а саму гифку/видео через URL напрямую — так Telegram
|
||||
# корректно показывает её как анимацию, а не как файл
|
||||
await message.answer(caption)
|
||||
await message.answer_animation(animation=file_url)
|
||||
elif ext in ("png", "jpg", "jpeg", "webp"):
|
||||
input_file = types.URLInputFile(file_url)
|
||||
await message.answer_photo(photo=input_file, caption=caption)
|
||||
else:
|
||||
input_file = types.URLInputFile(file_url)
|
||||
await message.answer_document(document=input_file, caption=caption)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error in uwu command")
|
||||
await message.reply("Произошла ошибка при обращении к API :(")
|
||||
BIN
maket/2.png
Normal file
BIN
maket/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 701 KiB |
BIN
maket/cvetok.jpg
Normal file
BIN
maket/cvetok.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
maket/vape.jpg
Normal file
BIN
maket/vape.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
|
|
@ -1,6 +1,8 @@
|
|||
aiogram>=3.4.1
|
||||
pillow>=10.3.0
|
||||
requests>=2.31.0
|
||||
aiohttp>=3.9.5
|
||||
tzdata>=2024.1
|
||||
beautifulsoup4
|
||||
lxml
|
||||
pysqlite3-binary>=0.5.3
|
||||
|
|
|
|||
Loading…
Reference in a new issue