Compare commits

..

No commits in common. "cb1e042e646544a30c893f0056218ff3ddfc873f" and "add3553ce07ec675e2590512117bc7d43c87218b" have entirely different histories.

6 changed files with 307 additions and 195 deletions

View file

@ -35,7 +35,13 @@ ODDS_API_KEY = os.getenv("ODDS_API_KEY", "")
ODDS_API_BASE = "https://api.the-odds-api.com/v4" ODDS_API_BASE = "https://api.the-odds-api.com/v4"
BET_MAX_AMOUNT = 3.0 BET_MAX_AMOUNT = 3.0
ODDS_SPORTS = { ODDS_SPORTS = {
"dota2": "esports_dota2", "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",
} }
ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно) ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно)
@ -213,8 +219,8 @@ def get_hint_text(templates_str: str) -> str:
"• /talk [текст] — ИИ базарит на фене.\n" "• /talk [текст] — ИИ базарит на фене.\n"
"• /penis_casino [ставка] — казино на размер.\n" "• /penis_casino [ставка] — казино на размер.\n"
"• /gadanie [тема] — гадание на фене с мемом.\n" "• /gadanie [тема] — гадание на фене с мемом.\n"
"• /matches — матчи для ставок.\n" "• /matches [epl|ucl|laliga|bundesliga|seriea|ligue1|europa] — матчи для ставок.\n"
"• /bet [номер] [исход] [ставка] — поставить.\n" "• /bet [лига] [номер] [исход] [ставка] — поставить см.\n"
"• /mybets — мои активные ставки.\n" "• /mybets — мои активные ставки.\n"
"• /bet_history — история ставок за месяц + статистика.\n" "• /bet_history — история ставок за месяц + статистика.\n"
"• /sports_debug — диагностика API матчей.\n" "• /sports_debug — диагностика API матчей.\n"

View file

