1
0
Fork 0
forked from zovos/bot_tg

Compare commits

...

8 commits

Author SHA1 Message Date
Danil
aee53349b1 Добавлены ставки на матчи 2026-03-19 23:49:25 +03:00
q
a7c047f847 modern 2026-03-18 09:39:32 +03:00
q
9295538f5f Add lesson schedule command 2026-03-17 13:29:10 +03:00
q
5b294415d0 Add polychaetsi tracker and improve Docker setup 2026-03-10 13:08:04 +03:00
q
43503dc778 Send all Magnit price matches 2026-03-09 11:51:43 +03:00
q
75a63bb6fc Add Magnit prices command 2026-03-09 11:48:20 +03:00
q
682fc7422c fix dockerfile 2026-03-07 16:09:44 +03:00
q
373aa8f6c2 Merge pull request 'feature/FenyaBot' (#12) from Dan4ick/bot_tg:feature/FenyaBot into main
Reviewed-on: zovos/bot_tg#12
2026-03-07 13:06:19 +00:00
10 changed files with 1085 additions and 78 deletions

View file

@ -5,3 +5,4 @@ __pycache__
.idea .idea
.vscode .vscode
.DS_Store .DS_Store
db

1
.env
View file

@ -1,2 +1,3 @@
BOT_TOKEN=8488665890:AAH7FY5I2xzHhfOdxlzb11ewl2I-MMTH2-o BOT_TOKEN=8488665890:AAH7FY5I2xzHhfOdxlzb11ewl2I-MMTH2-o
TZ=Europe/Moscow TZ=Europe/Moscow
ODDS_API_KEY=c29bc26dee8aa9d95a5ce8d14ea63923

View file

@ -1,9 +1,15 @@
FROM python:3.11-slim FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app WORKDIR /app
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN python -m pip install --upgrade pip \
COPY main.py config.py impact.ttf zparser.py ./ && python -m pip install -r requirements.txt
COPY maket ./maket
COPY AI ./AI COPY . .
RUN mkdir -p /db
CMD ["python", "main.py"] CMD ["python", "main.py"]

View file

@ -4,20 +4,41 @@
- вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`); - вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`);
- делает мемы из присланных фото (`/gen_mem Up:... Down:...`); - делает мемы из присланных фото (`/gen_mem Up:... Down:...`);
- подсказывает, сколько минут до перекура по заданному расписанию (`/break`); - подсказывает, сколько минут до перекура по заданному расписанию (`/break`);
- показывает расписание, текущую и следующую пару (`/schedule`, можно с днём недели);
- умеет базарить (`/talk`) с контекстом 100 сообщений; - умеет базарить (`/talk`) с контекстом 100 сообщений;
- авто-ответы в чате (10% шанс + триггер-слова); - авто-ответы в чате (10% шанс + триггер-слова);
- миниигра `/penis` + `/top_penis`; - миниигра `/penis` + `/top_penis`;
- казино на размер (`/penis_casino`); - казино на размер (`/penis_casino`);
- гадание на фене с мемом (`/gadanie`). - гадание на фене с мемом (`/gadanie`);
- ставки на матчи CS2, Dota 2, футбол с реальными коэффициентами (`/matches`, `/bet`).
## Запуск ## Запуск
```bash ```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`
## Команды ## Команды
@ -26,12 +47,17 @@ docker-compose up --build
- `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240). - `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240).
- `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down. - `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down.
- `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур. - `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур.
- `/schedule [пн|mon]` — показывает расписание на ближайший указанный день недели; без аргумента показывает сегодня, текущую и следующую пару.
- `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений). - `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений).
- `/penis` — миниигра, 1 раз в 24 часа. - `/penis` — миниигра, 1 раз в 24 часа.
- `/top_penis` — топ по длине. - `/top_penis` — топ по длине.
- `/polychaetsi` — топ по слову «получается».
- `/penis_casino <ставка>` — казино на размер (макс 1 см, казино всегда в выигрыше). - `/penis_casino <ставка>` — казино на размер (макс 1 см, казино всегда в выигрыше).
- `/gadanie <тема>` — ИИ-гадание на фене + авто-мем. - `/gadanie <тема>` — ИИ-гадание на фене + авто-мем.
- `/ebalnik` — вкл/выкл авто-ответы бота в чате. - `/ebalnik` — вкл/выкл авто-ответы бота в чате.
- `/matches [cs|dota|football]` — ближайшие матчи с коэффициентами.
- `/bet <спорт> <номер> <команда> <ставка>` — поставить см на матч.
- `/mybets` — мои активные ставки.
- `/svodka` — сводка СВО. - `/svodka` — сводка СВО.
## Макеты и шрифт ## Макеты и шрифт
@ -45,14 +71,22 @@ docker-compose up --build
- llama.cpp для ИИ-фич. - llama.cpp для ИИ-фич.
- Dockerfile + docker-compose для развёртывания. - Dockerfile + docker-compose для развёртывания.
## Docker Notes
- Образ ставит `tzdata`, поэтому `ZoneInfo("Europe/Moscow")` нормально работает в контейнере.
- В образ копируется весь проект, а runtime-данные исключены через `.dockerignore`.
- Если меняешь код или картинки, достаточно пересобрать: `docker compose up --build -d`.
## Кратко о логике ## Кратко о логике
- `/maket` подбирает размер текста, чтобы уместить его в область макета. - `/maket` подбирает размер текста, чтобы уместить его в область макета.
- `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой. - `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой.
- `/break` считает по расписанию в `config.py`. - `/break` считает по расписанию в `config.py` и показывает следующую пару.
- Названия пар и верхняя/нижняя неделя лежат в `config.py` в `LESSON_SCHEDULE`.
- `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite). - `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite).
- `/penis_casino` — слоты с множителем x1.2x3 (`games/casino.py`). - `/penis_casino` — слоты с множителем x1.2x3 (`games/casino.py`).
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`). - `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).
- `/matches` + `/bet` — ставки на реальные матчи, коэффициенты через The-Odds-API (`games/betting.py`).
## ИИ-модуль (`AI/talk_handler.py`) ## ИИ-модуль (`AI/talk_handler.py`)

109
config.py
View file

@ -18,9 +18,36 @@ PENIS_COOLDOWN = timedelta(hours=24)
PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds()) PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds())
PENIS_TOP_LIMIT = 10 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 CASINO_MAX_BET = 1.0
# --- Ставки на матчи ---
ODDS_API_KEY = os.getenv("ODDS_API_KEY", "")
ODDS_API_BASE = "https://api.the-odds-api.com/v4"
ODDS_SPORTS = {
"cs": "esports_csgo",
"dota": "esports_dota2",
"football": "soccer_epl",
}
BET_MAX_AMOUNT = 3.0
BET_DB_PATH = "/db/bets.sqlite3"
ODDS_CACHE_TTL = 900
# --- 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 = { TEMPLATES = {
"1": { "1": {
@ -41,6 +68,81 @@ TEMPLATES = {
def make_time(h: int, m: int) -> time: def make_time(h: int, m: int) -> time:
return time(hour=h, minute=m) 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 = { SCHEDULE = {
0: [ # Понедельник 0: [ # Понедельник
(make_time(9, 50), make_time(10, 35), 5), (make_time(9, 50), make_time(10, 35), 5),
@ -97,12 +199,17 @@ def get_hint_text(templates_str: str) -> str:
f"• /maket «id» «текст» — доступные: {templates_str}.\n" f"• /maket «id» «текст» — доступные: {templates_str}.\n"
"• /gen_mem Up:верх Down:низ — отправь с фото.\n" "• /gen_mem Up:верх Down:низ — отправь с фото.\n"
"• /break — узнать время до перекура.\n" "• /break — узнать время до перекура.\n"
"• /schedule [пн|mon] — расписание; без аргумента еще текущая и следующая пара.\n"
"• /penis — миниигра 1 раз в 24 часа.\n" "• /penis — миниигра 1 раз в 24 часа.\n"
"• /top_penis — топ по длине.\n" "• /top_penis — топ по длине.\n"
"• /talk [текст] — ИИ базарит на фене.\n" "• /talk [текст] — ИИ базарит на фене.\n"
"• /penis_casino [ставка] — казино на размер.\n" "• /penis_casino [ставка] — казино на размер.\n"
"• /gadanie [тема] — гадание на фене с мемом.\n" "• /gadanie [тема] — гадание на фене с мемом.\n"
"• /matches [cs|dota|football] — матчи для ставок.\n"
"• /bet <спорт> <номер> <команда> <ставка> — поставить см.\n"
"• /mybets — мои ставки.\n"
"• /ebalnik — включить/выключить автоответы в чате.\n" "• /ebalnik — включить/выключить автоответы в чате.\n"
"• /prices /fetch — цены.\n" "• /polychaetsi — топ по слову «получается».\n"
"• /prices /fetch — цены и остатки Магнита.\n"
"Подсказка: в группах пиши /команда@username_бота." "Подсказка: в группах пиши /команда@username_бота."
) )

View file

@ -1,10 +1,15 @@
version: "3.9"
services: services:
bot: bot:
build: . build:
context: .
volumes: volumes:
- ./db:/db - ./db:/db
env_file: env_file:
- .env - .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 restart: unless-stopped

308
games/betting.py Normal file
View file

@ -0,0 +1,308 @@
import time as _time
import sqlite3
import logging
from datetime import datetime
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]] = {}
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',
created_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.commit()
_init_bets_db()
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:
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:
logger.warning(f"Odds API returned {resp.status}")
return cached[1] if cached else []
data = await resp.json()
except Exception:
logger.exception("Failed to fetch odds")
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["key"] == "h2h":
for outcome in market["outcomes"]:
name = outcome["name"]
price = outcome["price"]
if name not in best_odds or price > best_odds[name]:
best_odds[name] = price
if len(best_odds) >= 2:
matches.append({
"id": event["id"],
"home": event["home_team"],
"away": event["away_team"],
"commence": event.get("commence_time", ""),
"odds": best_odds,
})
_matches_cache[sport_key] = (now, matches)
return matches
def format_matches(matches: list[dict], sport_label: str) -> str:
if not matches:
return f"🏟 {sport_label} — матчей не найдено."
lines = [f"🏟 {sport_label} — ближайшие матчи:\n"]
for i, m in enumerate(matches[:10], 1):
odds_parts = []
for team, coeff in m["odds"].items():
odds_parts.append(f"{team}: {coeff:.2f}")
odds_str = " | ".join(odds_parts)
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']}\n {odds_str}{start_str}\n")
lines.append("Ставка: /bet <номер> <команда> <сумма>")
return "\n".join(lines)
def _get_sport_label(key: str) -> str:
labels = {"cs": "🎮 CS2", "dota": "🎮 Dota 2", "football": "⚽ Футбол"}
return labels.get(key, key)
async def get_formatted_matches(sport_alias: str | None) -> str:
if sport_alias and sport_alias in config.ODDS_SPORTS:
sport_key = config.ODDS_SPORTS[sport_alias]
matches = await fetch_matches(sport_key)
return format_matches(matches, _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, _get_sport_label(alias)))
return "\n\n".join(parts)
_last_fetched_matches: dict[str, list[dict]] = {}
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:
available = ", ".join(match["odds"].keys())
return f"Команда не найдена. Доступные: {available}"
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, created_ts)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)""",
(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)
lines.append(
f"{r['home_team']} vs {r['away_team']}\n"
f" {r['chosen_team']} (x{r['odds']:.2f}), {r['amount']:.1f} см → {potential:.1f} см"
)
return "\n".join(lines)
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
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:
continue
scores_data = await resp.json()
except Exception:
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
for s in scores:
score_val = int(s.get("score", 0))
if score_val > max_score:
max_score = score_val
winner = s["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:
if bet["chosen_team"] == winner:
winnings = round(bet["amount"] * bet["odds"], 1)
update_user_length(bet["user_id"], winnings)
conn.execute("UPDATE bets SET status = 'won' WHERE id = ?", (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' WHERE id = ?", (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")
return notifications

671
main.py
View file

@ -1,5 +1,6 @@
# Системные импорты # Системные импорты
import asyncio, logging, os, re, random, sqlite3 import asyncio, json, logging, os, re, random, sqlite3, threading
from dataclasses import dataclass
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
@ -21,11 +22,75 @@ from aiogram.client.default import DefaultBotProperties
from AI.talk_handler import handle_talk, generate_autoreply, push_message from AI.talk_handler import handle_talk, generate_autoreply, push_message
from games.casino import play_casino from games.casino import play_casino
from games.fortune import generate_fortune from games.fortune import generate_fortune
from games.betting import get_formatted_matches, get_match_by_index, place_bet, get_user_bets, settle_bets
from zparser import get_military_data from zparser import get_military_data
import config import config
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_polychaetsi_lock = threading.Lock()
_polychaetsi_word_re = re.compile(r"[а-яё]+", re.IGNORECASE)
_no_lesson_re = re.compile(r"(нету\s+пары|нет\s+пары|пары\s+нету|пары\s+нет)", re.IGNORECASE)
_weekday_names = {
0: "Понедельник",
1: "Вторник",
2: "Среда",
3: "Четверг",
4: "Пятница",
5: "Суббота",
6: "Воскресенье",
}
_weekday_aliases = {
"mon": 0,
"monday": 0,
"пн": 0,
"пон": 0,
"понедельник": 0,
"tue": 1,
"tues": 1,
"tuesday": 1,
"вт": 1,
"втр": 1,
"вторник": 1,
"wed": 2,
"wednesday": 2,
"ср": 2,
"срд": 2,
"среда": 2,
"thu": 3,
"thur": 3,
"thurs": 3,
"thursday": 3,
"чт": 3,
"чет": 3,
"четверг": 3,
"fri": 4,
"friday": 4,
"пт": 4,
"пят": 4,
"пятница": 4,
"sat": 5,
"saturday": 5,
"сб": 5,
"суб": 5,
"суббота": 5,
"sun": 6,
"sunday": 6,
"вс": 6,
"вск": 6,
"воскресенье": 6,
}
@dataclass(frozen=True)
class LessonSlot:
pair_number: int | None
title: str
start: time | None
end: time | None
is_free: bool
# --- Инициализация и вспомогательные функции --- # --- Инициализация и вспомогательные функции ---
@ -46,6 +111,13 @@ def init_penis_db() -> None:
conn.execute("CREATE INDEX IF NOT EXISTS idx_penis_stats_length ON penis_stats(length DESC)") conn.execute("CREATE INDEX IF NOT EXISTS idx_penis_stats_length ON penis_stats(length DESC)")
conn.commit() conn.commit()
def init_polychaetsi_storage() -> None:
"""Инициализация json-хранилища статистики слова 'получается'."""
path = config.POLYCHAETSI_STATS_PATH
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text('{"users": {}}', encoding="utf-8")
def templates_list_text() -> str: def templates_list_text() -> str:
"""Список макетов для подсказки.""" """Список макетов для подсказки."""
parts = [] parts = []
@ -62,12 +134,144 @@ def _format_delta_ms(delta: timedelta) -> str:
seconds, milliseconds = divmod(rem, 1_000) seconds, milliseconds = divmod(rem, 1_000)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}" return f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}"
def _normalize_now(now: datetime | None = None) -> datetime:
if now is None:
return datetime.now(ZoneInfo(config.DEFAULT_TZ))
if now.tzinfo is None:
return now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ))
return now
def _current_week_key(now: datetime) -> str:
week_number = now.isocalendar().week
is_upper = (week_number % 2 == 1) == config.UPPER_WEEK_IS_ODD
return "upper" if is_upper else "lower"
def _resolve_schedule_date(now: datetime, requested_weekday: int | None = None) -> datetime:
if requested_weekday is None:
return now
delta_days = (requested_weekday - now.weekday()) % 7
return now + timedelta(days=delta_days)
def _parse_weekday_arg(value: str) -> int | None:
normalized = re.sub(r"[^a-zа-яё]", "", value.lower())
if not normalized:
return None
return _weekday_aliases.get(normalized)
def _build_lesson_slot(raw_entry: dict) -> LessonSlot:
pair_number = raw_entry.get("pair")
start_t, end_t = config.PAIR_TIMES.get(pair_number, (None, None))
title = str(raw_entry.get("title", "")).strip() or "Пара не указана"
return LessonSlot(
pair_number=pair_number,
title=title,
start=start_t,
end=end_t,
is_free=bool(_no_lesson_re.search(title)),
)
def get_lessons_for_day(now: datetime | None = None) -> tuple[str, list[LessonSlot]]:
now = _normalize_now(now)
week_key = _current_week_key(now)
raw_day = config.LESSON_SCHEDULE.get(week_key, {}).get(now.weekday(), [])
lessons = [_build_lesson_slot(item) for item in raw_day]
return week_key, lessons
def _lesson_bounds(now: datetime, lesson: LessonSlot) -> tuple[datetime | None, datetime | None]:
if lesson.start is None or lesson.end is None:
return None, None
start_dt = now.replace(hour=lesson.start.hour, minute=lesson.start.minute, second=0, microsecond=0)
end_dt = now.replace(hour=lesson.end.hour, minute=lesson.end.minute, second=0, microsecond=0)
return start_dt, end_dt
def _find_current_lesson(now: datetime, lessons: list[LessonSlot]) -> LessonSlot | None:
for lesson in lessons:
start_dt, end_dt = _lesson_bounds(now, lesson)
if start_dt and end_dt and start_dt <= now < end_dt:
return lesson
return None
def _find_next_lesson(now: datetime, lessons: list[LessonSlot]) -> LessonSlot | None:
for lesson in lessons:
start_dt, _ = _lesson_bounds(now, lesson)
if start_dt and start_dt > now and not lesson.is_free:
return lesson
return None
def _format_lesson_brief(lesson: LessonSlot) -> str:
prefix = f"{lesson.pair_number} пара" if lesson.pair_number is not None else "Пара"
if lesson.start and lesson.end:
return f"{prefix} ({lesson.start:%H:%M}-{lesson.end:%H:%M}) — {lesson.title}"
return f"{prefix}{lesson.title}"
def _format_lesson_line(lesson: LessonSlot) -> str:
if lesson.pair_number is None:
return f"{lesson.title}"
if lesson.start and lesson.end:
return f"{lesson.pair_number}. {lesson.start:%H:%M}-{lesson.end:%H:%M}{lesson.title}"
return f"{lesson.pair_number}. {lesson.title}"
def next_lesson_text(now: datetime | None = None) -> str:
now = _normalize_now(now)
_, lessons = get_lessons_for_day(now)
next_lesson = _find_next_lesson(now, lessons)
if not next_lesson:
return "Следующей пары сегодня нет."
return f"Следующая пара: {_format_lesson_brief(next_lesson)}"
def build_schedule_text(now: datetime | None = None, requested_weekday: int | None = None) -> str:
now = _normalize_now(now)
target_now = _resolve_schedule_date(now, requested_weekday)
week_key, lessons = get_lessons_for_day(target_now)
show_live_status = target_now.date() == now.date()
current_lesson = _find_current_lesson(now, lessons) if show_live_status else None
next_lesson = _find_next_lesson(now, lessons) if show_live_status else None
week_label = "Верхняя" if week_key == "upper" else "Нижняя"
day_label = _weekday_names.get(target_now.weekday(), "Сегодня")
if not show_live_status:
day_label = f"{day_label}, {target_now:%d.%m.%Y}"
lines = [f"{week_label} неделя", day_label]
if lessons:
lines.append("")
lines.extend(_format_lesson_line(lesson) for lesson in lessons)
else:
lines.append("")
lines.append("На этот день расписание пустое.")
if show_live_status:
lines.append("")
if current_lesson:
if current_lesson.is_free:
lines.append(f"Сейчас: {current_lesson.pair_number} пара свободна")
else:
lines.append(f"Сейчас: {_format_lesson_brief(current_lesson)}")
else:
lines.append("Сейчас: пары нет")
if next_lesson:
lines.append(f"Следующая: {_format_lesson_brief(next_lesson)}")
else:
lines.append("Следующая: сегодня нет")
return "\n".join(lines)
def minutes_until_break(now: datetime | None = None) -> str: def minutes_until_break(now: datetime | None = None) -> str:
"""Логика расписания перекуров.""" """Логика расписания перекуров."""
if now is None: now = _normalize_now(now)
now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ))
day = now.weekday() day = now.weekday()
today_schedule = config.SCHEDULE.get(day, []) today_schedule = config.SCHEDULE.get(day, [])
@ -94,10 +298,7 @@ def minutes_until_break(now: datetime | None = None) -> str:
def next_break_window(now: datetime | None = None) -> tuple[datetime | None, datetime | None]: def next_break_window(now: datetime | None = None) -> tuple[datetime | None, datetime | None]:
"""Возвращает (начало, конец) ближайшего перекура или (None, None).""" """Возвращает (начало, конец) ближайшего перекура или (None, None)."""
if now is None: now = _normalize_now(now)
now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
elif now.tzinfo is None:
now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ))
day = now.weekday() day = now.weekday()
today_schedule = config.SCHEDULE.get(day, []) today_schedule = config.SCHEDULE.get(day, [])
@ -388,6 +589,83 @@ def minutes_until_next_lesson(now: datetime | None = None) -> str:
minutes = (total_seconds % 3600) // 60 minutes = (total_seconds % 3600) // 60
return f"Твой мозг окончательно сгниет, а очко порвется через: {days} дн {hours} ч {minutes} мин" return f"Твой мозг окончательно сгниет, а очко порвется через: {days} дн {hours} ч {minutes} мин"
def _prepare_magnit_query(value: str) -> str:
value = re.sub(r"(?<=[a-zа-яё])(?=[A-ZА-ЯЁ])", " ", value)
value = re.sub(r"[_-]+", " ", value)
return re.sub(r"\s+", " ", value).strip()
def _normalize_magnit_product(entry) -> dict[str, str]:
if isinstance(entry, str):
raw_query = entry.strip()
if not raw_query:
raise ValueError("Пустой query в MAGNIT_PRODUCTS.")
return {
"query": _prepare_magnit_query(raw_query),
"label": raw_query,
"store_code": config.MAGNIT_STORE_CODE,
}
if isinstance(entry, dict):
raw_query = str(entry.get("query", "")).strip()
if not raw_query:
raise ValueError("У товара в MAGNIT_PRODUCTS отсутствует query.")
return {
"query": _prepare_magnit_query(raw_query),
"label": str(entry.get("label") or raw_query).strip() or raw_query,
"store_code": str(entry.get("store_code") or config.MAGNIT_STORE_CODE).strip(),
}
raise ValueError("MAGNIT_PRODUCTS должен содержать строки или словари.")
def _pick_magnit_items(data: dict) -> list[dict]:
items = data.get("items")
if isinstance(items, list):
return [item for item in items if isinstance(item, dict)]
best_match = data.get("best_match")
if isinstance(best_match, dict):
return [best_match]
return []
def _format_money(value) -> str | None:
if value is None:
return None
amount = f"{float(value):.2f}".rstrip("0").rstrip(".")
return f"{amount}"
def _format_magnit_price(item: dict) -> str:
new_price = _format_money(item.get("price"))
old_price = _format_money(item.get("old_price"))
if old_price and new_price and item.get("old_price") and item.get("old_price") != item.get("price"):
return f"{old_price} -> {new_price}"
return new_price or "нет цены"
def _format_magnit_quantity(value) -> str:
if value is None:
return "нет данных"
return f"{value} шт."
async def _send_lines_in_chunks(message: Message, lines: list[str], limit: int = 4000) -> None:
if not lines:
await message.answer("Товары не найдены.")
return
chunk: list[str] = []
chunk_len = 0
for line in lines:
extra_len = len(line) + (1 if chunk else 0)
if chunk and chunk_len + extra_len > limit:
await message.answer("\n".join(chunk))
chunk = [line]
chunk_len = len(line)
continue
chunk.append(line)
chunk_len += extra_len
if chunk:
await message.answer("\n".join(chunk))
# --- Игровая логика (/penis) --- # --- Игровая логика (/penis) ---
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]:
@ -495,6 +773,161 @@ def build_penis_top(limit: int = config.PENIS_TOP_LIMIT) -> str:
lines.append(f"{prefix} {name}{length:.1f} см") lines.append(f"{prefix} {name}{length:.1f} см")
return "\n".join(lines) return "\n".join(lines)
def _load_polychaetsi_stats() -> dict:
path = config.POLYCHAETSI_STATS_PATH
if not path.exists():
return {"users": {}}
try:
with path.open("r", encoding="utf-8") as fh:
data = json.load(fh)
except (OSError, json.JSONDecodeError):
logger.exception("Failed to load polychaetsi stats")
return {"users": {}}
if not isinstance(data, dict):
return {"users": {}}
users = data.get("users")
if not isinstance(users, dict):
data["users"] = {}
return data
def _save_polychaetsi_stats(data: dict) -> None:
path = config.POLYCHAETSI_STATS_PATH
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_name(f"{path.name}.tmp")
with tmp_path.open("w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
tmp_path.replace(path)
def _damerau_levenshtein_distance(source: str, target: str, max_distance: int = 2) -> int:
if source == target:
return 0
if abs(len(source) - len(target)) > max_distance:
return max_distance + 1
previous_previous: list[int] | None = None
previous = list(range(len(target) + 1))
for i, source_char in enumerate(source, start=1):
current = [i] + [0] * len(target)
row_min = current[0]
for j, target_char in enumerate(target, start=1):
cost = 0 if source_char == target_char else 1
current[j] = min(
previous[j] + 1,
current[j - 1] + 1,
previous[j - 1] + cost,
)
if (
previous_previous is not None
and i > 1
and j > 1
and source_char == target[j - 2]
and source[i - 2] == target_char
):
current[j] = min(current[j], previous_previous[j - 2] + 1)
row_min = min(row_min, current[j])
if row_min > max_distance:
return max_distance + 1
previous_previous, previous = previous, current
return previous[-1]
def _normalize_polychaetsi_word(word: str) -> str:
normalized = word.lower().replace("ё", "е")
# Подтягиваем самые частые ошибки в возвратном окончании.
return re.sub(r"(ть?ся|ть?са|цца|ца)$", "тся", normalized)
def is_polychaetsi_variant(word: str) -> bool:
normalized = _normalize_polychaetsi_word(word)
target = config.POLYCHAETSI_TARGET
if normalized == target:
return True
if not normalized or normalized[0] != "п":
return False
if len(normalized) < len(target) - 1 or len(normalized) > len(target) + 1:
return False
if "ч" not in normalized or "а" not in normalized or "е" not in normalized:
return False
if not normalized.endswith(("я", "а")):
return False
return _damerau_levenshtein_distance(normalized, target, max_distance=2) <= 2
def count_polychaetsi_mentions(text: str) -> int:
return sum(1 for word in _polychaetsi_word_re.findall(text) if is_polychaetsi_variant(word))
def register_polychaetsi_hits(user: types.User, hits: int) -> int:
display_name = format_penis_user_name(user)
with _polychaetsi_lock:
data = _load_polychaetsi_stats()
users = data.setdefault("users", {})
user_key = str(user.id)
entry = users.get(user_key)
if not isinstance(entry, dict):
entry = {"display_name": display_name, "count": 0}
stored_count = entry.get("count", 0)
try:
count = int(stored_count)
except (TypeError, ValueError):
count = 0
count += hits
entry["display_name"] = display_name
entry["count"] = count
users[user_key] = entry
_save_polychaetsi_stats(data)
return count
def build_polychaetsi_top(limit: int = config.POLYCHAETSI_TOP_LIMIT) -> str:
with _polychaetsi_lock:
data = _load_polychaetsi_stats()
users = data.get("users", {})
if not isinstance(users, dict):
return "ТОП получается:\nпока пусто."
entries: list[tuple[int, str, int]] = []
for user_key, raw_entry in users.items():
if not isinstance(raw_entry, dict):
continue
try:
count = int(raw_entry.get("count", 0))
except (TypeError, ValueError):
continue
if count <= 0:
continue
try:
numeric_user_id = int(user_key)
except (TypeError, ValueError):
numeric_user_id = 0
name = str(raw_entry.get("display_name") or f"user_{user_key}").strip() or f"user_{user_key}"
entries.append((count, name, numeric_user_id))
if not entries:
return "ТОП получается:\nпока пусто."
entries.sort(key=lambda item: (-item[0], item[1].lower(), item[2]))
medals = {1: "🥇", 2: "🥈", 3: "🥉"}
lines = ["ТОП получается:"]
for index, (count, name, _) in enumerate(entries[:limit], start=1):
prefix = medals.get(index, f"{index}.")
lines.append(f"{prefix} {name}{count}")
return "\n".join(lines)
# --- Хендлеры aiogram --- # --- Хендлеры aiogram ---
async def handle_help(message: Message): async def handle_help(message: Message):
@ -503,9 +936,24 @@ async def handle_help(message: Message):
async def handle_start(message: Message): async def handle_start(message: Message):
await message.answer("Соси") await message.answer("Соси")
async def handle_schedule_cmd(message: Message):
now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
raw_text = message.text or ""
parts = raw_text.split(maxsplit=1)
requested_weekday = None
if len(parts) > 1:
requested_weekday = _parse_weekday_arg(parts[1].split(maxsplit=1)[0])
if requested_weekday is None:
await message.answer(
"Не понял день недели. Примеры: /schedule mon, /schedule fri, /schedule пн, /schedule чт."
)
return
await message.answer(build_schedule_text(now, requested_weekday))
async def handle_break_cmd(message: Message): async def handle_break_cmd(message: Message):
now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
await message.answer(minutes_until_break(now)) await message.answer(f"{minutes_until_break(now)}\n{next_lesson_text(now)}")
# Ставим будильник на ближайший перекур для этого чата # Ставим будильник на ближайший перекур для этого чата
start_dt, _ = next_break_window(now) start_dt, _ = next_break_window(now)
@ -522,6 +970,9 @@ async def handle_penis_cmd(message: Message):
async def handle_top_penis_cmd(message: Message): async def handle_top_penis_cmd(message: Message):
await message.answer(build_penis_top()) await message.answer(build_penis_top())
async def handle_polychaetsi_cmd(message: Message):
await message.answer(build_polychaetsi_top())
async def handle_size_cmd(message: Message): async def handle_size_cmd(message: Message):
size = round(random.uniform(0.1, 35.0), 1) size = round(random.uniform(0.1, 35.0), 1)
if size < 5: if size < 5:
@ -542,61 +993,62 @@ async def handle_zvetok_cmd(message: Message):
await message.answer(minutes_until_next_lesson()) await message.answer(minutes_until_next_lesson())
async def handle_prices_cmd(message: Message): async def handle_prices_cmd(message: Message):
def fetch():
return requests.get(
"https://bebrik.xyz/magnit/products",
headers={"X-Token": "sisi"},
timeout=10,
)
try: try:
resp = await asyncio.to_thread(fetch) products = [_normalize_magnit_product(entry) for entry in config.MAGNIT_PRODUCTS]
if resp.status_code != 200: except ValueError as exc:
await message.answer("пошел нахуй") await message.answer(f"Проверь MAGNIT_PRODUCTS в config.py: {exc}")
return return
data = resp.json()
if not data: if not products:
await message.answer("иди нахуй") await message.answer("Список MAGNIT_PRODUCTS пуст. Добавь товары в config.py.")
return return
text = "смерть в 25:\n\n"
for item in data: async def fetch_product(product_cfg: dict[str, str]) -> list[str]:
name = item.get("name") def fetch():
price = item.get("price") return requests.get(
text += f"{name}{price}\n" f"{config.MAGNIT_API_BASE_URL.rstrip('/')}/price",
await message.answer(text[:4000]) params={
except Exception: "store_code": product_cfg["store_code"],
await message.answer("unable to connect x-server гандон") "query": product_cfg["query"],
},
timeout=config.MAGNIT_REQUEST_TIMEOUT,
)
label = product_cfg["label"] or product_cfg["query"] or "товар"
try:
resp = await asyncio.to_thread(fetch)
if resp.status_code != 200:
logger.warning("Magnit API returned %s for %s", resp.status_code, product_cfg["query"])
return [f"{label}, ошибка API ({resp.status_code}), -"]
data = resp.json()
if not isinstance(data, dict):
return [f"{label}, битый ответ API, -"]
items = _pick_magnit_items(data)
if not items:
return [f"{label}, товар не найден, -"]
lines = []
for item in items:
name = str(item.get("name") or label).strip() or label
price = _format_magnit_price(item)
quantity = _format_magnit_quantity(item.get("quantity"))
lines.append(f"{name}, {price}, {quantity}")
return lines
except requests.RequestException:
logger.exception("Failed to fetch Magnit product: %s", product_cfg["query"])
return [f"{label}, ошибка запроса, -"]
except ValueError:
logger.exception("Magnit API returned invalid JSON for %s", product_cfg["query"])
return [f"{label}, битый JSON, -"]
result_groups = await asyncio.gather(*(fetch_product(product) for product in products))
lines = [line for group in result_groups for line in group]
await _send_lines_in_chunks(message, lines)
async def handle_fetch_cmd(message: Message): async def handle_fetch_cmd(message: Message):
def fetch(): await handle_prices_cmd(message)
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}")
await message.answer("\n".join(lines)[:4000])
except Exception:
await message.answer("unable to connect x-server гандон")
async def handle_svodka_cmd(message: Message): async def handle_svodka_cmd(message: Message):
data = await asyncio.to_thread(get_military_data, "creamy_caprice", 5) data = await asyncio.to_thread(get_military_data, "creamy_caprice", 5)
@ -687,6 +1139,20 @@ async def handle_keywords(message: Message):
) )
return return
polychaetsi_hits = count_polychaetsi_mentions(text_lower)
if polychaetsi_hits > 0 and message.from_user:
total_hits = await asyncio.to_thread(register_polychaetsi_hits, message.from_user, polychaetsi_hits)
photo_path = config.POLYCHAETSI_IMAGE_PATH
if photo_path.exists():
await message.answer_photo(
photo=FSInputFile(photo_path),
caption=f"x{total_hits}",
)
else:
logger.warning("cvetok.jpg not found, fallback to text response")
await message.answer(f"x{total_hits}")
return
# Записываем каждое обычное сообщение в историю чата # Записываем каждое обычное сообщение в историю чата
await asyncio.to_thread(push_message, chat_id, "user", user_name, message.text) await asyncio.to_thread(push_message, chat_id, "user", user_name, message.text)
@ -750,6 +1216,67 @@ async def handle_gadanie_cmd(message: Message):
logger.exception("Gadanie failed") logger.exception("Gadanie failed")
await message.reply("Бля, карты рассыпались, попробуй позже.") await message.reply("Бля, карты рассыпались, попробуй позже.")
async def handle_matches_cmd(message: Message):
parts = message.text.split(maxsplit=1)
sport_alias = parts[1].strip().lower() if len(parts) > 1 else None
await message.bot.send_chat_action(chat_id=message.chat.id, action="typing")
result = await get_formatted_matches(sport_alias)
await message.reply(result, parse_mode=None)
async def handle_bet_cmd(message: Message):
if not message.from_user:
await message.answer("Команда доступна только пользователям.")
return
parts = message.text.split()
if len(parts) < 4:
await message.reply(
"Формат: /bet <номер> <команда> <ставка>\n"
"Пример: /bet cs 1 NAVI 0.5\n"
"Спорт: cs / dota / football"
)
return
# /bet <спорт> <номер> <команда> <ставка>
if len(parts) >= 5:
sport_alias = parts[1].lower()
try:
index = int(parts[2])
except ValueError:
await message.reply("Номер матча должен быть числом.")
return
team = parts[3]
try:
amount = round(float(parts[4].replace(",", ".")), 1)
except ValueError:
await message.reply("Ставка должна быть числом.")
return
else:
await message.reply("Формат: /bet <спорт> <номер> <команда> <ставка>")
return
match = await get_match_by_index(sport_alias, index)
if not match:
await message.reply("Матч не найден. Посмотри /matches")
return
result = await asyncio.to_thread(place_bet, message.from_user.id, match, sport_alias, team, amount)
await message.reply(result, parse_mode=None)
async def handle_mybets_cmd(message: Message):
if not message.from_user:
return
result = await asyncio.to_thread(get_user_bets, message.from_user.id)
await message.reply(result, parse_mode=None)
async def _settle_loop(bot: Bot):
while True:
await asyncio.sleep(1800)
try:
notifications = await settle_bets()
for note in notifications:
logger.info(note)
except Exception:
logger.exception("settle_bets failed")
# --- Запуск --- # --- Запуск ---
def main(): def main():
@ -757,17 +1284,20 @@ def main():
if not token: raise RuntimeError("BOT_TOKEN не задан!") if not token: raise RuntimeError("BOT_TOKEN не задан!")
init_penis_db() init_penis_db()
init_polychaetsi_storage()
bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher() dp = Dispatcher()
# Регистрация хендлеров # Регистрация хендлеров
dp.message.register(handle_start, CommandStart()) dp.message.register(handle_start, CommandStart())
dp.message.register(handle_help, Command("help")) dp.message.register(handle_help, Command("help"))
dp.message.register(handle_schedule_cmd, Command("schedule"))
dp.message.register(handle_break_cmd, Command("break")) dp.message.register(handle_break_cmd, Command("break"))
dp.message.register(handle_maket_cmd, Command("maket")) dp.message.register(handle_maket_cmd, Command("maket"))
dp.message.register(handle_size_cmd, Command("size")) dp.message.register(handle_size_cmd, Command("size"))
dp.message.register(handle_penis_cmd, Command("penis")) dp.message.register(handle_penis_cmd, Command("penis"))
dp.message.register(handle_top_penis_cmd, Command("top_penis")) dp.message.register(handle_top_penis_cmd, Command("top_penis"))
dp.message.register(handle_polychaetsi_cmd, Command("polychaetsi"))
dp.message.register(handle_prices_cmd, Command("prices")) dp.message.register(handle_prices_cmd, Command("prices"))
dp.message.register(handle_fetch_cmd, Command("fetch")) dp.message.register(handle_fetch_cmd, Command("fetch"))
dp.message.register(handle_zvetok_cmd, Command("zvetok")) dp.message.register(handle_zvetok_cmd, Command("zvetok"))
@ -776,6 +1306,9 @@ def main():
dp.message.register(handle_ebalnik_cmd, Command("ebalnik")) dp.message.register(handle_ebalnik_cmd, Command("ebalnik"))
dp.message.register(handle_penis_casino_cmd, Command("penis_casino")) dp.message.register(handle_penis_casino_cmd, Command("penis_casino"))
dp.message.register(handle_gadanie_cmd, Command("gadanie")) dp.message.register(handle_gadanie_cmd, Command("gadanie"))
dp.message.register(handle_matches_cmd, Command("matches"))
dp.message.register(handle_bet_cmd, Command("bet"))
dp.message.register(handle_mybets_cmd, Command("mybets"))
dp.message.register(handle_gen_mem, Command("gen_mem")) dp.message.register(handle_gen_mem, Command("gen_mem"))
dp.message.register(handle_keywords, F.text) dp.message.register(handle_keywords, F.text)
@ -784,19 +1317,24 @@ def main():
commands = [ commands = [
BotCommand(command="start", description="Приветствие"), BotCommand(command="start", description="Приветствие"),
BotCommand(command="help", description="Подсказки по командам"), BotCommand(command="help", description="Подсказки по командам"),
BotCommand(command="schedule", 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="polychaetsi", description="топ по получается"),
BotCommand(command="fetch", description="цены (новый источник)"), BotCommand(command="prices", description="цены и остатки Магнита"),
BotCommand(command="fetch", description="alias для /prices"),
BotCommand(command="zvetok", description="Цветянский бля"), BotCommand(command="zvetok", description="Цветянский бля"),
BotCommand(command="talk", description="Побазарить"), BotCommand(command="talk", description="Побазарить"),
BotCommand(command="ebalnik", description="вкл/выкл автответов"), BotCommand(command="ebalnik", description="вкл/выкл автответов"),
BotCommand(command="penis_casino", description="казино на размер"), BotCommand(command="penis_casino", description="казино на размер"),
BotCommand(command="gadanie", description="гадание на фене"), BotCommand(command="gadanie", description="гадание на фене"),
BotCommand(command="matches", description="матчи для ставок"),
BotCommand(command="bet", description="поставить см на матч"),
BotCommand(command="mybets", description="мои ставки"),
BotCommand(command="svodka", description="СВО: итоги"), BotCommand(command="svodka", description="СВО: итоги"),
] ]
scopes = ( scopes = (
@ -810,6 +1348,11 @@ def main():
logger.warning("Не удалось выставить команды, но бот продолжит работу.") logger.warning("Не удалось выставить команды, но бот продолжит работу.")
dp.startup.register(on_startup) dp.startup.register(on_startup)
async def start_settle(bot: Bot):
asyncio.create_task(_settle_loop(bot))
dp.startup.register(start_settle)
logger.info("Бот запущен...") logger.info("Бот запущен...")
asyncio.run(dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())) asyncio.run(dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()))

BIN
maket/cvetok.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -1,6 +1,8 @@
aiogram>=3.4.1 aiogram>=3.4.1
pillow>=10.3.0 pillow>=10.3.0
requests>=2.31.0 requests>=2.31.0
aiohttp>=3.9.5
tzdata>=2024.1
beautifulsoup4 beautifulsoup4
lxml lxml
pysqlite3-binary>=0.5.3 pysqlite3-binary>=0.5.3