Merge remote-tracking branch 'origin/main' into main

Resolve config.py conflict by taking Danil's PR #39 schedule content
(teacher names/rooms, per-week Thursday split) as the source of truth,
per explicit user decision, with two fixes on top:
- UPPER_WEEK_IS_ODD restored to False (current ISO-even week is upper,
  confirmed by the user)
- added the missing Saturday entry under LESSON_SCHEDULE["lower"]
  (PR #39 only defined it under "upper", which would have silently
  dropped the Saturday philosophy lecture on lower weeks)
This commit is contained in:
zovos 2026-09-02 15:39:13 +00:00
commit e3c6e1a4df
7 changed files with 267 additions and 235 deletions

View file

@ -63,6 +63,7 @@ docker compose down
- `/bet [спорт] <номер> <исход> <ставка>` — поставить см на матч. - `/bet [спорт] <номер> <исход> <ставка>` — поставить см на матч.
- `/mybets` — мои активные ставки. - `/mybets` — мои активные ставки.
- `/svodka` — сводка СВО. - `/svodka` — сводка СВО.
- `/uwu [теги]` — поиск случайной пикчи/гифки на e621 (по умолчанию sfw, можно указать свои теги).
## Экономика ZOV OS 💰 ## Экономика ZOV OS 💰

437
config.py
View file