@ -1,17 +1,11 @@
import logging import logging
import time as _time import time as _time
import hashlib
import random
import urllib.request
import json
import gzip
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from bs4 import BeautifulSoup
import aiohttp
import psycopg2 import psycopg2
import psycopg2.extras import psycopg2.extras
import aiohttp
import config import config
from db import get_conn from db import get_conn
@ -25,7 +19,6 @@ _last_viewed_sport_by_user: dict[int, str] = {}
_DRAW_NAMES = {"draw", "tie", "ничья"} _DRAW_NAMES = {"draw", "tie", "ничья"}
BET_HISTORY_DAYS = 30 BET_HISTORY_DAYS = 30
_LIQUIPEDIA_CACHE_TTL = 300 # 5 minutes cache to respect Liquipedia rate limits
def _init_bets_db() -> None: def _init_bets_db() -> None:
@ -75,119 +68,91 @@ def get_user_sport_context(user_id: int) -> str | None:
async def _fetch_available_sports() -> list[dict]: async def _fetch_available_sports() -> list[dict]:
return [{"key": "dota2", "title": "🎮 Dota 2", "active": True}] global _sports_cache
async def fetch_liquipedia_matches() -> list[dict]:
global _matches_cache
now = _time.time() now = _time.time()
cached = _matches_cache.get("liquipedia_all") if _sports_cache and (now - _sports_cache[0]) < 3600:
if cached and (now - cached[0]) < _LIQUIPEDIA_CACHE_TTL: 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] return cached[1]
url = "https://liquipedia.net/dota2/api.php?action=parse&page=Liquipedia:Matches&format=json" if not config.ODDS_API_KEY:
headers = { logger.warning("ODDS_API_KEY не задан, матчи недоступны")
"User-Agent": "Dota2BettingTelegramBot/1.0 (contact: danil@example.com)", return []
"Accept-Encoding": "gzip"
url = f"{config.ODDS_API_BASE}/sports/{sport_key}/odds"
params = {
"apiKey": config.ODDS_API_KEY,
"regions": "eu",
"markets": "h2h",
"oddsFormat": "decimal",
} }
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp: async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status != 200: if resp.status != 200:
logger.warning(f"Liquipedia API returned {resp.status}") body = await resp.text()
logger.warning(f"Odds API returned {resp.status} for {sport_key}: {body[:300]}")
return cached[1] if cached else [] return cached[1] if cached else []
data = await resp.json() data = await resp.json()
html_content = data['parse']['text']['*'] logger.info(f"Odds API for {sport_key}: received {len(data)} events")
soup = BeautifulSoup(html_content, "lxml")
match_divs = soup.find_all("div", class_="match-info")
matches = []
for idx, div in enumerate(match_divs):
# Tournament
tournament_div = div.find("div", class_="match-info-tournament")
tournament = tournament_div.get_text(strip=True) if tournament_div else "Unknown Tournament"
# Teams
opponents = div.find_all("div", class_="match-info-header-opponent")
team1 = "TBD"
team2 = "TBD"
winner = None
completed = False
if len(opponents) >= 2:
o1, o2 = opponents[0], opponents[1]
team1 = o1.get_text(strip=True)
team2 = o2.get_text(strip=True)
c1 = o1.get("class", [])
c2 = o2.get("class", [])
has_win_loss = any("winner" in c or "loser" in c for c in c1 + c2)
if has_win_loss:
completed = True
if any("winner" in c for c in c1):
winner = team1
elif any("winner" in c for c in c2):
winner = team2
else:
teams = div.find_all("span", class_="block-team")
if len(teams) >= 2:
team1 = teams[0].get_text(strip=True)
team2 = teams[1].get_text(strip=True)
# Start time
timer_span = div.find("span", class_="timer-object")
timestamp = ""
if timer_span:
timestamp = timer_span.get("data-timestamp", "")
if not team1 or not team2 or team1 == "TBD" or team2 == "TBD":
continue
# ID generation
match_id = hashlib.md5(f"lp_{team1}_{team2}_{timestamp}".encode('utf-8')).hexdigest()
# Deterministic Odds
seed = int(hashlib.md5(match_id.encode('utf-8')).hexdigest(), 16) % 10**8
r = random.Random(seed)
odds_home = round(r.uniform(1.3, 3.2), 2)
margin = 1.08
prob_home = 1.0 / odds_home
prob_away = margin - prob_home
if prob_away < 0.1:
prob_away = 0.1
odds_away = round(1.0 / prob_away, 2)
matches.append({
"id": match_id,
"home": team1,
"away": team2,
"commence": datetime.fromtimestamp(int(timestamp)).isoformat() + "Z" if timestamp else "",
"timestamp": timestamp,
"completed": completed,
"winner": winner,
"odds": {
team1: odds_home,
team2: odds_away
},
"tournament": tournament
})
_matches_cache["liquipedia_all"] = (now, matches)
logger.info(f"Scraped and cached {len(matches)} matches from Liquipedia")
return matches
except Exception: except Exception:
logger.exception("Failed to fetch matches from Liquipedia") logger.exception(f"Failed to fetch odds for {sport_key}")
return cached[1] if cached else [] 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 []
async def fetch_matches(sport_key: str = "esports_dota2") -> list[dict]: matches = []
all_matches = await fetch_liquipedia_matches() for event in data:
# Return only active (not completed) matches for betting best_odds = {}
return [m for m in all_matches if not m["completed"]] 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]]: def _get_match_outcomes(match: dict) -> list[dict[str, str | float]]:
@ -203,7 +168,20 @@ def _get_match_outcomes(match: dict) -> list[dict[str, str | float]]:
home = match.get("home", "") home = match.get("home", "")
away = match.get("away", "") away = match.get("away", "")
append_outcome("1", home) 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) 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 return outcomes
@ -226,6 +204,11 @@ def resolve_match_outcome(match: dict, raw_choice: str) -> str | None:
if normalized == str(item["code"]).casefold(): if normalized == str(item["code"]).casefold():
return str(item["name"]) 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: for item in outcomes:
if normalized == str(item["name"]).casefold(): if normalized == str(item["name"]).casefold():
return str(item["name"]) return str(item["name"])
@ -265,22 +248,67 @@ def format_matches(matches: list[dict], sport_alias: str, sport_label: str) -> s
lines.append(f"{i}️⃣ {m['home']} vs {m['away']}{start_str}\n{odds_str}\n") lines.append(f"{i}️⃣ {m['home']} vs {m['away']}{start_str}\n{odds_str}\n")
lines.append("Ставка: /bet [номер] [исход] [ставка]") lines.append(f"Ставка: /bet {sport_alias} [номер] [исход] [ставка]")
lines.append("Исход: 1/2. Можно писать ещё название команды или коэффициент.") lines.append(f"Коротко после /matches {sport_alias}: /bet [номер] [исход] [ставка]")
lines.append("Исход: 1/2, для ничьи X. Можно писать ещё название команды или коэффициент.")
return "\n".join(lines) return "\n".join(lines)
def _get_sport_label(key: str) -> str: def _get_sport_label(key: str) -> str:
return "🎮 Dota 2" 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: async def get_formatted_matches(sport_alias: str | None) -> str:
matches = await fetch_matches() if not config.ODDS_API_KEY:
return format_matches(matches, "dota2", "🎮 Dota 2") 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. Проверь 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: async def get_match_by_index(sport_alias: str, index: int) -> dict | None:
matches = await fetch_matches() 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): if 0 < index <= len(matches):
return matches[index - 1] return matches[index - 1]
return None return None
@ -467,7 +495,7 @@ async def settle_bets() -> list[str]:
try: try:
with get_conn() as conn: with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute("SELECT DISTINCT match_id FROM bets WHERE status = 'pending'") cur.execute("SELECT DISTINCT match_id, sport FROM bets WHERE status = 'pending'")
pending = cur.fetchall() pending = cur.fetchall()
except psycopg2.Error: except psycopg2.Error:
return notifications return notifications
@ -475,60 +503,118 @@ async def settle_bets() -> list[str]:
if not pending: if not pending:
return notifications return notifications
all_matches = await fetch_liquipedia_matches()
match_map = {m["id"]: m for m in all_matches}
now_ts = int(_time.time()) now_ts = int(_time.time())
for row in pending: for row in pending:
match_id = row["match_id"] match_id = row["match_id"]
event = match_map.get(match_id) sport = row["sport"]
if not event or not event.get("completed"): sport_key = config.ODDS_SPORTS.get(sport, sport)
continue
winner = event.get("winner")
if not winner:
continue
try: try:
with get_conn() as conn: url = f"{config.ODDS_API_BASE}/sports/{sport_key}/scores"
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) params = {"apiKey": config.ODDS_API_KEY, "daysFrom": 3}
cur.execute( async with aiohttp.ClientSession() as session:
"SELECT * FROM bets WHERE match_id = %s AND status = 'pending'", async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp:
(match_id,), if resp.status != 200:
) logger.warning(f"Scores API returned {resp.status} for {sport_key}")
bets = cur.fetchall() continue
scores_data = await resp.json()
except Exception:
logger.exception(f"Failed to fetch scores for {sport_key}")
continue
wcur = conn.cursor() for event in scores_data:
for bet in bets: if event.get("id") != match_id or not event.get("completed"):
chosen = bet["chosen_team"] continue
bet_won = (chosen == winner)
if bet_won: scores = event.get("scores")
winnings = round(bet["amount"] * bet["odds"], 1) if not scores:
update_user_length(bet["user_id"], winnings) continue
wcur.execute(
"UPDATE bets SET status = 'won', payout = %s, resolved_ts = %s WHERE id = %s", score_values = [(s.get("name", ""), int(s.get("score", 0))) for s in scores]
(winnings, now_ts, bet["id"]), winner = None
) is_draw = False
notifications.append(
f"🎉 user_id={bet['user_id']}: выиграл {winnings:.1f} см " if len(score_values) >= 2 and score_values[0][1] == score_values[1][1]:
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" is_draw = True
) winner = "Draw"
else: else:
wcur.execute( max_score = -1
"UPDATE bets SET status = 'lost', payout = 0, resolved_ts = %s WHERE id = %s", for name, score_val in score_values:
(now_ts, bet["id"]), if score_val > max_score:
) max_score = score_val
notifications.append( winner = name
f"❌ user_id={bet['user_id']}: проиграл {bet['amount']:.1f} см "
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" if not winner:
) continue
except psycopg2.Error:
logger.exception("Failed to settle bets") try:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute(
"SELECT * FROM bets WHERE match_id = %s AND status = 'pending'",
(match_id,),
)
bets = cur.fetchall()
wcur = conn.cursor()
for bet in bets:
chosen = bet["chosen_team"]
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)
wcur.execute(
"UPDATE bets SET status = 'won', payout = %s, resolved_ts = %s WHERE id = %s",
(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:
wcur.execute(
"UPDATE bets SET status = 'lost', payout = 0, resolved_ts = %s WHERE id = %s",
(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']})"
)
except psycopg2.Error:
logger.exception("Failed to settle bets")
cleanup_old_bets() cleanup_old_bets()
return notifications return notifications
async def debug_sports() -> str: async def debug_sports() -> str:
return "🎮 Dota 2 — Scraper Liquipedia активен. Odds 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}")
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)

View file

@ -180,7 +180,7 @@ async def handle_uwu_cmd(message: types.Message):
file_url = post["url"] file_url = post["url"]
ext = post["ext"] ext = post["ext"]
caption = post["caption"] caption = post["caption"]
spoiler = post.get("rating") in ("e", "q") # спойлер для explicit и questionable spoiler = post.get("rating") == "e" # спойлер для explicit
if ext in ("gif", "webm", "mp4"): if ext in ("gif", "webm", "mp4"):
# Для анимаций: отправляем caption отдельным сообщением, # Для анимаций: отправляем caption отдельным сообщением,

57
main.py
View file

@ -1461,17 +1461,37 @@ async def handle_gadanie_cmd(message: Message):
async def handle_matches_cmd(message: Message): async def handle_matches_cmd(message: Message):
raw_text = message.text or "" raw_text = message.text or ""
parts = raw_text.split(maxsplit=1) parts = raw_text.split(maxsplit=1)
sport_alias = parts[1].strip().lower() if len(parts) > 1 else "dota2" sport_alias = parts[1].strip().lower() if len(parts) > 1 else None
# Если лига не указана — показать список
if not sport_alias:
labels = {
"epl": "⚽ EPL (Англия)",
"ucl": "⚽ Champions League",
"laliga": "⚽ La Liga (Испания)",
"bundesliga": "⚽ Bundesliga (Германия)",
"seriea": "⚽ Serie A (Италия)",
"ligue1": "⚽ Ligue 1 (Франция)",
"europa": "⚽ Europa League",
}
lines = ["🏆 Доступные лиги для ставок:\n"]
for alias, name in labels.items():
lines.append(f" {name} → /matches {alias}")
lines.append("\nПример: /matches epl")
await message.reply("\n".join(lines), parse_mode=None)
return
if sport_alias not in config.ODDS_SPORTS: if sport_alias not in config.ODDS_SPORTS:
if message.from_user: if message.from_user:
clear_user_sport_context(message.from_user.id) clear_user_sport_context(message.from_user.id)
await message.reply("Неизвестная лига. Доступно: dota2", parse_mode=None) available = " / ".join(config.ODDS_SPORTS.keys())
await message.reply(f"Неизвестная лига. Доступно: {available}", parse_mode=None)
return return
if message.from_user: if message.from_user:
remember_user_sport_context(message.from_user.id, sport_alias) if sport_alias in config.ODDS_SPORTS:
remember_user_sport_context(message.from_user.id, sport_alias)
else:
clear_user_sport_context(message.from_user.id)
await message.bot.send_chat_action(chat_id=message.chat.id, action="typing") await message.bot.send_chat_action(chat_id=message.chat.id, action="typing")
result = await get_formatted_matches(sport_alias) result = await get_formatted_matches(sport_alias)
await message.reply(result, parse_mode=None) await message.reply(result, parse_mode=None)
@ -1484,34 +1504,41 @@ async def handle_bet_cmd(message: Message):
parts = raw_text.split() parts = raw_text.split()
if len(parts) < 4: if len(parts) < 4:
await message.reply( await message.reply(
"Формат: /bet [номер] [исход] [ставка]\n" "Формат: /bet [спорт] [номер] [исход] [ставка]\n"
"Или: /bet dota2 [номер] [исход] [ставка]\n" "Коротко после /matches cs|dota|football: /bet [номер] [исход] [ставка]\n"
"Исход: 1/2. Можно писать ещё название команды или коэффициент.\n" "Исход: 1/2, для ничьи X, можно ещё название команды или коэффициент.\n"
"Пример: /bet 1 1 0.5", "Пример: /bet cs 1 1 0.5",
parse_mode=None, parse_mode=None,
) )
return return
sport_alias: str sport_alias: str | None
if parts[1].lower() == "dota2": if parts[1].lower() in config.ODDS_SPORTS:
if len(parts) < 5: if len(parts) < 5:
await message.reply( await message.reply(
"Формат: /bet dota2 [номер] [исход] [ставка]\nПример: /bet dota2 1 1 0.5", "Формат: /bet [спорт] [номер] [исход] [ставка]\nПример: /bet cs 1 1 0.5",
parse_mode=None, parse_mode=None,
) )
return return
sport_alias = "dota2" sport_alias = parts[1].lower()
index_token = parts[2] index_token = parts[2]
selection = " ".join(parts[3:-1]).strip() selection = " ".join(parts[3:-1]).strip()
amount_token = parts[-1] amount_token = parts[-1]
else: else:
sport_alias = "dota2" sport_alias = get_user_sport_context(message.from_user.id)
if not sport_alias:
await message.reply(
"Сначала открой /matches с нужной лигой (epl, ucl, laliga...).\n"
"Либо пиши полный формат: /bet [спорт] [номер] [исход] [ставка]",
parse_mode=None,
)
return
index_token = parts[1] index_token = parts[1]
selection = " ".join(parts[2:-1]).strip() selection = " ".join(parts[2:-1]).strip()
amount_token = parts[-1] amount_token = parts[-1]
if not selection: if not selection:
await message.reply("Нужен исход: 1, 2, название команды или коэффициент.", parse_mode=None) await message.reply("Нужен исход: 1, 2, X, название команды или коэффициент.", parse_mode=None)
return return
try: try:

View file

@ -174,7 +174,13 @@ async def get_profile(user: dict = Depends(get_current_user)) -> ProfileResponse
async def get_leagues() -> list[LeagueInfo]: async def get_leagues() -> list[LeagueInfo]:
"""Список доступных лиг для ставок.""" """Список доступных лиг для ставок."""
labels = { labels = {
"dota2": "🎮 Dota 2", "epl": "⚽ EPL (Англия)",
"ucl": "⚽ Champions League",
"laliga": "⚽ La Liga (Испания)",
"bundesliga": "⚽ Bundesliga (Германия)",
"seriea": "⚽ Serie A (Италия)",
"ligue1": "⚽ Ligue 1 (Франция)",
"europa": "⚽ Europa League",
} }
return [ return [
LeagueInfo(alias=alias, name=labels.get(alias, alias), sport_key=key) LeagueInfo(alias=alias, name=labels.get(alias, alias), sport_key=key)

View file

@ -295,17 +295,13 @@ async function showBetHistory() {
async function renderBetting(data) { async function renderBetting(data) {
if (data?.league) { if (data?.league) {
return renderMatches(data.league, data.leagueName, data.isOnlyLeague); return renderMatches(data.league, data.leagueName);
} }
container.innerHTML = `<div class="section-header">⚽ Ставки на матчи</div>${skeleton(2)}`; container.innerHTML = `<div class="section-header">⚽ Ставки на матчи</div>${skeleton(2)}`;
try { try {
const leagues = await API.getLeagues(); const leagues = await API.getLeagues();
if (leagues.length === 1) {
return renderMatches(leagues[0].alias, leagues[0].name, true);
}
container.innerHTML = ` container.innerHTML = `
<div class="section-header"> Выбери лигу</div> <div class="section-header"> Выбери лигу</div>
<div class="league-list" id="league-list"></div> <div class="league-list" id="league-list"></div>
@ -321,7 +317,7 @@ async function renderBetting(data) {
`); `);
item.addEventListener('click', () => { item.addEventListener('click', () => {
haptic(); haptic();
navigate('betting', { league: l.alias, leagueName: l.name, isOnlyLeague: false }); navigate('betting', { league: l.alias, leagueName: l.name });
}); });
list.appendChild(item); list.appendChild(item);
}); });
@ -330,22 +326,15 @@ async function renderBetting(data) {
} }
} }
async function renderMatches(league, leagueName, isOnlyLeague = false) { async function renderMatches(league, leagueName) {
const headerText = isOnlyLeague ? '🎮 Dota 2 Матчи' : leagueName;
const backTarget = isOnlyLeague ? 'home' : 'betting';
const instructionsHtml = isOnlyLeague
? `<div style="font-size:13px;color:var(--text-secondary);margin-bottom:12px">Ставки на актуальные матчи Dota 2. Выберите исход и укажите сумму ставки в сантиметрах.</div>`
: '';
container.innerHTML = ` container.innerHTML = `
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px"> <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px">
<button class="btn btn-secondary btn-sm" id="back-btn" style="width:auto;padding:8px 12px"> Назад</button> <button class="btn btn-secondary btn-sm" id="back-btn" style="width:auto;padding:8px 12px"> Назад</button>
<div class="section-header" style="margin:0">${headerText}</div> <div class="section-header" style="margin:0">${leagueName}</div>
</div> </div>
${instructionsHtml}
${skeleton(3)} ${skeleton(3)}
`; `;
document.getElementById('back-btn').onclick = () => navigate(backTarget); document.getElementById('back-btn').onclick = () => navigate('betting');
try { try {
const matches = await API.getMatches(league); const matches = await API.getMatches(league);
@ -372,12 +361,10 @@ async function renderMatches(league, leagueName, isOnlyLeague = false) {
<span class="odds-label">П1</span> <span class="odds-label">П1</span>
<span class="odds-value">${m.odds_home?.toFixed(2) || '—'}</span> <span class="odds-value">${m.odds_home?.toFixed(2) || '—'}</span>
</button> </button>
${m.odds_draw ? `
<button class="odds-btn" data-outcome="X" data-odds="${m.odds_draw || 0}"> <button class="odds-btn" data-outcome="X" data-odds="${m.odds_draw || 0}">
<span class="odds-label">X</span> <span class="odds-label">X</span>
<span class="odds-value">${m.odds_draw?.toFixed(2) || '—'}</span> <span class="odds-value">${m.odds_draw?.toFixed(2) || '—'}</span>
</button> </button>
` : ''}
<button class="odds-btn" data-outcome="2" data-odds="${m.odds_away || 0}"> <button class="odds-btn" data-outcome="2" data-odds="${m.odds_away || 0}">
<span class="odds-label">П2</span> <span class="odds-label">П2</span>
<span class="odds-value">${m.odds_away?.toFixed(2) || '—'}</span> <span class="odds-value">${m.odds_away?.toFixed(2) || '—'}</span>