Compare commits
2 commits
4ecc25239e
...
801d6da69b
| Author | SHA1 | Date | |
|---|---|---|---|
| 801d6da69b | |||
|
|
6b6c1dd15d |
3 changed files with 320 additions and 25 deletions
|
|
@ -211,7 +211,9 @@ def get_hint_text(templates_str: str) -> str:
|
||||||
"• /gadanie [тема] — гадание на фене с мемом.\n"
|
"• /gadanie [тема] — гадание на фене с мемом.\n"
|
||||||
"• /matches [cs|dota|football] — матчи для ставок.\n"
|
"• /matches [cs|dota|football] — матчи для ставок.\n"
|
||||||
"• /bet [спорт] [номер] [исход] [ставка] — поставить см.\n"
|
"• /bet [спорт] [номер] [исход] [ставка] — поставить см.\n"
|
||||||
"• /mybets — мои ставки.\n"
|
"• /mybets — мои активные ставки.\n"
|
||||||
|
"• /bet_history — история ставок за месяц + статистика.\n"
|
||||||
|
"• /sports_debug — диагностика API матчей.\n"
|
||||||
"• /ebalnik — включить/выключить автоответы в чате.\n"
|
"• /ebalnik — включить/выключить автоответы в чате.\n"
|
||||||
"• /uwu [теги] — случайная картинка с e621.\n"
|
"• /uwu [теги] — случайная картинка с e621.\n"
|
||||||
"• /polychaetsi — топ по слову «получается».\n"
|
"• /polychaetsi — топ по слову «получается».\n"
|
||||||
|
|
|
||||||
305
games/betting.py
305
games/betting.py
|
|
@ -1,7 +1,7 @@
|
||||||
import time as _time
|
import time as _time
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
@ -13,9 +13,13 @@ from games.casino import get_user_length, update_user_length
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_matches_cache: dict[str, tuple[float, list]] = {}
|
_matches_cache: dict[str, tuple[float, list]] = {}
|
||||||
|
_sports_cache: tuple[float, list] | None = None
|
||||||
_last_viewed_sport_by_user: dict[int, str] = {}
|
_last_viewed_sport_by_user: dict[int, str] = {}
|
||||||
_DRAW_NAMES = {"draw", "tie", "ничья"}
|
_DRAW_NAMES = {"draw", "tie", "ничья"}
|
||||||
|
|
||||||
|
# Сколько дней хранить историю ставок
|
||||||
|
BET_HISTORY_DAYS = 30
|
||||||
|
|
||||||
|
|
||||||
def _init_bets_db() -> None:
|
def _init_bets_db() -> None:
|
||||||
Path(config.BET_DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
Path(config.BET_DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
@ -33,12 +37,25 @@ def _init_bets_db() -> None:
|
||||||
amount REAL NOT NULL,
|
amount REAL NOT NULL,
|
||||||
odds REAL NOT NULL,
|
odds REAL NOT NULL,
|
||||||
status TEXT DEFAULT 'pending',
|
status TEXT DEFAULT 'pending',
|
||||||
created_ts INTEGER
|
payout REAL DEFAULT 0,
|
||||||
|
created_ts INTEGER,
|
||||||
|
resolved_ts INTEGER
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_user ON bets(user_id, status)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_user ON bets(user_id, status)")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_match ON bets(match_id, status)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_match ON bets(match_id, status)")
|
||||||
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_created ON bets(created_ts)")
|
||||||
|
|
||||||
|
# Миграция: добавить столбцы если их нет
|
||||||
|
cursor = conn.execute("PRAGMA table_info(bets)")
|
||||||
|
columns = {row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
if "payout" not in columns:
|
||||||
|
conn.execute("ALTER TABLE bets ADD COLUMN payout REAL DEFAULT 0")
|
||||||
|
if "resolved_ts" not in columns:
|
||||||
|
conn.execute("ALTER TABLE bets ADD COLUMN resolved_ts INTEGER")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -58,6 +75,34 @@ def get_user_sport_context(user_id: int) -> str | None:
|
||||||
return _last_viewed_sport_by_user.get(user_id)
|
return _last_viewed_sport_by_user.get(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_available_sports() -> list[dict]:
|
||||||
|
"""Получить список доступных видов спорта из API."""
|
||||||
|
global _sports_cache
|
||||||
|
now = _time.time()
|
||||||
|
if _sports_cache and (now - _sports_cache[0]) < 3600:
|
||||||
|
return _sports_cache[1]
|
||||||
|
|
||||||
|
if not config.ODDS_API_KEY:
|
||||||
|
return []
|
||||||
|
|
||||||
|
url = f"{config.ODDS_API_BASE}/sports"
|
||||||
|
params = {"apiKey": config.ODDS_API_KEY}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
body = await resp.text()
|
||||||
|
logger.warning(f"Sports API returned {resp.status}: {body[:200]}")
|
||||||
|
return []
|
||||||
|
data = await resp.json()
|
||||||
|
_sports_cache = (now, data)
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to fetch sports list")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def fetch_matches(sport_key: str) -> list[dict]:
|
async def fetch_matches(sport_key: str) -> list[dict]:
|
||||||
now = _time.time()
|
now = _time.time()
|
||||||
cached = _matches_cache.get(sport_key)
|
cached = _matches_cache.get(sport_key)
|
||||||
|
|
@ -65,6 +110,7 @@ async def fetch_matches(sport_key: str) -> list[dict]:
|
||||||
return cached[1]
|
return cached[1]
|
||||||
|
|
||||||
if not config.ODDS_API_KEY:
|
if not config.ODDS_API_KEY:
|
||||||
|
logger.warning("ODDS_API_KEY не задан, матчи недоступны")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
url = f"{config.ODDS_API_BASE}/sports/{sport_key}/odds"
|
url = f"{config.ODDS_API_BASE}/sports/{sport_key}/odds"
|
||||||
|
|
@ -79,11 +125,17 @@ async def fetch_matches(sport_key: str) -> list[dict]:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url, params=params, 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"Odds 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()
|
||||||
|
logger.info(f"Odds API for {sport_key}: received {len(data)} events")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to fetch odds")
|
logger.exception(f"Failed to fetch odds for {sport_key}")
|
||||||
|
return cached[1] if cached else []
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
logger.warning(f"Odds API returned non-list for {sport_key}: {type(data)}")
|
||||||
return cached[1] if cached else []
|
return cached[1] if cached else []
|
||||||
|
|
||||||
matches = []
|
matches = []
|
||||||
|
|
@ -91,23 +143,24 @@ async def fetch_matches(sport_key: str) -> list[dict]:
|
||||||
best_odds = {}
|
best_odds = {}
|
||||||
for bookmaker in event.get("bookmakers", []):
|
for bookmaker in event.get("bookmakers", []):
|
||||||
for market in bookmaker.get("markets", []):
|
for market in bookmaker.get("markets", []):
|
||||||
if market["key"] == "h2h":
|
if market.get("key") == "h2h":
|
||||||
for outcome in market["outcomes"]:
|
for outcome in market.get("outcomes", []):
|
||||||
name = outcome["name"]
|
name = outcome.get("name", "")
|
||||||
price = outcome["price"]
|
price = outcome.get("price", 0)
|
||||||
if name not in best_odds or price > best_odds[name]:
|
if name and price and (name not in best_odds or price > best_odds[name]):
|
||||||
best_odds[name] = price
|
best_odds[name] = price
|
||||||
|
|
||||||
if len(best_odds) >= 2:
|
if len(best_odds) >= 2:
|
||||||
matches.append({
|
matches.append({
|
||||||
"id": event["id"],
|
"id": event.get("id", ""),
|
||||||
"home": event["home_team"],
|
"home": event.get("home_team", ""),
|
||||||
"away": event["away_team"],
|
"away": event.get("away_team", ""),
|
||||||
"commence": event.get("commence_time", ""),
|
"commence": event.get("commence_time", ""),
|
||||||
"odds": best_odds,
|
"odds": best_odds,
|
||||||
})
|
})
|
||||||
|
|
||||||
_matches_cache[sport_key] = (now, matches)
|
_matches_cache[sport_key] = (now, matches)
|
||||||
|
logger.info(f"Parsed {len(matches)} matches for {sport_key}")
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -216,16 +269,45 @@ def _get_sport_label(key: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
async def get_formatted_matches(sport_alias: str | None) -> str:
|
async def get_formatted_matches(sport_alias: str | None) -> str:
|
||||||
|
if not config.ODDS_API_KEY:
|
||||||
|
return "⚠️ ODDS_API_KEY не задан. Ставки недоступны."
|
||||||
|
|
||||||
if sport_alias and sport_alias in config.ODDS_SPORTS:
|
if sport_alias and sport_alias in config.ODDS_SPORTS:
|
||||||
sport_key = config.ODDS_SPORTS[sport_alias]
|
sport_key = config.ODDS_SPORTS[sport_alias]
|
||||||
matches = await fetch_matches(sport_key)
|
matches = await fetch_matches(sport_key)
|
||||||
|
if not matches:
|
||||||
|
# Попробуем проверить, существует ли вообще этот спорт
|
||||||
|
sports = await _fetch_available_sports()
|
||||||
|
valid_keys = {s.get("key") for s in sports}
|
||||||
|
if sport_key not in valid_keys:
|
||||||
|
# Ищем похожий ключ
|
||||||
|
suggestions = [s for s in sports if sport_alias in s.get("key", "").lower()
|
||||||
|
or sport_alias in s.get("title", "").lower()]
|
||||||
|
if suggestions:
|
||||||
|
suggest_text = "\n".join(
|
||||||
|
f" • {s['key']} — {s.get('title', '')}"
|
||||||
|
for s in suggestions[:5]
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"⚠️ Ключ {sport_key} не найден в API.\n"
|
||||||
|
f"Похожие виды спорта:\n{suggest_text}\n\n"
|
||||||
|
f"Обнови sport keys в config.py"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"⚠️ Ключ {sport_key} не найден в API. "
|
||||||
|
f"Проверь ODDS_SPORTS в config.py"
|
||||||
|
)
|
||||||
return format_matches(matches, sport_alias, _get_sport_label(sport_alias))
|
return format_matches(matches, sport_alias, _get_sport_label(sport_alias))
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
for alias, sport_key in config.ODDS_SPORTS.items():
|
for alias, sport_key in config.ODDS_SPORTS.items():
|
||||||
matches = await fetch_matches(sport_key)
|
matches = await fetch_matches(sport_key)
|
||||||
parts.append(format_matches(matches, alias, _get_sport_label(alias)))
|
parts.append(format_matches(matches, alias, _get_sport_label(alias)))
|
||||||
return "\n\n".join(parts)
|
|
||||||
|
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:
|
||||||
|
|
@ -268,8 +350,8 @@ def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: f
|
||||||
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO bets (user_id, match_id, sport, home_team, away_team,
|
"""INSERT INTO bets (user_id, match_id, sport, home_team, away_team,
|
||||||
chosen_team, amount, odds, status, created_ts)
|
chosen_team, amount, odds, status, payout, created_ts)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)""",
|
||||||
(user_id, match["id"], sport, match["home"], match["away"],
|
(user_id, match["id"], sport, match["home"], match["away"],
|
||||||
chosen_team, amount, team_odds, now_ts),
|
chosen_team, amount, team_odds, now_ts),
|
||||||
)
|
)
|
||||||
|
|
@ -304,16 +386,129 @@ def get_user_bets(user_id: int) -> str:
|
||||||
if not rows:
|
if not rows:
|
||||||
return "У тебя нет активных ставок."
|
return "У тебя нет активных ставок."
|
||||||
|
|
||||||
lines = ["📋 Твои ставки:\n"]
|
lines = ["📋 Твои активные ставки:\n"]
|
||||||
for r in rows:
|
for r in rows:
|
||||||
potential = round(r["amount"] * r["odds"], 1)
|
potential = round(r["amount"] * r["odds"], 1)
|
||||||
|
created = ""
|
||||||
|
if r["created_ts"]:
|
||||||
|
dt = datetime.fromtimestamp(r["created_ts"], tz=ZoneInfo(config.DEFAULT_TZ))
|
||||||
|
created = f" 📅 {dt.strftime('%d.%m %H:%M')}\n"
|
||||||
lines.append(
|
lines.append(
|
||||||
f"• {r['home_team']} vs {r['away_team']}\n"
|
f"• {r['home_team']} vs {r['away_team']}\n"
|
||||||
f" {r['chosen_team']} (x{r['odds']:.2f}), {r['amount']:.1f} см → {potential:.1f} см"
|
f" {r['chosen_team']} (x{r['odds']:.2f}), {r['amount']:.1f} см → {potential:.1f} см\n"
|
||||||
|
f"{created}"
|
||||||
)
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_bet_history(user_id: int) -> str:
|
||||||
|
"""Полная история ставок за последний месяц."""
|
||||||
|
cutoff_ts = int((_time.time()) - BET_HISTORY_DAYS * 86400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT * FROM bets
|
||||||
|
WHERE user_id = ? AND created_ts >= ?
|
||||||
|
ORDER BY created_ts DESC
|
||||||
|
LIMIT 50""",
|
||||||
|
(user_id, cutoff_ts),
|
||||||
|
).fetchall()
|
||||||
|
except sqlite3.Error:
|
||||||
|
logger.exception("Failed to get bet history")
|
||||||
|
return "Ошибка БД."
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return "📋 За последний месяц ставок не было."
|
||||||
|
|
||||||
|
# Считаем статистику
|
||||||
|
total_bet = 0.0
|
||||||
|
total_won = 0.0
|
||||||
|
total_lost = 0.0
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
pending = 0
|
||||||
|
|
||||||
|
lines = [f"📋 История ставок за {BET_HISTORY_DAYS} дней:\n"]
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
status = r["status"]
|
||||||
|
amount = float(r["amount"])
|
||||||
|
odds_val = float(r["odds"])
|
||||||
|
payout = float(r["payout"] or 0)
|
||||||
|
|
||||||
|
total_bet += amount
|
||||||
|
|
||||||
|
if status == "won":
|
||||||
|
wins += 1
|
||||||
|
total_won += payout
|
||||||
|
status_icon = "✅"
|
||||||
|
result_text = f"+{payout:.1f} см"
|
||||||
|
elif status == "lost":
|
||||||
|
losses += 1
|
||||||
|
total_lost += amount
|
||||||
|
status_icon = "❌"
|
||||||
|
result_text = f"-{amount:.1f} см"
|
||||||
|
else:
|
||||||
|
pending += 1
|
||||||
|
status_icon = "⏳"
|
||||||
|
potential = round(amount * odds_val, 1)
|
||||||
|
result_text = f"ожидание → {potential:.1f} см"
|
||||||
|
|
||||||
|
created = ""
|
||||||
|
if r["created_ts"]:
|
||||||
|
dt = datetime.fromtimestamp(r["created_ts"], tz=ZoneInfo(config.DEFAULT_TZ))
|
||||||
|
created = dt.strftime('%d.%m %H:%M')
|
||||||
|
|
||||||
|
lines.append(
|
||||||
|
f"{status_icon} {r['home_team']} vs {r['away_team']}\n"
|
||||||
|
f" {r['chosen_team']} (x{odds_val:.2f}) | {amount:.1f} см | {result_text}\n"
|
||||||
|
f" 📅 {created}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Итоговая статистика
|
||||||
|
net = total_won - total_lost
|
||||||
|
net_sign = "+" if net >= 0 else ""
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("━━━ 📊 СТАТИСТИКА ━━━")
|
||||||
|
lines.append(f"🎰 Всего ставок: {len(rows)}")
|
||||||
|
lines.append(f"✅ Побед: {wins}")
|
||||||
|
lines.append(f"❌ Поражений: {losses}")
|
||||||
|
if pending:
|
||||||
|
lines.append(f"⏳ В ожидании: {pending}")
|
||||||
|
lines.append(f"💰 Поставлено: {total_bet:.1f} см")
|
||||||
|
lines.append(f"🏆 Выиграно: +{total_won:.1f} см")
|
||||||
|
lines.append(f"💸 Проиграно: -{total_lost:.1f} см")
|
||||||
|
lines.append(f"📈 Итого: {net_sign}{net:.1f} см")
|
||||||
|
|
||||||
|
if wins + losses > 0:
|
||||||
|
winrate = wins / (wins + losses) * 100
|
||||||
|
lines.append(f"📊 Винрейт: {winrate:.0f}%")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_old_bets() -> int:
|
||||||
|
"""Удалить завершённые ставки старше BET_HISTORY_DAYS дней."""
|
||||||
|
cutoff_ts = int(_time.time() - BET_HISTORY_DAYS * 86400)
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(config.BET_DB_PATH) as conn:
|
||||||
|
cursor = conn.execute(
|
||||||
|
"DELETE FROM bets WHERE status != 'pending' AND created_ts < ?",
|
||||||
|
(cutoff_ts,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
deleted = cursor.rowcount
|
||||||
|
if deleted > 0:
|
||||||
|
logger.info(f"Cleaned up {deleted} old bets")
|
||||||
|
return deleted
|
||||||
|
except sqlite3.Error:
|
||||||
|
logger.exception("Failed to cleanup old bets")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
async def settle_bets() -> list[str]:
|
async def settle_bets() -> list[str]:
|
||||||
notifications = []
|
notifications = []
|
||||||
try:
|
try:
|
||||||
|
|
@ -325,6 +520,11 @@ async def settle_bets() -> list[str]:
|
||||||
except sqlite3.Error:
|
except sqlite3.Error:
|
||||||
return notifications
|
return notifications
|
||||||
|
|
||||||
|
if not pending:
|
||||||
|
return notifications
|
||||||
|
|
||||||
|
now_ts = int(_time.time())
|
||||||
|
|
||||||
for row in pending:
|
for row in pending:
|
||||||
match_id = row["match_id"]
|
match_id = row["match_id"]
|
||||||
sport = row["sport"]
|
sport = row["sport"]
|
||||||
|
|
@ -336,9 +536,11 @@ async def settle_bets() -> list[str]:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url, params=params, 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"Scores API returned {resp.status} for {sport_key}")
|
||||||
continue
|
continue
|
||||||
scores_data = await resp.json()
|
scores_data = await resp.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.exception(f"Failed to fetch scores for {sport_key}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for event in scores_data:
|
for event in scores_data:
|
||||||
|
|
@ -353,11 +555,22 @@ async def settle_bets() -> list[str]:
|
||||||
|
|
||||||
winner = None
|
winner = None
|
||||||
max_score = -1
|
max_score = -1
|
||||||
|
is_draw = False
|
||||||
|
|
||||||
|
score_values = []
|
||||||
for s in scores:
|
for s in scores:
|
||||||
score_val = int(s.get("score", 0))
|
score_val = int(s.get("score", 0))
|
||||||
|
score_values.append((s.get("name", ""), score_val))
|
||||||
|
|
||||||
|
if len(score_values) >= 2 and score_values[0][1] == score_values[1][1]:
|
||||||
|
is_draw = True
|
||||||
|
# Для ничьи ищем "Draw" среди ставок
|
||||||
|
winner = "Draw"
|
||||||
|
else:
|
||||||
|
for name, score_val in score_values:
|
||||||
if score_val > max_score:
|
if score_val > max_score:
|
||||||
max_score = score_val
|
max_score = score_val
|
||||||
winner = s["name"]
|
winner = name
|
||||||
|
|
||||||
if not winner:
|
if not winner:
|
||||||
continue
|
continue
|
||||||
|
|
@ -371,16 +584,31 @@ async def settle_bets() -> list[str]:
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
for bet in bets:
|
for bet in bets:
|
||||||
if bet["chosen_team"] == winner:
|
chosen = bet["chosen_team"]
|
||||||
|
bet_won = False
|
||||||
|
|
||||||
|
if is_draw:
|
||||||
|
# Ставка на ничью выигрывает
|
||||||
|
bet_won = chosen.casefold() in _DRAW_NAMES or chosen == "Draw"
|
||||||
|
else:
|
||||||
|
bet_won = chosen == winner
|
||||||
|
|
||||||
|
if bet_won:
|
||||||
winnings = round(bet["amount"] * bet["odds"], 1)
|
winnings = round(bet["amount"] * bet["odds"], 1)
|
||||||
update_user_length(bet["user_id"], winnings)
|
update_user_length(bet["user_id"], winnings)
|
||||||
conn.execute("UPDATE bets SET status = 'won' WHERE id = ?", (bet["id"],))
|
conn.execute(
|
||||||
|
"UPDATE bets SET status = 'won', payout = ?, resolved_ts = ? WHERE id = ?",
|
||||||
|
(winnings, now_ts, bet["id"]),
|
||||||
|
)
|
||||||
notifications.append(
|
notifications.append(
|
||||||
f"🎉 user_id={bet['user_id']}: выиграл {winnings:.1f} см "
|
f"🎉 user_id={bet['user_id']}: выиграл {winnings:.1f} см "
|
||||||
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})"
|
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
conn.execute("UPDATE bets SET status = 'lost' WHERE id = ?", (bet["id"],))
|
conn.execute(
|
||||||
|
"UPDATE bets SET status = 'lost', payout = 0, resolved_ts = ? WHERE id = ?",
|
||||||
|
(now_ts, bet["id"]),
|
||||||
|
)
|
||||||
notifications.append(
|
notifications.append(
|
||||||
f"❌ user_id={bet['user_id']}: проиграл {bet['amount']:.1f} см "
|
f"❌ user_id={bet['user_id']}: проиграл {bet['amount']:.1f} см "
|
||||||
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})"
|
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})"
|
||||||
|
|
@ -389,4 +617,39 @@ async def settle_bets() -> list[str]:
|
||||||
except sqlite3.Error:
|
except sqlite3.Error:
|
||||||
logger.exception("Failed to settle bets")
|
logger.exception("Failed to settle bets")
|
||||||
|
|
||||||
|
# Очистка старых ставок
|
||||||
|
cleanup_old_bets()
|
||||||
|
|
||||||
return notifications
|
return notifications
|
||||||
|
|
||||||
|
|
||||||
|
async def debug_sports() -> str:
|
||||||
|
"""Диагностика: показать доступные виды спорта из API."""
|
||||||
|
if not config.ODDS_API_KEY:
|
||||||
|
return "⚠️ ODDS_API_KEY не задан."
|
||||||
|
|
||||||
|
sports = await _fetch_available_sports()
|
||||||
|
if not sports:
|
||||||
|
return "❌ Не удалось получить список спортов от API."
|
||||||
|
|
||||||
|
# Показываем текущие ключи и проверяем их
|
||||||
|
current_keys = set(config.ODDS_SPORTS.values())
|
||||||
|
valid_keys = {s.get("key") for s in sports}
|
||||||
|
|
||||||
|
lines = ["🔍 Диагностика sport keys:\n"]
|
||||||
|
|
||||||
|
for alias, key in config.ODDS_SPORTS.items():
|
||||||
|
status = "✅" if key in valid_keys else "❌ НЕ НАЙДЕН"
|
||||||
|
lines.append(f" {alias} → {key} {status}")
|
||||||
|
|
||||||
|
# Показать доступные esports и football
|
||||||
|
lines.append("\n📋 Доступные eSports и футбол:")
|
||||||
|
for s in sports:
|
||||||
|
key = s.get("key", "")
|
||||||
|
title = s.get("title", "")
|
||||||
|
active = s.get("active", False)
|
||||||
|
if active and ("esport" in key.lower() or "soccer" in key.lower() or "football" in key.lower()):
|
||||||
|
marker = " ← ИСПОЛЬЗУЕТСЯ" if key in current_keys else ""
|
||||||
|
lines.append(f" • {key} — {title}{marker}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
|
||||||
32
main.py
32
main.py
|
|
@ -25,9 +25,11 @@ from games.fortune import generate_fortune
|
||||||
from games.uwu import handle_uwu_cmd
|
from games.uwu import handle_uwu_cmd
|
||||||
from games.betting import (
|
from games.betting import (
|
||||||
clear_user_sport_context,
|
clear_user_sport_context,
|
||||||
|
debug_sports,
|
||||||
describe_match_outcomes,
|
describe_match_outcomes,
|
||||||
get_formatted_matches,
|
get_formatted_matches,
|
||||||
get_match_by_index,
|
get_match_by_index,
|
||||||
|
get_user_bet_history,
|
||||||
get_user_bets,
|
get_user_bets,
|
||||||
get_user_sport_context,
|
get_user_sport_context,
|
||||||
place_bet,
|
place_bet,
|
||||||
|
|
@ -1325,6 +1327,30 @@ async def handle_mybets_cmd(message: Message):
|
||||||
result = await asyncio.to_thread(get_user_bets, message.from_user.id)
|
result = await asyncio.to_thread(get_user_bets, message.from_user.id)
|
||||||
await message.reply(result, parse_mode=None)
|
await message.reply(result, parse_mode=None)
|
||||||
|
|
||||||
|
async def handle_bet_history_cmd(message: Message):
|
||||||
|
if not message.from_user:
|
||||||
|
return
|
||||||
|
result = await asyncio.to_thread(get_user_bet_history, message.from_user.id)
|
||||||
|
# Разделяем на части если слишком длинно
|
||||||
|
if len(result) > 4000:
|
||||||
|
parts = result.split("\n\n")
|
||||||
|
chunk = ""
|
||||||
|
for part in parts:
|
||||||
|
if len(chunk) + len(part) + 2 > 4000:
|
||||||
|
await message.reply(chunk, parse_mode=None)
|
||||||
|
chunk = part
|
||||||
|
else:
|
||||||
|
chunk = chunk + "\n\n" + part if chunk else part
|
||||||
|
if chunk:
|
||||||
|
await message.reply(chunk, parse_mode=None)
|
||||||
|
else:
|
||||||
|
await message.reply(result, parse_mode=None)
|
||||||
|
|
||||||
|
async def handle_sports_debug_cmd(message: Message):
|
||||||
|
await message.bot.send_chat_action(chat_id=message.chat.id, action="typing")
|
||||||
|
result = await debug_sports()
|
||||||
|
await message.reply(result, parse_mode=None)
|
||||||
|
|
||||||
async def _settle_loop(bot: Bot):
|
async def _settle_loop(bot: Bot):
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1800)
|
await asyncio.sleep(1800)
|
||||||
|
|
@ -1367,6 +1393,8 @@ def main():
|
||||||
dp.message.register(handle_matches_cmd, Command("matches"))
|
dp.message.register(handle_matches_cmd, Command("matches"))
|
||||||
dp.message.register(handle_bet_cmd, Command("bet"))
|
dp.message.register(handle_bet_cmd, Command("bet"))
|
||||||
dp.message.register(handle_mybets_cmd, Command("mybets"))
|
dp.message.register(handle_mybets_cmd, Command("mybets"))
|
||||||
|
dp.message.register(handle_bet_history_cmd, Command("bet_history"))
|
||||||
|
dp.message.register(handle_sports_debug_cmd, Command("sports_debug"))
|
||||||
dp.message.register(handle_gen_mem, Command("gen_mem"))
|
dp.message.register(handle_gen_mem, Command("gen_mem"))
|
||||||
dp.message.register(handle_uwu_cmd, Command("uwu"))
|
dp.message.register(handle_uwu_cmd, Command("uwu"))
|
||||||
dp.message.register(handle_keywords, F.text)
|
dp.message.register(handle_keywords, F.text)
|
||||||
|
|
@ -1393,7 +1421,9 @@ def main():
|
||||||
BotCommand(command="gadanie", description="гадание на фене"),
|
BotCommand(command="gadanie", description="гадание на фене"),
|
||||||
BotCommand(command="matches", description="матчи для ставок"),
|
BotCommand(command="matches", description="матчи для ставок"),
|
||||||
BotCommand(command="bet", description="поставить см на матч"),
|
BotCommand(command="bet", description="поставить см на матч"),
|
||||||
BotCommand(command="mybets", description="мои ставки"),
|
BotCommand(command="mybets", description="мои активные ставки"),
|
||||||
|
BotCommand(command="bet_history", description="история ставок за месяц"),
|
||||||
|
BotCommand(command="sports_debug", description="диагностика API матчей"),
|
||||||
BotCommand(command="svodka", description="СВО: итоги"),
|
BotCommand(command="svodka", description="СВО: итоги"),
|
||||||
BotCommand(command="uwu", description="Случайная картинка с e621"),
|
BotCommand(command="uwu", description="Случайная картинка с e621"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue