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]] = {} _last_viewed_sport_by_user: dict[int, str] = {} _DRAW_NAMES = {"draw", "tie", "ничья"} 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() def remember_user_sport_context(user_id: int, sport_alias: str) -> None: if sport_alias in config.ODDS_SPORTS: _last_viewed_sport_by_user[user_id] = sport_alias def clear_user_sport_context(user_id: int) -> None: _last_viewed_sport_by_user.pop(user_id, None) def get_user_sport_context(user_id: int) -> str | None: return _last_viewed_sport_by_user.get(user_id) async def fetch_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 _get_match_outcomes(match: dict) -> list[dict[str, str | float]]: odds = match.get("odds", {}) outcomes: list[dict[str, str | float]] = [] added: set[str] = set() def append_outcome(code: str, name: str) -> None: if name in odds and name not in added: outcomes.append({"code": code, "name": name, "odds": odds[name]}) added.add(name) home = match.get("home", "") away = match.get("away", "") append_outcome("1", home) draw_name = next((name for name in odds if name.casefold() in _DRAW_NAMES), None) if draw_name: append_outcome("X", draw_name) append_outcome("2", away) next_code = 3 for name in odds: if name in added: continue append_outcome(str(next_code), name) next_code += 1 return outcomes def describe_match_outcomes(match: dict) -> str: return ", ".join( f"{item['code']}={item['name']} x{item['odds']:.2f}" for item in _get_match_outcomes(match) ) def resolve_match_outcome(match: dict, raw_choice: str) -> str | None: choice = raw_choice.strip() if not choice: return None normalized = choice.casefold() outcomes = _get_match_outcomes(match) for item in outcomes: if normalized == str(item["code"]).casefold(): return str(item["name"]) if normalized in _DRAW_NAMES or normalized == "x": for item in outcomes: if str(item["code"]).casefold() == "x": return str(item["name"]) for item in outcomes: if normalized == str(item["name"]).casefold(): return str(item["name"]) try: choice_odds = round(float(choice.replace(",", ".")), 2) except ValueError: return None for item in outcomes: if round(float(item["odds"]), 2) == choice_odds: return str(item["name"]) return None def format_matches(matches: list[dict], sport_alias: str, sport_label: str) -> str: if not matches: return f"🏟 {sport_label} [{sport_alias}] — матчей не найдено." lines = [f"🏟 {sport_label} [{sport_alias}] — ближайшие матчи:\n"] for i, m in enumerate(matches[:10], 1): outcomes = _get_match_outcomes(m) odds_str = "\n".join( f" {item['code']}) {item['name']}: {item['odds']:.2f}" for item in outcomes ) start_str = "" if m["commence"]: try: dt = datetime.fromisoformat(m["commence"].replace("Z", "+00:00")) msk = dt.astimezone(ZoneInfo(config.DEFAULT_TZ)) start_str = f"\n 🕐 {msk.strftime('%d.%m %H:%M')} МСК" except Exception: pass lines.append(f"{i}️⃣ {m['home']} vs {m['away']}{start_str}\n{odds_str}\n") lines.append(f"Ставка: /bet {sport_alias} [номер] [исход] [ставка]") lines.append(f"Коротко после /matches {sport_alias}: /bet [номер] [исход] [ставка]") lines.append("Исход: 1/2, для ничьи X. Можно писать ещё название команды или коэффициент.") return "\n".join(lines) def _get_sport_label(key: str) -> str: labels = {"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, 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))) return "\n\n".join(parts) async def get_match_by_index(sport_alias: str, index: int) -> dict | None: sport_key = config.ODDS_SPORTS.get(sport_alias) if not sport_key: return None matches = await fetch_matches(sport_key) if 0 < index <= len(matches): return matches[index - 1] return None def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: float) -> str: current = get_user_length(user_id) if current is None: return "Сначала заведи счёт через /penis, братуха." if current <= 0: return "🚫 С кредитом ставки не принимаем." if amount <= 0: return "Ставка должна быть больше нуля." if amount > config.BET_MAX_AMOUNT: return f"Максимальная ставка — {config.BET_MAX_AMOUNT} см." if amount > current: return f"У тебя {current:.1f} см, а ставишь {amount:.1f}. Не хватает, фраер." team_odds = match["odds"].get(chosen_team) if team_odds is None: return f"Исход не найден. Доступные: {describe_match_outcomes(match)}" new_length = update_user_length(user_id, -amount) if new_length is None: return "Ошибка БД." now_ts = int(_time.time()) try: with sqlite3.connect(config.BET_DB_PATH) as conn: conn.execute( """INSERT INTO bets (user_id, match_id, sport, home_team, away_team, chosen_team, amount, odds, status, 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