@ -1,205 +1,232 @@
import os import os
from pathlib import Path from pathlib import Path
from datetime import timedelta, time from datetime import timedelta, time
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
# --- Базовые пути --- # --- Базовые пути ---
BASE_DIR = Path(__file__).parent BASE_DIR = Path(__file__).parent
FONT_PATH = BASE_DIR / "impact.ttf" FONT_PATH = BASE_DIR / "impact.ttf"
DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow") DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow")
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://bot:botpass@postgres:5432/botdb") DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://bot:botpass@postgres:5432/botdb")
# --- Настройки /penis --- # --- Настройки /penis ---
PENIS_START_LENGTH = 10.0 PENIS_START_LENGTH = 10.0
PENIS_MIN_DELTA = 0.1 PENIS_MIN_DELTA = 0.1
PENIS_MAX_DELTA = 7.0 PENIS_MAX_DELTA = 7.0
PENIS_COOLDOWN = timedelta(hours=24) PENIS_COOLDOWN = timedelta(hours=24)
PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds()) PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds())
PENIS_TOP_LIMIT = 10 PENIS_TOP_LIMIT = 10
# --- Настройки "получается" --- # --- Настройки "получается" ---
POLYCHAETSI_STATS_PATH = Path(os.getenv("POLYCHAETSI_STATS_PATH", "/db/polychaetsi_stats.json")) POLYCHAETSI_STATS_PATH = Path(os.getenv("POLYCHAETSI_STATS_PATH", "/db/polychaetsi_stats.json"))
POLYCHAETSI_TARGET = "получается" POLYCHAETSI_TARGET = "получается"
POLYCHAETSI_TOP_LIMIT = 10 POLYCHAETSI_TOP_LIMIT = 10
POLYCHAETSI_IMAGE_PATH = BASE_DIR / "maket" / "cvetok.jpg" POLYCHAETSI_IMAGE_PATH = BASE_DIR / "maket" / "cvetok.jpg"
# --- Казино --- # --- Казино ---
CASINO_MAX_BET = 1.0 CASINO_MAX_BET = 1.0
# --- E621 API --- # --- E621 API ---
E621_LOGIN = os.getenv("E621_LOGIN", "") E621_LOGIN = os.getenv("E621_LOGIN", "")
E621_API_KEY = os.getenv("E621_API_KEY", "") E621_API_KEY = os.getenv("E621_API_KEY", "")
# --- Ставки на матчи --- # --- Ставки на матчи ---
ODDS_API_KEY = os.getenv("ODDS_API_KEY", "") 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", "dota2": "esports_dota2",
} }
ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно) ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно)
# --- Nude --- # --- Nude ---
NUDE_HISTORY_PATH = Path(os.getenv("NUDE_HISTORY_PATH", "/db/nude_history.json")) NUDE_HISTORY_PATH = Path(os.getenv("NUDE_HISTORY_PATH", "/db/nude_history.json"))
NUDE_POSTS_PER_PAGE = int(os.getenv("NUDE_POSTS_PER_PAGE", "100")) NUDE_POSTS_PER_PAGE = int(os.getenv("NUDE_POSTS_PER_PAGE", "100"))
NUDE_POSTS_POOL_TARGET = int(os.getenv("NUDE_POSTS_POOL_TARGET", "1200")) NUDE_POSTS_POOL_TARGET = int(os.getenv("NUDE_POSTS_POOL_TARGET", "1200"))
NUDE_MAX_POSTS_TO_SCAN = int(os.getenv("NUDE_MAX_POSTS_TO_SCAN", "2000")) NUDE_MAX_POSTS_TO_SCAN = int(os.getenv("NUDE_MAX_POSTS_TO_SCAN", "2000"))
NUDE_MIN_UNSEEN_POOL = int(os.getenv("NUDE_MIN_UNSEEN_POOL", "100")) NUDE_MIN_UNSEEN_POOL = int(os.getenv("NUDE_MIN_UNSEEN_POOL", "100"))
NUDE_HISTORY_MAX_SIZE = int(os.getenv("NUDE_HISTORY_MAX_SIZE", "5000")) NUDE_HISTORY_MAX_SIZE = int(os.getenv("NUDE_HISTORY_MAX_SIZE", "5000"))
# --- Magnit API --- # --- Magnit API ---
MAGNIT_API_BASE_URL = os.getenv("MAGNIT_API_BASE_URL", "https://mirror.porno4free.ru/magnit") MAGNIT_API_BASE_URL = os.getenv("MAGNIT_API_BASE_URL", "https://mirror.porno4free.ru/magnit")
MAGNIT_STORE_CODE = os.getenv("MAGNIT_STORE_CODE", "618224") MAGNIT_STORE_CODE = os.getenv("MAGNIT_STORE_CODE", "618224")
MAGNIT_REQUEST_TIMEOUT = int(os.getenv("MAGNIT_REQUEST_TIMEOUT", "15")) MAGNIT_REQUEST_TIMEOUT = int(os.getenv("MAGNIT_REQUEST_TIMEOUT", "15"))
# Можно указывать просто строку с названием или словарь с query/label/store_code. # Можно указывать просто строку с названием или словарь с query/label/store_code.
MAGNIT_PRODUCTS = [ MAGNIT_PRODUCTS = [
"RedBull", "RedBull",
] ]
# --- Конфигурация макетов --- # --- Конфигурация макетов ---
TEMPLATES = { TEMPLATES = {
"1": { "1": {
"path": BASE_DIR / "maket" / "1.png", "path": BASE_DIR / "maket" / "1.png",
"anchor": (104, 240), "anchor": (104, 240),
"padding": 20, "padding": 20,
"label": "Владимир Владимирович Путин самый лучший президент", "label": "Владимир Владимирович Путин самый лучший президент",
}, },
"2": { "2": {
"path": BASE_DIR / "maket" / "2.png", "path": BASE_DIR / "maket" / "2.png",
"anchor": (40, 40), "anchor": (40, 40),
"box_size": (582, 346), "box_size": (582, 346),
"label": "Макет 2", "label": "Макет 2",
}, },
} }
# --- Расписание --- # --- Расписание ---
def make_time(h: int, m: int) -> time: def make_time(h: int, m: int) -> time:
return time(hour=h, minute=m) return time(hour=h, minute=m)
# Если у тебя наоборот, просто поменяй на False. # Если у тебя наоборот, просто поменяй на False.
UPPER_WEEK_IS_ODD = False UPPER_WEEK_IS_ODD = False
PAIR_TIMES = { PAIR_TIMES = {
1: (make_time(8, 0), make_time(9, 35)), 1: (make_time(8, 0), make_time(9, 35)),
2: (make_time(9, 50), make_time(11, 25)), 2: (make_time(9, 50), make_time(11, 25)),
3: (make_time(11, 55), make_time(13, 30)), 3: (make_time(11, 55), make_time(13, 30)),
4: (make_time(13, 45), make_time(15, 20)), 4: (make_time(13, 45), make_time(15, 20)),
5: (make_time(15, 50), make_time(17, 25)), 5: (make_time(15, 50), make_time(17, 25)),
} }
# Расписание 2 курс, гр. 27 (25ВТ-09.03.03.01-о2), осень 2026-27. LESSON_SCHEDULE = {
# Источник: Raspis_osen_2_kurs_2026-27_g.pdf (черновик, водяной знак "Проект", возможны изменения). "upper": {
# Разделения на верхнюю/нижнюю неделю в источнике нет — обе недели одинаковые. 0: [
_MON = [ {"pair": 1, "title": "Нет пары"},
{"pair": 2, "title": "Экономика и право (практика), Индустриев М.А. до 06.11 / Васильева Е.В. с 07.11, ауд. 504"}, {"pair": 2, "title": "Экономика и право (практика), пр. Индустриев М.А. (до 6.11.25) / Васильева Е.В. (с 7.11.25), а. 504"},
{"pair": 3, "title": "Экономика и право (лекция), Туманян Ю.Р. до 06.11 / Васильева Е.В. с 07.11, ауд. 510"}, {"pair": 3, "title": "Экономика и право (лекция), пр. Туманян Ю.Р. (до 6.11.25) / Васильева Е.В. (с 7.11.25), а. 510"},
{"pair": 4, "title": "Геоинформационные системы (лекция), Ушканова Е.В., ауд. 403а (НИИМ и ПМ)"}, {"pair": 4, "title": "Геоинформационные системы (лекция), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"},
] ],
_TUE = [ 1: [
{"pair": 3, "title": "Специальные разделы математики (практика), Вакулов Б.Г., ауд. 504"}, {"pair": 1, "title": "Нет пары"},
{"pair": 4, "title": "Геоинформационные системы (лаб), Ушканова Е.В., ауд. 403а (НИИМ и ПМ)"}, {"pair": 2, "title": "Нет пары"},
] {"pair": 3, "title": "Специальные разделы математики (практика), пр. Вакулин Б.Г., а. 504"},
_WED = [ {"pair": 4, "title": "1-я подгруппа: Геоинформационные системы (лаб), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"},
{"pair": 2, "title": "Технология программирования (лекция), Губский Д.С., ауд. 510"}, ],
{"pair": 3, "title": "1 подгруппа: Основы функционирования вычислительной техники (лаб), Дацюк О.В., ауд. 606 | 2 подгруппа: Геоинформационные системы (лаб), Ушканова Е.В., ауд. 403а (НИИМ и ПМ)"}, 2: [
{"pair": 4, "title": "1 подгруппа: Технология программирования (лаб), Губский Д.С., ауд. 609 | 2 подгруппа: Технология программирования (лаб), Иванова И.Н., ауд. 609а"}, {"pair": 1, "title": "Нет пары"},
{"pair": 5, "title": "Основы функционирования вычислительной техники (лаб), Дацюк О.В., ауд. 606"}, {"pair": 2, "title": "Технологии программирования (лекция), пр. Губский Д.С., а. 510"},
] {"pair": 3, "title": "1-я подгруппа: Основы функционирования ВТ (лаб), пр. Дядик О.В., а. 606. 2-я подгруппа: Геоинформационные системы (лаб), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"},
_THU = [ {"pair": 4, "title": "1-я подгруппа: Технологии программирования (лаб), пр. Губский Д.С., а. 609. 2-я подгруппа: Технологии программирования (лаб), пр. Иванова Н.Н., а. 609 а"},
{"pair": 2, "title": "Основы функционирования вычислительной техники (лекция), Дацюк О.В., ауд. 510"}, {"pair": 5, "title": "2-я подгруппа: Основы функционирования вычислительной техники (лаб), пр. Дядик О.В., а. 606"},
{"pair": 3, "title": "1 подгруппа: Основы функционирования вычислительной техники (практика), Дацюк О.В., ауд. 818 | 2 подгруппа: Специальные разделы математики (практика), Вакулов Б.Г., ауд. 507"}, ],
{"pair": 4, "title": "Специальные разделы математики (лекция), Каплицкий В.М., ауд. 510"}, 3: [
] {"pair": 1, "title": "Нет пары"},
_FRI = [ {"pair": 2, "title": "Специальные разделы математики (прак), пр. Вакулин Б.Г., а. 502"},
{"pair": None, "title": "Иностранный язык (время уточняется)"}, {"pair": 3, "title": "Основы функционирования вычислительной техники (прак), пр. Дядик О.В., а. 818"},
] {"pair": 4, "title": "Специальные разделы математики (лекция), пр. Котлярский В.М., а. 510"},
_SAT = [ ],
{"pair": 1, "title": "Философия (лекция, совместно с гр. 26), Новохатько А.Г., MS Teams"}, 4: [
] {"pair": None, "title": "Иностранный язык"},
],
_LESSON_WEEK = { 5: [
0: _MON, {"pair": 1, "title": "Философия (лекция), пр. Новосёлова А.Г., MS Teams"},
1: _TUE, ],
2: _WED, },
3: _THU, "lower": {
4: _FRI, 0: [
5: _SAT, {"pair": 1, "title": "Нет пары"},
} {"pair": 2, "title": "Экономика и право (практика), пр. Индустриев М.А. (до 6.11.25) / Васильева Е.В. (с 7.11.25), а. 504"},
{"pair": 3, "title": "Экономика и право (лекция), пр. Туманян Ю.Р. (до 6.11.25) / Васильева Е.В. (с 7.11.25), а. 510"},
LESSON_SCHEDULE = { {"pair": 4, "title": "Геоинформационные системы (лекция), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"},
"upper": _LESSON_WEEK, ],
"lower": _LESSON_WEEK, 1: [
} {"pair": 1, "title": "Нет пары"},
{"pair": 2, "title": "Нет пары"},
# SCHEDULE — тайминг перекуров для /break, построен из фактических пар в {"pair": 3, "title": "Специальные разделы математики (практика), пр. Вакулин Б.Г., а. 504"},
# _LESSON_WEEK, а не задаётся руками, чтобы не рассинхронизироваться при {"pair": 4, "title": "1-я подгруппа: Геоинформационные системы (лаб), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"},
# правках расписания (раньше здесь была отдельная захардкоженная таблица, ],
# которая не обновилась вместе с LESSON_SCHEDULE и врала на днях с занятиями). 2: [
# Каждая пара (95 мин) состоит из двух академ. часов по 45 мин с 5-минутным {"pair": 1, "title": "Нет пары"},
# перекуром между ними; между парами перекур — фактический зазор по PAIR_TIMES. {"pair": 2, "title": "Технологии программирования (лекция), пр. Губский Д.С., а. 510"},
def _time_to_minutes(t: time) -> int: {"pair": 3, "title": "1-я подгруппа: Основы функционирования ВТ (лаб), пр. Дядик О.В., а. 606. 2-я подгруппа: Геоинформационные системы (лаб), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"},
return t.hour * 60 + t.minute {"pair": 4, "title": "1-я подгруппа: Технологии программирования (лаб), пр. Губский Д.С., а. 609. 2-я подгруппа: Технологии программирования (лаб), пр. Иванова Н.Н., а. 609 а"},
{"pair": 5, "title": "2-я подгруппа: Основы функционирования вычислительной техники (лаб), пр. Дядик О.В., а. 606"},
],
def _minutes_to_time(m: int) -> time: 3: [
return make_time(m // 60, m % 60) {"pair": 1, "title": "Нет пары"},
{"pair": 2, "title": "Основы функционирования вычислительной техники (лекция), пр. Дядик О.В., а. 514"},
{"pair": 3, "title": "Нет пары"},
def _build_day_schedule(pairs: list[int]) -> list[tuple[time, time, int | None]]: {"pair": 4, "title": "Специальные разделы математики (лекция), пр. Котлярский В.М., а. 510"},
entries = [] ],
for idx, pair in enumerate(pairs): 4: [
start_t, end_t = PAIR_TIMES[pair] {"pair": None, "title": "Иностранный язык"},
start_m = _time_to_minutes(start_t) ],
end_m = _time_to_minutes(end_t) # В PR отсутствовали пятница/суббота на нижней неделе — этих пар нет
mid_m = start_m + 45 # только в исходном коммите; иняз и философия по субботам идут
second_start_m = mid_m + 5 # независимо от чётности недели, так что дублируем из "upper".
entries.append((start_t, _minutes_to_time(mid_m), 5)) 5: [
if idx == len(pairs) - 1: {"pair": 1, "title": "Философия (лекция), пр. Новосёлова А.Г., MS Teams"},
entries.append((_minutes_to_time(second_start_m), end_t, None)) ],
else: },
next_start_t, _ = PAIR_TIMES[pairs[idx + 1]] }
gap = _time_to_minutes(next_start_t) - end_m
entries.append((_minutes_to_time(second_start_m), end_t, gap)) SCHEDULE = {
return entries 0: [ # Понедельник
(make_time(9, 50), make_time(10, 35), 5),
(make_time(10, 40), make_time(11, 25), 30),
def _pairs_for_day(day_lessons: list[dict]) -> list[int]: (make_time(11, 55), make_time(12, 40), 5),
return sorted({item["pair"] for item in day_lessons if item.get("pair") is not None}) (make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
(make_time(14, 35), make_time(15, 20), None),
SCHEDULE = { ],
day: _build_day_schedule(_pairs_for_day(lessons)) 1: [ # Вторник
for day, lessons in _LESSON_WEEK.items() (make_time(11, 55), make_time(12, 40), 5),
} (make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
# --- Остальное --- (make_time(14, 35), make_time(15, 20), None),
KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'} ],
2: [ # Среда
# --- Авто-ответы ИИ --- (make_time(9, 50), make_time(10, 35), 5),
AUTOREPLY_CHANCE = 0.10 (make_time(10, 40), make_time(11, 25), 30),
AUTOREPLY_COOLDOWN = 120 (make_time(11, 55), make_time(12, 40), 5),
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'ваномас'} (make_time(12, 45), make_time(13, 30), 15),
(make_time(13, 45), make_time(14, 30), 5),
def get_hint_text(templates_str: str) -> str: (make_time(14, 35), make_time(15, 20), 30),
return ( (make_time(15, 50), make_time(16, 35), 5),
"Команды:\n" (make_time(16, 40), make_time(17, 25), None),
f"• /maket «id» «текст» — доступные: {templates_str}.\n" ],
"• /gen_mem Up:верх Down:низ — отправь с фото.\n" 3: [ # Четверг
"• /break — узнать время до перекура.\n" (make_time(9, 50), make_time(10, 35), 5),
"• /schedule [пн|mon] — расписание; без аргумента еще текущая и следующая пара.\n" (make_time(10, 40), make_time(11, 25), 30),
"• /penis — миниигра 1 раз в 24 часа.\n" (make_time(11, 55), make_time(12, 40), 5),
"• /top_penis — топ по длине.\n" (make_time(12, 45), make_time(13, 30), 15),
"• /talk [текст] — ИИ базарит на фене.\n" (make_time(13, 45), make_time(14, 30), 5),
"• /penis_casino [ставка] — казино на размер.\n" (make_time(14, 35), make_time(15, 20), None),
"• /gadanie [тема] — гадание на фене с мемом.\n" ],
"• /matches — матчи для ставок.\n" 4: [], # Пятница
"• /bet [номер] [исход] [ставка] — поставить.\n" 5: [ # Суббота
"• /mybets — мои активные ставки.\n" (make_time(8, 0), make_time(8, 45), 5),
"• /bet_history — история ставок за месяц + статистика.\n" (make_time(8, 50), make_time(9, 35), None),
"• /sports_debug — диагностика API матчей.\n" ],
"• /ebalnik — включить/выключить автоответы в чате.\n" }
"• /uwu [теги] — случайная картинка с e621.\n"
"• /polychaetsi — топ по слову «получается».\n" # --- Остальное ---
"• /prices /fetch — цены и остатки Магнита.\n" KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'}
"Подсказка: в группах пиши /команда@username_бота."
) # --- Авто-ответы ИИ ---
AUTOREPLY_CHANCE = 0.10
AUTOREPLY_COOLDOWN = 120
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'ваномас'}
def get_hint_text(templates_str: str) -> str:
return (
"Команды:\n"
f"• /maket «id» «текст» — доступные: {templates_str}.\n"
"• /gen_mem Up:верх Down:низ — отправь с фото.\n"
"• /break — узнать время до перекура.\n"
"• /schedule [пн|mon] — расписание; без аргумента еще текущая и следующая пара.\n"
"• /penis — миниигра 1 раз в 24 часа.\n"
"• /top_penis — топ по длине.\n"
"• /talk [текст] — ИИ базарит на фене.\n"
"• /penis_casino [ставка] — казино на размер.\n"
"• /gadanie [тема] — гадание на фене с мемом.\n"
"• /matches — матчи для ставок.\n"
"• /bet [номер] [исход] [ставка] — поставить.\n"
"• /mybets — мои активные ставки.\n"
"• /bet_history — история ставок за месяц + статистика.\n"
"• /sports_debug — диагностика API матчей.\n"
"• /ebalnik — включить/выключить автоответы в чате.\n"
"• /uwu [теги] — случайная картинка с e621.\n"
"• /polychaetsi — топ по слову «получается».\n"
"• /prices /fetch — цены и остатки Магнита.\n"
"Подсказка: в группах пиши /команда@username_бота."
)

View file

@ -286,26 +286,26 @@ async def get_match_by_index(sport_alias: str, index: int) -> dict | None:
return None return None
def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: float) -> str: def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: float) -> tuple[bool, str]:
current = get_user_length(user_id) current = get_user_length(user_id)
if current is None: if current is None:
return "Сначала заведи счёт через /penis, братуха." return False, "Сначала заведи счёт через /penis, братуха."
if current <= 0: if current <= 0:
return "🚫 С кредитом ставки не принимаем." return False, "🚫 С кредитом ставки не принимаем."
if amount <= 0: if amount <= 0:
return "Ставка должна быть больше нуля." return False, "Ставка должна быть больше нуля."
if amount > config.BET_MAX_AMOUNT: if amount > config.BET_MAX_AMOUNT:
return f"Максимальная ставка — {config.BET_MAX_AMOUNT} см." return False, f"Максимальная ставка — {config.BET_MAX_AMOUNT} см."
if amount > current: if round(amount, 2) > round(current, 2):
return f"У тебя {current:.1f} см, а ставишь {amount:.1f}. Не хватает, фраер." return False, f"У тебя {current:.1f} см, а ставишь {amount:.1f}. Не хватает, фраер."
team_odds = match["odds"].get(chosen_team) team_odds = match["odds"].get(chosen_team)
if team_odds is None: if team_odds is None:
return f"Исход не найден. Доступные: {describe_match_outcomes(match)}" return False, f"Исход не найден. Доступные: {describe_match_outcomes(match)}"
new_length = update_user_length(user_id, -amount) new_length = update_user_length(user_id, -amount)
if new_length is None: if new_length is None:
return "Ошибка БД." return False, "Ошибка БД."
now_ts = int(_time.time()) now_ts = int(_time.time())
try: try:
@ -323,12 +323,11 @@ def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: f
except psycopg2.Error: except psycopg2.Error:
logger.exception("Failed to place bet") logger.exception("Failed to place bet")
update_user_length(user_id, amount) update_user_length(user_id, amount)
return "Ошибка БД." return False, "Ошибка БД."
potential = round(amount * team_odds, 1) potential = round(amount * team_odds, 1)
return ( return True, (
f"✅ Ставка принята!\n" f"✅ Ставка принята!\n"
f"🏟 {match['home']} vs {match['away']}\n"
f"📌 {chosen_team} (x{team_odds:.2f})\n" f"📌 {chosen_team} (x{team_odds:.2f})\n"
f"💰 Ставка: {amount:.1f} см\n" f"💰 Ставка: {amount:.1f} см\n"
f"🎯 Возможный выигрыш: {potential:.1f} см\n" f"🎯 Возможный выигрыш: {potential:.1f} см\n"

View file

@ -75,7 +75,7 @@ def play_casino(user_id: int, bet: float) -> str:
return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай." return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай."
if bet <= 0: if bet <= 0:
return "Ставка должна быть больше нуля, фраер." return "Ставка должна быть больше нуля, фраер."
if bet > current: if round(bet, 2) > round(current, 2):
return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер." return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер."
reels, matches = spin_slots() reels, matches = spin_slots()

View file

@ -179,7 +179,8 @@ class EconomyManager:
return False return False
from_balance = self.get_user_balance(from_user_id) from_balance = self.get_user_balance(from_user_id)
if amount > from_balance * 0.2 or from_balance < amount: limit = round(from_balance * 0.2, 2)
if round(amount, 2) > limit or round(from_balance, 2) < round(amount, 2):
return False return False
with get_conn() as conn: with get_conn() as conn:
@ -201,15 +202,17 @@ class EconomyManager:
logger.info(f"Transfer: {amount} from user {from_user_id} to user {to_user_id}") logger.info(f"Transfer: {amount} from user {from_user_id} to user {to_user_id}")
return True return True
def create_deposit(self, user_id: int, amount: float, days: int) -> bool: def create_deposit(self, user_id: int, amount: float, days: int) -> tuple[bool, str]:
if amount <= 0: if amount <= 0:
return False return False, "Сумма вклада должна быть больше нуля."
balance = self.get_user_balance(user_id) balance = self.get_user_balance(user_id)
min_deposit = float(self.get_setting('deposit_min_amount')) min_deposit = float(self.get_setting('deposit_min_amount'))
if amount < min_deposit or balance < amount: if round(amount, 2) < round(min_deposit, 2):
return False return False, f"Минимальная сумма вклада — {min_deposit:.1f} см. Твой баланс: {balance:.1f} см."
if round(balance, 2) < round(amount, 2):
return False, f"Недостаточно средств. Твой баланс: {balance:.1f} см, а сумма вклада: {amount:.1f} см."
central_bank_rate = float(self.get_setting('central_bank_rate')) central_bank_rate = float(self.get_setting('central_bank_rate'))
matures_at = datetime.now() + timedelta(days=days) matures_at = datetime.now() + timedelta(days=days)
@ -230,7 +233,7 @@ class EconomyManager:
''', (user_id, amount, central_bank_rate, matures_at)) ''', (user_id, amount, central_bank_rate, matures_at))
logger.info(f"Deposit created: user {user_id}, amount {amount}, rate {central_bank_rate}%") logger.info(f"Deposit created: user {user_id}, amount {amount}, rate {central_bank_rate}%")
return True return True, "Вклад успешно открыт!"
def create_loan(self, user_id: int, amount: float, days: int) -> bool: def create_loan(self, user_id: int, amount: float, days: int) -> bool:
if amount <= 0: if amount <= 0:
@ -239,7 +242,7 @@ class EconomyManager:
max_loan = float(self.get_setting('loan_max_amount')) max_loan = float(self.get_setting('loan_max_amount'))
loan_rate = float(self.get_setting('loan_interest_rate')) loan_rate = float(self.get_setting('loan_interest_rate'))
if amount > max_loan: if round(amount, 2) > round(max_loan, 2):
return False return False
with get_conn() as conn: with get_conn() as conn:
@ -251,13 +254,13 @@ class EconomyManager:
(user_id,), (user_id,),
) )
outstanding = float(cur.fetchone()['total']) outstanding = float(cur.fetchone()['total'])
if outstanding + amount > max_loan: if round(outstanding + amount, 2) > round(max_loan, 2):
return False return False
# ЦБ должен иметь достаточно капитала # ЦБ должен иметь достаточно капитала
cur.execute("SELECT capital FROM central_bank WHERE id=1") cur.execute("SELECT capital FROM central_bank WHERE id=1")
cb_capital = float(cur.fetchone()['capital']) cb_capital = float(cur.fetchone()['capital'])
if cb_capital < amount: if round(cb_capital, 2) < round(amount, 2):
return False return False
due_at = datetime.now() + timedelta(days=days) due_at = datetime.now() + timedelta(days=days)
@ -293,10 +296,10 @@ class EconomyManager:
if not loan or loan['is_repaid']: if not loan or loan['is_repaid']:
return False return False
total_repayment = loan['amount'] * (1 + loan['interest_rate'] / 100) total_repayment = round(loan['amount'] * (1 + loan['interest_rate'] / 100), 2)
balance = self.get_user_balance(user_id) balance = self.get_user_balance(user_id)
if balance < total_repayment: if round(balance, 2) < total_repayment:
return False return False
wcur = conn.cursor() wcur = conn.cursor()
@ -479,7 +482,7 @@ class EconomyManager:
logger.info(f"Central bank updated: {capital_change} ({transaction_type})") logger.info(f"Central bank updated: {capital_change} ({transaction_type})")
def emit_money(self, amount: float, reason: str = "") -> bool: def emit_money(self, amount: float, reason: str = "") -> bool:
if self.get_central_bank_stats()['capital'] < amount: if round(self.get_central_bank_stats()['capital'], 2) < round(amount, 2):
return False return False
self.update_central_bank(-amount, 'emission', reason) self.update_central_bank(-amount, 'emission', reason)
logger.info(f"Money emitted: {amount} cm ({reason})") logger.info(f"Money emitted: {amount} cm ({reason})")

