Ставки на спорт #13

Merged
q merged 1 commit from Dan4ick/bot_tg:feature/FenyaBot into main 2026-03-20 07:57:20 +00:00
5 changed files with 405 additions and 1 deletions

1
.env
View file

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

View file

@ -9,7 +9,8 @@
- авто-ответы в чате (10% шанс + триггер-слова);
- миниигра `/penis` + `/top_penis`;
- казино на размер (`/penis_casino`);
- гадание на фене с мемом (`/gadanie`).
- гадание на фене с мемом (`/gadanie`);
- ставки на матчи CS2, Dota 2, футбол с реальными коэффициентами (`/matches`, `/bet`).
## Запуск
@ -31,11 +32,13 @@ 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`
## Команды
@ -52,6 +55,9 @@ docker compose down
- `/penis_casino <ставка>` — казино на размер (макс 1 см, казино всегда в выигрыше).
- `/gadanie <тема>` — ИИ-гадание на фене + авто-мем.
- `/ebalnik` — вкл/выкл авто-ответы бота в чате.
- `/matches [cs|dota|football]` — ближайшие матчи с коэффициентами.
- `/bet <спорт> <номер> <команда> <ставка>` — поставить см на матч.
- `/mybets` — мои активные ставки.
- `/svodka` — сводка СВО.
## Макеты и шрифт
@ -80,6 +86,7 @@ docker compose down
- `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite).
- `/penis_casino` — слоты с множителем x1.2x3 (`games/casino.py`).
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).
- `/matches` + `/bet` — ставки на реальные матчи, коэффициенты через The-Odds-API (`games/betting.py`).
## ИИ-модуль (`AI/talk_handler.py`)

View file

@ -27,6 +27,18 @@ POLYCHAETSI_IMAGE_PATH = BASE_DIR / "maket" / "cvetok.jpg"
# --- Казино ---
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")
@ -193,6 +205,9 @@ def get_hint_text(templates_str: str) -> str:
"• /talk [текст] — ИИ базарит на фене.\n"
"• /penis_casino [ставка] — казино на размер.\n"
"• /gadanie [тема] — гадание на фене с мемом.\n"
"• /matches [cs|dota|football] — матчи для ставок.\n"
"• /bet <спорт> <номер> <команда> <ставка> — поставить см.\n"
"• /mybets — мои ставки.\n"
"• /ebalnik — включить/выключить автоответы в чате.\n"
"• /polychaetsi — топ по слову «получается».\n"
"• /prices /fetch — цены и остатки Магнита.\n"

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

73
main.py
View file

@ -22,6 +22,7 @@ from aiogram.client.default import DefaultBotProperties
from AI.talk_handler import handle_talk, generate_autoreply, push_message
from games.casino import play_casino
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
import config
@ -1215,6 +1216,67 @@ async def handle_gadanie_cmd(message: Message):
logger.exception("Gadanie failed")
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():
@ -1244,6 +1306,9 @@ def main():
dp.message.register(handle_ebalnik_cmd, Command("ebalnik"))
dp.message.register(handle_penis_casino_cmd, Command("penis_casino"))
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_keywords, F.text)
@ -1267,6 +1332,9 @@ def main():
BotCommand(command="ebalnik", description="вкл/выкл автответов"),
BotCommand(command="penis_casino", description="казино на размер"),
BotCommand(command="gadanie", description="гадание на фене"),
BotCommand(command="matches", description="матчи для ставок"),
BotCommand(command="bet", description="поставить см на матч"),
BotCommand(command="mybets", description="мои ставки"),
BotCommand(command="svodka", description="СВО: итоги"),
]
scopes = (
@ -1280,6 +1348,11 @@ def main():
logger.warning("Не удалось выставить команды, но бот продолжит работу.")
dp.startup.register(on_startup)
async def start_settle(bot: Bot):
asyncio.create_task(_settle_loop(bot))
dp.startup.register(start_settle)
logger.info("Бот запущен...")
asyncio.run(dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()))