forked from zovos/bot_tg
fix
This commit is contained in:
parent
7289296169
commit
107a0647d1
4 changed files with 184 additions and 43 deletions
|
|
@ -56,7 +56,7 @@ docker compose down
|
|||
- `/gadanie <тема>` — ИИ-гадание на фене + авто-мем.
|
||||
- `/ebalnik` — вкл/выкл авто-ответы бота в чате.
|
||||
- `/matches [cs|dota|football]` — ближайшие матчи с коэффициентами.
|
||||
- `/bet <спорт> <номер> <команда> <ставка>` — поставить см на матч.
|
||||
- `/bet [спорт] <номер> <исход> <ставка>` — поставить см на матч.
|
||||
- `/mybets` — мои активные ставки.
|
||||
- `/svodka` — сводка СВО.
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ docker compose down
|
|||
- `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite).
|
||||
- `/penis_casino` — слоты с множителем x1.2–x3 (`games/casino.py`).
|
||||
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).
|
||||
- `/matches` + `/bet` — ставки на реальные матчи, коэффициенты через The-Odds-API (`games/betting.py`).
|
||||
- `/matches` + `/bet` — ставки на реальные матчи, коэффициенты через The-Odds-API (`games/betting.py`). После `/matches cs|dota|football` можно короче: `/bet <номер> <исход> <ставка>`.
|
||||
|
||||
## ИИ-модуль (`AI/talk_handler.py`)
|
||||
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ def get_hint_text(templates_str: str) -> str:
|
|||
"• /penis_casino [ставка] — казино на размер.\n"
|
||||
"• /gadanie [тема] — гадание на фене с мемом.\n"
|
||||
"• /matches [cs|dota|football] — матчи для ставок.\n"
|
||||
"• /bet <спорт> <номер> <команда> <ставка> — поставить см.\n"
|
||||
"• /bet [спорт] [номер] [исход] [ставка] — поставить см.\n"
|
||||
"• /mybets — мои ставки.\n"
|
||||
"• /ebalnik — включить/выключить автоответы в чате.\n"
|
||||
"• /polychaetsi — топ по слову «получается».\n"
|
||||
|
|
|
|||
118
games/betting.py
118
games/betting.py
|
|
@ -13,6 +13,8 @@ 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:
|
||||
|
|
@ -43,6 +45,19 @@ def _init_bets_db() -> None:
|
|||
_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)
|
||||
|
|
@ -96,16 +111,87 @@ async def fetch_matches(sport_key: str) -> list[dict]:
|
|||
return matches
|
||||
|
||||
|
||||
def format_matches(matches: list[dict], sport_label: str) -> str:
|
||||
if not matches:
|
||||
return f"🏟 {sport_label} — матчей не найдено."
|
||||
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()
|
||||
|
||||
lines = [f"🏟 {sport_label} — ближайшие матчи:\n"]
|
||||
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):
|
||||
odds_parts = []
|
||||
for team, coeff in m["odds"].items():
|
||||
odds_parts.append(f"{team}: {coeff:.2f}")
|
||||
odds_str = " | ".join(odds_parts)
|
||||
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"]:
|
||||
|
|
@ -116,9 +202,11 @@ def format_matches(matches: list[dict], sport_label: str) -> str:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
lines.append(f"{i}️⃣ {m['home']} vs {m['away']}\n {odds_str}{start_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(f"Коротко после /matches {sport_alias}: /bet [номер] [исход] [ставка]")
|
||||
lines.append("Исход: 1/2, для ничьи X. Можно писать ещё название команды или коэффициент.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
|
@ -131,18 +219,15 @@ 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))
|
||||
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, _get_sport_label(alias)))
|
||||
parts.append(format_matches(matches, alias, _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:
|
||||
|
|
@ -172,8 +257,7 @@ def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: f
|
|||
|
||||
team_odds = match["odds"].get(chosen_team)
|
||||
if team_odds is None:
|
||||
available = ", ".join(match["odds"].keys())
|
||||
return f"Команда не найдена. Доступные: {available}"
|
||||
return f"Исход не найден. Доступные: {describe_match_outcomes(match)}"
|
||||
|
||||
new_length = update_user_length(user_id, -amount)
|
||||
if new_length is None:
|
||||
|
|
|
|||
103
main.py
103
main.py
|
|
@ -22,7 +22,18 @@ 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 games.betting import (
|
||||
clear_user_sport_context,
|
||||
describe_match_outcomes,
|
||||
get_formatted_matches,
|
||||
get_match_by_index,
|
||||
get_user_bets,
|
||||
get_user_sport_context,
|
||||
place_bet,
|
||||
remember_user_sport_context,
|
||||
resolve_match_outcome,
|
||||
settle_bets,
|
||||
)
|
||||
from zparser import get_military_data
|
||||
import config
|
||||
|
||||
|
|
@ -931,7 +942,7 @@ def build_polychaetsi_top(limit: int = config.POLYCHAETSI_TOP_LIMIT) -> str:
|
|||
# --- Хендлеры aiogram ---
|
||||
|
||||
async def handle_help(message: Message):
|
||||
await message.answer(config.get_hint_text(templates_list_text()))
|
||||
await message.answer(config.get_hint_text(templates_list_text()), parse_mode=None)
|
||||
|
||||
async def handle_start(message: Message):
|
||||
await message.answer("Соси")
|
||||
|
|
@ -1185,7 +1196,7 @@ async def handle_penis_casino_cmd(message: Message):
|
|||
return
|
||||
parts = message.text.split(maxsplit=1)
|
||||
if len(parts) < 2:
|
||||
await message.reply("Формат: /penis_casino <ставка>\nПример: /penis_casino 0.5")
|
||||
await message.reply("Формат: /penis_casino [ставка]\nПример: /penis_casino 0.5", parse_mode=None)
|
||||
return
|
||||
try:
|
||||
bet = round(float(parts[1].replace(",", ".")), 1)
|
||||
|
|
@ -1217,8 +1228,19 @@ async def handle_gadanie_cmd(message: Message):
|
|||
await message.reply("Бля, карты рассыпались, попробуй позже.")
|
||||
|
||||
async def handle_matches_cmd(message: Message):
|
||||
parts = message.text.split(maxsplit=1)
|
||||
raw_text = message.text or ""
|
||||
parts = raw_text.split(maxsplit=1)
|
||||
sport_alias = parts[1].strip().lower() if len(parts) > 1 else None
|
||||
if sport_alias and sport_alias not in config.ODDS_SPORTS:
|
||||
if message.from_user:
|
||||
clear_user_sport_context(message.from_user.id)
|
||||
await message.reply("Неизвестный спорт. Доступно: cs / dota / football", parse_mode=None)
|
||||
return
|
||||
if message.from_user:
|
||||
if sport_alias and 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")
|
||||
result = await get_formatted_matches(sport_alias)
|
||||
await message.reply(result, parse_mode=None)
|
||||
|
|
@ -1227,35 +1249,70 @@ async def handle_bet_cmd(message: Message):
|
|||
if not message.from_user:
|
||||
await message.answer("Команда доступна только пользователям.")
|
||||
return
|
||||
parts = message.text.split()
|
||||
raw_text = message.text or ""
|
||||
parts = raw_text.split()
|
||||
if len(parts) < 4:
|
||||
await message.reply(
|
||||
"Формат: /bet <номер> <команда> <ставка>\n"
|
||||
"Пример: /bet cs 1 NAVI 0.5\n"
|
||||
"Спорт: cs / dota / football"
|
||||
"Формат: /bet [спорт] [номер] [исход] [ставка]\n"
|
||||
"Коротко после /matches cs|dota|football: /bet [номер] [исход] [ставка]\n"
|
||||
"Исход: 1/2, для ничьи X, можно ещё название команды или коэффициент.\n"
|
||||
"Пример: /bet cs 1 1 0.5",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
|
||||
sport_alias: str | None
|
||||
if parts[1].lower() in config.ODDS_SPORTS:
|
||||
if len(parts) < 5:
|
||||
await message.reply(
|
||||
"Формат: /bet [спорт] [номер] [исход] [ставка]\nПример: /bet cs 1 1 0.5",
|
||||
parse_mode=None,
|
||||
)
|
||||
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
|
||||
index_token = parts[2]
|
||||
selection = " ".join(parts[3:-1]).strip()
|
||||
amount_token = parts[-1]
|
||||
else:
|
||||
await message.reply("Формат: /bet <спорт> <номер> <команда> <ставка>")
|
||||
sport_alias = get_user_sport_context(message.from_user.id)
|
||||
if not sport_alias:
|
||||
await message.reply(
|
||||
"Сначала открой /matches cs, /matches dota или /matches football.\n"
|
||||
"Либо пиши полный формат: /bet [спорт] [номер] [исход] [ставка]",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
index_token = parts[1]
|
||||
selection = " ".join(parts[2:-1]).strip()
|
||||
amount_token = parts[-1]
|
||||
|
||||
if not selection:
|
||||
await message.reply("Нужен исход: 1, 2, X, название команды или коэффициент.", parse_mode=None)
|
||||
return
|
||||
|
||||
try:
|
||||
index = int(index_token)
|
||||
except ValueError:
|
||||
await message.reply("Номер матча должен быть числом.", parse_mode=None)
|
||||
return
|
||||
|
||||
try:
|
||||
amount = round(float(amount_token.replace(",", ".")), 1)
|
||||
except ValueError:
|
||||
await message.reply("Ставка должна быть числом.", parse_mode=None)
|
||||
return
|
||||
|
||||
match = await get_match_by_index(sport_alias, index)
|
||||
if not match:
|
||||
await message.reply("Матч не найден. Посмотри /matches")
|
||||
await message.reply(f"Матч не найден. Посмотри /matches {sport_alias}", parse_mode=None)
|
||||
return
|
||||
|
||||
team = resolve_match_outcome(match, selection)
|
||||
if not team:
|
||||
await message.reply(
|
||||
f"Исход не найден. Доступные: {describe_match_outcomes(match)}",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
|
||||
result = await asyncio.to_thread(place_bet, message.from_user.id, match, sport_alias, team, amount)
|
||||
|
|
|
|||
Loading…
Reference in a new issue