View file

@ -1263,12 +1263,12 @@ async def handle_deposit_cmd(message: Message):
await message.reply("Сумма и дни должны быть положительными.", parse_mode=None) await message.reply("Сумма и дни должны быть положительными.", parse_mode=None)
return return
success = await asyncio.to_thread(economy.create_deposit, message.from_user.id, amount, days) success, error_msg = await asyncio.to_thread(economy.create_deposit, message.from_user.id, amount, days)
if success: if success:
rate = economy.get_setting('central_bank_rate') rate = economy.get_setting('central_bank_rate')
await message.reply(f"✅ Вклад открыт!\n💰 Сумма: {amount:.1f} см\n📅 Срок: {days} дней\n📈 Ставка: {rate}% годовых", parse_mode=None) await message.reply(f"✅ Вклад открыт!\n💰 Сумма: {amount:.1f} см\n📅 Срок: {days} дней\n📈 Ставка: {rate}% годовых", parse_mode=None)
else: else:
await message.reply("❌ Не удалось открыть вклад. Проверь баланс и минимальную сумму.", parse_mode=None) await message.reply(f"❌ Не удалось открыть вклад. {error_msg}", parse_mode=None)
async def handle_loans_cmd(message: Message): async def handle_loans_cmd(message: Message):
"""Показать кредиты""" """Показать кредиты"""
@ -1562,7 +1562,7 @@ async def handle_bet_cmd(message: Message):
) )
return return
result = await asyncio.to_thread(place_bet, message.from_user.id, match, sport_alias, team, amount) success, result = await asyncio.to_thread(place_bet, message.from_user.id, match, sport_alias, team, amount)
await message.reply(result, parse_mode=None) await message.reply(result, parse_mode=None)
async def handle_mybets_cmd(message: Message): async def handle_mybets_cmd(message: Message):

