1
0
Fork 0
forked from zovos/bot_tg
This commit is contained in:
q 2026-03-20 12:24:05 +03:00
parent 7289296169
commit 107a0647d1
4 changed files with 184 additions and 43 deletions

View file

@ -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.2x3 (`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`)

View file

@ -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"

View file

@ -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
View file

@ -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)