View file

@ -253,7 +253,9 @@ async def create_bet(
raise HTTPException(status_code=400, detail="Неверный исход ставки") raise HTTPException(status_code=400, detail="Неверный исход ставки")
# 3. Разместить ставку (синхронная функция) # 3. Разместить ставку (синхронная функция)
result = await asyncio.to_thread(place_bet, user_id, match, req.league, team, req.amount) success, result = await asyncio.to_thread(place_bet, user_id, match, req.league, team, req.amount)
if not success:
raise HTTPException(status_code=400, detail=result)
return {"message": result} return {"message": result}
@ -294,7 +296,7 @@ async def play_casino_api(
raise HTTPException(status_code=400, detail=f"Макс ставка: {config.CASINO_MAX_BET} см") raise HTTPException(status_code=400, detail=f"Макс ставка: {config.CASINO_MAX_BET} см")
if req.bet <= 0: if req.bet <= 0:
raise HTTPException(status_code=400, detail="Ставка > 0") raise HTTPException(status_code=400, detail="Ставка > 0")
if req.bet > current: if round(req.bet, 2) > round(current, 2):
raise HTTPException(status_code=400, detail=f"У тебя {current:.1f} см") raise HTTPException(status_code=400, detail=f"У тебя {current:.1f} см")
reels, match_count = await asyncio.to_thread(spin_slots) reels, match_count = await asyncio.to_thread(spin_slots)