diff --git a/.env b/.env deleted file mode 100644 index b7916fa..0000000 --- a/.env +++ /dev/null @@ -1,9 +0,0 @@ -BOT_TOKEN=8505447901:AAHFkaDG0fT0tAixuJ4QP91IwMgO6cZndRM #8156642467:AAEalekr-33lFS3tTTkXIEiaawfDzR6lBJc -TZ=Europe/Moscow -ODDS_API_KEY=c29bc26dee8aa9d95a5ce8d14ea63923 -LLAMA_API_URL=https://mirror.porno4free.ru/zovos-ai/ -LLAMA_FORCE_DISABLE_THINKING=0 -LLAMA_LOG_THINKING=1 -WEBAPP_URL=https://uninterleaved-scrawnily-nicol.ngrok-free.dev - -NGROK_AUTHTOKEN=39k4ojhN5UPUWQwb1ly84ORM2IE_5JnhMaKk5p8LFM3TCetyg diff --git a/AI/talk_handler.py b/AI/talk_handler.py index bd98c3f..d0b4594 100644 --- a/AI/talk_handler.py +++ b/AI/talk_handler.py @@ -7,21 +7,19 @@ import mimetypes import os import random import re -import sqlite3 import time from io import BytesIO -import re -from pathlib import Path from typing import Any import aiohttp +import psycopg2.extras from aiogram.types import Message +from db import get_conn + logger = logging.getLogger(__name__) -# Основные настройки поведения и стиля бота редактируются здесь. LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-ai/") -DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH") or getattr(__import__("config"), "CHAT_HISTORY_DB_PATH", str(Path(__file__).resolve().with_name("chat_history.sqlite3"))) BOT_MEMORY_NAME = os.getenv("BOT_MEMORY_NAME", "бот") SKIP_TOKEN = "" FORCE_DISABLE_THINKING = os.getenv("LLAMA_FORCE_DISABLE_THINKING", "1").lower() not in {"0", "false", "no"} @@ -246,133 +244,95 @@ _cached_bot_id: int | None = None _cached_bot_username: str = "" -def _connect_db() -> sqlite3.Connection: - conn = sqlite3.connect(DB_PATH, timeout=30) - conn.row_factory = sqlite3.Row - conn.execute("PRAGMA busy_timeout = 30000") - return conn - - -def _column_exists(conn: sqlite3.Connection, table: str, column: str) -> bool: - rows = conn.execute(f"PRAGMA table_info({table})").fetchall() - return any(row["name"] == column for row in rows) - - -def _get_state_from_conn(conn: sqlite3.Connection, chat_id: int, key: str, default: str = "") -> str: - row = conn.execute( - "SELECT value FROM chat_state WHERE chat_id = ? AND key = ?", +def _get_state_from_conn(conn, chat_id: int, key: str, default: str = "") -> str: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute( + "SELECT value FROM chat_state WHERE chat_id = %s AND key = %s", (chat_id, key), - ).fetchone() + ) + row = cur.fetchone() return row["value"] if row else default -def _set_state_from_conn(conn: sqlite3.Connection, chat_id: int, key: str, value: str) -> None: - conn.execute( +def _set_state_from_conn(conn, chat_id: int, key: str, value: str) -> None: + cur = conn.cursor() + cur.execute( """ INSERT INTO chat_state (chat_id, key, value) - VALUES (?, ?, ?) - ON CONFLICT(chat_id, key) DO UPDATE SET value = excluded.value + VALUES (%s, %s, %s) + ON CONFLICT (chat_id, key) DO UPDATE SET value = EXCLUDED.value """, (chat_id, key, value), ) -def _get_talk_state_from_conn( - conn: sqlite3.Connection, - chat_id: int, - user_id: int, - key: str, - default: str = "", -) -> str: - row = conn.execute( - "SELECT value FROM talk_state WHERE chat_id = ? AND user_id = ? AND key = ?", +def _get_talk_state_from_conn(conn, chat_id: int, user_id: int, key: str, default: str = "") -> str: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute( + "SELECT value FROM talk_state WHERE chat_id = %s AND user_id = %s AND key = %s", (chat_id, user_id, key), - ).fetchone() + ) + row = cur.fetchone() return row["value"] if row else default -def _set_talk_state_from_conn( - conn: sqlite3.Connection, - chat_id: int, - user_id: int, - key: str, - value: str, -) -> None: - conn.execute( +def _set_talk_state_from_conn(conn, chat_id: int, user_id: int, key: str, value: str) -> None: + cur = conn.cursor() + cur.execute( """ INSERT INTO talk_state (chat_id, user_id, key, value) - VALUES (?, ?, ?, ?) - ON CONFLICT(chat_id, user_id, key) DO UPDATE SET value = excluded.value + VALUES (%s, %s, %s, %s) + ON CONFLICT (chat_id, user_id, key) DO UPDATE SET value = EXCLUDED.value """, (chat_id, user_id, key, value), ) def _init_db() -> None: - Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True) - with _connect_db() as conn: - conn.execute("PRAGMA journal_mode = WAL") - conn.execute( - """ + with get_conn() as conn: + cur = conn.cursor() + cur.execute(""" CREATE TABLE IF NOT EXISTS chat_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - chat_id INTEGER NOT NULL, + id BIGSERIAL PRIMARY KEY, + chat_id BIGINT NOT NULL, role TEXT NOT NULL, name TEXT NOT NULL, text TEXT NOT NULL, - created_at REAL NOT NULL DEFAULT 0 + created_at DOUBLE PRECISION NOT NULL DEFAULT 0 ) - """ - ) - if not _column_exists(conn, "chat_history", "created_at"): - conn.execute("ALTER TABLE chat_history ADD COLUMN created_at REAL NOT NULL DEFAULT 0") - conn.execute( - """ + """) + cur.execute("ALTER TABLE chat_history ADD COLUMN IF NOT EXISTS created_at DOUBLE PRECISION NOT NULL DEFAULT 0") + cur.execute(""" CREATE TABLE IF NOT EXISTS chat_state ( - chat_id INTEGER NOT NULL, + chat_id BIGINT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (chat_id, key) ) - """ - ) - conn.execute( - """ + """) + cur.execute(""" CREATE TABLE IF NOT EXISTS talk_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - chat_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, + id BIGSERIAL PRIMARY KEY, + chat_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, role TEXT NOT NULL, name TEXT NOT NULL, text TEXT NOT NULL, - created_at REAL NOT NULL DEFAULT 0 + created_at DOUBLE PRECISION NOT NULL DEFAULT 0 ) - """ - ) - conn.execute( - """ + """) + cur.execute(""" CREATE TABLE IF NOT EXISTS talk_state ( - chat_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, + chat_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (chat_id, user_id, key) ) - """ - ) - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_id ON chat_history(chat_id, id)" - ) - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_role_id ON chat_history(chat_id, role, id)" - ) - conn.execute( - """ - CREATE INDEX IF NOT EXISTS idx_talk_history_chat_user_id - ON talk_history(chat_id, user_id, id) - """ - ) - conn.commit() + """) + cur.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_id ON chat_history(chat_id, id)") + cur.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_role_id ON chat_history(chat_id, role, id)") + cur.execute("CREATE INDEX IF NOT EXISTS idx_talk_history_chat_user_id ON talk_history(chat_id, user_id, id)") def _clean_text(text: str) -> str: @@ -422,78 +382,59 @@ def push_message( cleaned_text = _clean_text(text) if not cleaned_text: return - with _connect_db() as conn: + with get_conn() as conn: + cur = conn.cursor() if user_id is None: - conn.execute( - """ - INSERT INTO chat_history (chat_id, role, name, text, created_at) - VALUES (?, ?, ?, ?, ?) - """, + cur.execute( + "INSERT INTO chat_history (chat_id, role, name, text, created_at) VALUES (%s, %s, %s, %s, %s)", (chat_id, role, name, cleaned_text, time.time()), ) else: - conn.execute( - """ - INSERT INTO talk_history (chat_id, user_id, role, name, text, created_at) - VALUES (?, ?, ?, ?, ?, ?) - """, + cur.execute( + "INSERT INTO talk_history (chat_id, user_id, role, name, text, created_at) VALUES (%s, %s, %s, %s, %s, %s)", (chat_id, user_id, role, name, cleaned_text, time.time()), ) - conn.commit() def _get_summary(chat_id: int, user_id: int | None = None) -> str: - with _connect_db() as conn: + with get_conn() as conn: if user_id is None: return _get_state_from_conn(conn, chat_id, "summary", "") return _get_talk_state_from_conn(conn, chat_id, user_id, "summary", "") -def _get_history_rows(chat_id: int, user_id: int | None = None) -> list[sqlite3.Row]: - with _connect_db() as conn: +def _get_history_rows(chat_id: int, user_id: int | None = None) -> list: + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) if user_id is None: - return conn.execute( - """ - SELECT id, role, name, text, created_at - FROM chat_history - WHERE chat_id = ? - ORDER BY id ASC - """, + cur.execute( + "SELECT id, role, name, text, created_at FROM chat_history WHERE chat_id = %s ORDER BY id ASC", (chat_id,), - ).fetchall() - return conn.execute( - """ - SELECT id, role, name, text, created_at - FROM talk_history - WHERE chat_id = ? AND user_id = ? - ORDER BY id ASC - """, - (chat_id, user_id), - ).fetchall() + ) + else: + cur.execute( + "SELECT id, role, name, text, created_at FROM talk_history WHERE chat_id = %s AND user_id = %s ORDER BY id ASC", + (chat_id, user_id), + ) + return cur.fetchall() def _latest_reply_stats(chat_id: int) -> tuple[float, int]: - with _connect_db() as conn: - last_assistant = conn.execute( - """ - SELECT id, created_at - FROM chat_history - WHERE chat_id = ? AND role = 'assistant' - ORDER BY id DESC - LIMIT 1 - """, + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute( + "SELECT id, created_at FROM chat_history WHERE chat_id = %s AND role = 'assistant' ORDER BY id DESC LIMIT 1", (chat_id,), - ).fetchone() + ) + last_assistant = cur.fetchone() if not last_assistant: return 0.0, 10_000 - user_messages_since_reply = conn.execute( - """ - SELECT COUNT(*) - FROM chat_history - WHERE chat_id = ? AND role = 'user' AND id > ? - """, + cnt_cur = conn.cursor() + cnt_cur.execute( + "SELECT COUNT(*) FROM chat_history WHERE chat_id = %s AND role = 'user' AND id > %s", (chat_id, last_assistant["id"]), - ).fetchone()[0] + ) + user_messages_since_reply = cnt_cur.fetchone()[0] return float(last_assistant["created_at"] or 0.0), int(user_messages_since_reply) @@ -503,23 +444,23 @@ def _store_summary_and_prune( last_row_id: int, user_id: int | None = None, ) -> None: - with _connect_db() as conn: + with get_conn() as conn: + cur = conn.cursor() if user_id is None: _set_state_from_conn(conn, chat_id, "summary", summary) - conn.execute( - "DELETE FROM chat_history WHERE chat_id = ? AND id <= ?", + cur.execute( + "DELETE FROM chat_history WHERE chat_id = %s AND id <= %s", (chat_id, last_row_id), ) else: _set_talk_state_from_conn(conn, chat_id, user_id, "summary", summary) - conn.execute( - "DELETE FROM talk_history WHERE chat_id = ? AND user_id = ? AND id <= ?", + cur.execute( + "DELETE FROM talk_history WHERE chat_id = %s AND user_id = %s AND id <= %s", (chat_id, user_id, last_row_id), ) - conn.commit() -def _format_row_for_llm(row: sqlite3.Row) -> dict[str, str]: +def _format_row_for_llm(row) -> dict[str, str]: if row["role"] == "user": return { "role": "user", diff --git a/config.py b/config.py index f95ba63..b0c2354 100644 --- a/config.py +++ b/config.py @@ -7,10 +7,9 @@ from zoneinfo import ZoneInfo BASE_DIR = Path(__file__).parent FONT_PATH = BASE_DIR / "impact.ttf" DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow") +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://bot:botpass@postgres:5432/botdb") # --- Настройки /penis --- -PENIS_DB_PATH = "/db/penis_stats.sqlite3" -CHAT_HISTORY_DB_PATH = "/db/chat_history.sqlite3" PENIS_START_LENGTH = 10.0 PENIS_MIN_DELTA = 0.1 PENIS_MAX_DELTA = 7.0 @@ -34,6 +33,7 @@ E621_API_KEY = os.getenv("E621_API_KEY", "") # --- Ставки на матчи --- ODDS_API_KEY = os.getenv("ODDS_API_KEY", "") ODDS_API_BASE = "https://api.the-odds-api.com/v4" +BET_MAX_AMOUNT = 3.0 ODDS_SPORTS = { "epl": "soccer_epl", "ucl": "soccer_uefa_champs_league", @@ -43,8 +43,6 @@ ODDS_SPORTS = { "ligue1": "soccer_france_ligue_one", "europa": "soccer_uefa_europa_league", } -BET_MAX_AMOUNT = 3.0 -BET_DB_PATH = "/db/bets.sqlite3" ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно) # --- Nude --- diff --git a/db.py b/db.py new file mode 100644 index 0000000..d98fedb --- /dev/null +++ b/db.py @@ -0,0 +1,35 @@ +import logging +import os +from contextlib import contextmanager +from typing import Generator + +import psycopg2 +import psycopg2.extras +import psycopg2.pool + +logger = logging.getLogger(__name__) + +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://bot:botpass@postgres:5432/botdb") + +_pool: psycopg2.pool.ThreadedConnectionPool | None = None + + +def _get_pool() -> psycopg2.pool.ThreadedConnectionPool: + global _pool + if _pool is None: + _pool = psycopg2.pool.ThreadedConnectionPool(2, 10, dsn=DATABASE_URL) + return _pool + + +@contextmanager +def get_conn() -> Generator[psycopg2.extensions.connection, None, None]: + pool = _get_pool() + conn = pool.getconn() + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + pool.putconn(conn) diff --git a/docker-compose.yaml b/docker-compose.yaml index 27a8e65..c25410f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,19 @@ services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: botdb + POSTGRES_USER: bot + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-botpass} + volumes: + - ./db/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U bot -d botdb"] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + bot: build: context: . @@ -8,14 +23,15 @@ services: - .env environment: TZ: Europe/Moscow - PENIS_DB_PATH: /db/penis_stats.sqlite3 - CHAT_HISTORY_DB_PATH: /db/chat_history.sqlite3 + DATABASE_URL: postgresql://bot:${POSTGRES_PASSWORD:-botpass}@postgres:5432/botdb POLYCHAETSI_STATS_PATH: /db/polychaetsi_stats.json - BETS_DB_PATH: /db/bets.sqlite3 - ECONOMY_DB_PATH: /db/economy.sqlite3 + NUDE_HISTORY_PATH: /db/nude_history.json LLAMA_API_URL: ${LLAMA_API_URL:-https://mirror.porno4free.ru/zovos-ai/} LLAMA_FORCE_DISABLE_THINKING: ${LLAMA_FORCE_DISABLE_THINKING:-0} LLAMA_LOG_THINKING: ${LLAMA_LOG_THINKING:-1} + depends_on: + postgres: + condition: service_healthy init: true restart: unless-stopped @@ -29,14 +45,16 @@ services: - .env environment: TZ: Europe/Moscow - PENIS_DB_PATH: /db/penis_stats.sqlite3 - CHAT_HISTORY_DB_PATH: /db/chat_history.sqlite3 + DATABASE_URL: postgresql://bot:${POSTGRES_PASSWORD:-botpass}@postgres:5432/botdb POLYCHAETSI_STATS_PATH: /db/polychaetsi_stats.json - BET_DB_PATH: /db/bets.sqlite3 + NUDE_HISTORY_PATH: /db/nude_history.json API_PORT: "8080" LLAMA_API_URL: ${LLAMA_API_URL:-https://mirror.porno4free.ru/zovos-ai/} LLAMA_FORCE_DISABLE_THINKING: ${LLAMA_FORCE_DISABLE_THINKING:-0} LLAMA_LOG_THINKING: ${LLAMA_LOG_THINKING:-1} + depends_on: + postgres: + condition: service_healthy restart: unless-stopped webapp-frontend: @@ -59,5 +77,6 @@ services: restart: unless-stopped volumes: + postgres_data: certbot_etc: certbot_www: diff --git a/games/betting.py b/games/betting.py index d805de8..89570d0 100644 --- a/games/betting.py +++ b/games/betting.py @@ -1,13 +1,14 @@ -import time as _time -import sqlite3 import logging -from datetime import datetime, timedelta -from pathlib import Path +import time as _time +from datetime import datetime from zoneinfo import ZoneInfo import aiohttp +import psycopg2 +import psycopg2.extras import config +from db import get_conn from games.casino import get_user_length, update_user_length logger = logging.getLogger(__name__) @@ -17,18 +18,16 @@ _sports_cache: tuple[float, list] | None = None _last_viewed_sport_by_user: dict[int, str] = {} _DRAW_NAMES = {"draw", "tie", "ничья"} -# Сколько дней хранить историю ставок BET_HISTORY_DAYS = 30 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( - """ + with get_conn() as conn: + cur = conn.cursor() + cur.execute(''' CREATE TABLE IF NOT EXISTS bets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, + id SERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, match_id TEXT NOT NULL, sport TEXT NOT NULL, home_team TEXT NOT NULL, @@ -38,25 +37,15 @@ def _init_bets_db() -> None: odds REAL NOT NULL, status TEXT DEFAULT 'pending', payout REAL DEFAULT 0, - created_ts INTEGER, - resolved_ts INTEGER + created_ts BIGINT, + resolved_ts BIGINT ) - """ - ) - 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_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() + ''') + cur.execute("CREATE INDEX IF NOT EXISTS idx_bets_user ON bets(user_id, status)") + cur.execute("CREATE INDEX IF NOT EXISTS idx_bets_match ON bets(match_id, status)") + cur.execute("CREATE INDEX IF NOT EXISTS idx_bets_created ON bets(created_ts)") + cur.execute("ALTER TABLE bets ADD COLUMN IF NOT EXISTS payout REAL DEFAULT 0") + cur.execute("ALTER TABLE bets ADD COLUMN IF NOT EXISTS resolved_ts BIGINT") try: @@ -79,7 +68,6 @@ def get_user_sport_context(user_id: int) -> str | None: async def _fetch_available_sports() -> list[dict]: - """Получить список доступных видов спорта из API.""" global _sports_cache now = _time.time() if _sports_cache and (now - _sports_cache[0]) < 3600: @@ -287,11 +275,9 @@ async def get_formatted_matches(sport_alias: str | None) -> str: sport_key = config.ODDS_SPORTS[sport_alias] 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: @@ -304,10 +290,7 @@ async def get_formatted_matches(sport_alias: str | None) -> str: f"Похожие виды спорта:\n{suggest_text}\n\n" f"Обнови sport keys в config.py" ) - return ( - f"⚠️ Ключ {sport_key} не найден в API. " - f"Проверь ODDS_SPORTS в config.py" - ) + return f"⚠️ Ключ {sport_key} не найден в API. Проверь ODDS_SPORTS в config.py" return format_matches(matches, sport_alias, _get_sport_label(sport_alias)) parts = [] @@ -335,16 +318,12 @@ def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: f 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}. Не хватает, фраер." @@ -358,16 +337,18 @@ def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: f 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, payout, created_ts) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)""", - (user_id, match["id"], sport, match["home"], match["away"], - chosen_team, amount, team_odds, now_ts), - ) - conn.commit() - except sqlite3.Error: + with get_conn() as conn: + cur = conn.cursor() + cur.execute(''' + INSERT INTO bets + (user_id, match_id, sport, home_team, away_team, + chosen_team, amount, odds, status, payout, created_ts) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'pending', 0, %s) + ''', ( + user_id, match["id"], sport, match["home"], match["away"], + chosen_team, amount, team_odds, now_ts, + )) + except psycopg2.Error: logger.exception("Failed to place bet") update_user_length(user_id, amount) return "Ошибка БД." @@ -385,13 +366,15 @@ def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: f 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: + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute(''' + SELECT * FROM bets + WHERE user_id = %s AND status = 'pending' + ORDER BY created_ts DESC LIMIT 10 + ''', (user_id,)) + rows = cur.fetchall() + except psycopg2.Error: return "Ошибка БД." if not rows: @@ -413,34 +396,26 @@ def get_user_bets(user_id: int) -> str: def get_user_bet_history(user_id: int) -> str: - """Полная история ставок за последний месяц.""" - cutoff_ts = int((_time.time()) - BET_HISTORY_DAYS * 86400) + 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: + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute(''' + SELECT * FROM bets + WHERE user_id = %s AND created_ts >= %s + ORDER BY created_ts DESC LIMIT 50 + ''', (user_id, cutoff_ts)) + rows = cur.fetchall() + except psycopg2.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 - + total_bet = total_won = total_lost = 0.0 + wins = losses = pending = 0 lines = [f"📋 История ставок за {BET_HISTORY_DAYS} дней:\n"] for r in rows: @@ -453,7 +428,6 @@ def get_user_bet_history(user_id: int) -> str: if status == "won": wins += 1 - # Для старых ставок payout может быть 0, пересчитываем actual_payout = payout_raw if payout_raw > 0 else round(amount * odds_val, 1) total_won += actual_payout status_icon = "✅" @@ -466,8 +440,7 @@ def get_user_bet_history(user_id: int) -> str: else: pending += 1 status_icon = "⏳" - potential = round(amount * odds_val, 1) - result_text = f"ожидание → {potential:.1f} см" + result_text = f"ожидание → {round(amount * odds_val, 1):.1f} см" created = "" if r["created_ts"]: @@ -480,10 +453,8 @@ def get_user_bet_history(user_id: int) -> str: f" 📅 {created}" ) - # Итоговая статистика net = total_won - total_lost net_sign = "+" if net >= 0 else "" - lines.append("") lines.append("━━━ 📊 СТАТИСТИКА ━━━") lines.append(f"🎰 Всего ставок: {len(rows)}") @@ -495,29 +466,26 @@ def get_user_bet_history(user_id: int) -> str: 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}%") + lines.append(f"📊 Винрейт: {wins / (wins + losses) * 100:.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 < ?", + with get_conn() as conn: + cur = conn.cursor() + cur.execute( + "DELETE FROM bets WHERE status != 'pending' AND created_ts < %s", (cutoff_ts,), ) - conn.commit() - deleted = cursor.rowcount + deleted = cur.rowcount if deleted > 0: logger.info(f"Cleaned up {deleted} old bets") return deleted - except sqlite3.Error: + except psycopg2.Error: logger.exception("Failed to cleanup old bets") return 0 @@ -525,12 +493,11 @@ def cleanup_old_bets() -> int: 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: + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute("SELECT DISTINCT match_id, sport FROM bets WHERE status = 'pending'") + pending = cur.fetchall() + except psycopg2.Error: return notifications if not pending: @@ -557,29 +524,22 @@ async def settle_bets() -> list[str]: continue for event in scores_data: - if event.get("id") != match_id: - continue - if not event.get("completed"): + if event.get("id") != match_id or not event.get("completed"): continue scores = event.get("scores") if not scores: continue + score_values = [(s.get("name", ""), int(s.get("score", 0))) for s in scores] winner = None - max_score = -1 is_draw = False - score_values = [] - for s in scores: - 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: + max_score = -1 for name, score_val in score_values: if score_val > max_score: max_score = score_val @@ -589,19 +549,18 @@ async def settle_bets() -> list[str]: 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'", + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute( + "SELECT * FROM bets WHERE match_id = %s AND status = 'pending'", (match_id,), - ).fetchall() + ) + bets = cur.fetchall() + wcur = conn.cursor() for bet in bets: 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 @@ -609,8 +568,8 @@ async def settle_bets() -> list[str]: if bet_won: winnings = round(bet["amount"] * bet["odds"], 1) update_user_length(bet["user_id"], winnings) - conn.execute( - "UPDATE bets SET status = 'won', payout = ?, resolved_ts = ? WHERE id = ?", + wcur.execute( + "UPDATE bets SET status = 'won', payout = %s, resolved_ts = %s WHERE id = %s", (winnings, now_ts, bet["id"]), ) notifications.append( @@ -618,26 +577,22 @@ async def settle_bets() -> list[str]: f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" ) else: - conn.execute( - "UPDATE bets SET status = 'lost', payout = 0, resolved_ts = ? WHERE id = ?", + wcur.execute( + "UPDATE bets SET status = 'lost', payout = 0, resolved_ts = %s WHERE id = %s", (now_ts, 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: + except psycopg2.Error: logger.exception("Failed to settle bets") - # Очистка старых ставок cleanup_old_bets() - return notifications async def debug_sports() -> str: - """Диагностика: показать доступные виды спорта из API.""" if not config.ODDS_API_KEY: return "⚠️ ODDS_API_KEY не задан." @@ -645,17 +600,14 @@ async def debug_sports() -> str: 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", "") diff --git a/games/casino.py b/games/casino.py index 71d6edb..140ba6d 100644 --- a/games/casino.py +++ b/games/casino.py @@ -1,20 +1,20 @@ -import random -import sqlite3 import logging +import random + +import psycopg2.extras import config +from db import get_conn from .economy import economy logger = logging.getLogger(__name__) -# Символы слотов и их веса (чем выше вес — тем чаще выпадает) SLOT_SYMBOLS = ["🍒", "🍋", "🔔", "💎", "7️⃣", "🍀"] -SLOT_WEIGHTS = [30, 25, 20, 12, 8, 5] # 🍒 чаще всего, 🍀 реже всего +SLOT_WEIGHTS = [30, 25, 20, 12, 8, 5] -# Множители для выигрышей: {кол-во совпадений: (мин, макс)} MULTIPLIERS = { - 2: (1.2, 1.8), # две одинаковых - 3: (2.0, 3.0), # три одинаковых (джекпот, редко) + 2: (1.2, 1.8), + 3: (2.0, 3.0), } CASINO_WIN_CHANCE = 0.35 @@ -31,63 +31,53 @@ def spin_slots() -> tuple[list[str], int]: counts = {} for s in reels: counts[s] = counts.get(s, 0) + 1 - max_match = max(counts.values()) - return reels, max_match + return reels, max(counts.values()) def get_user_length(user_id: int) -> float | None: try: - with sqlite3.connect(config.PENIS_DB_PATH) as conn: - row = conn.execute( - "SELECT length FROM penis_stats WHERE user_id = ?", - (user_id,), - ).fetchone() - return float(row[0]) if row else None - except sqlite3.Error: - logger.exception("casino: failed to get length") + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,)) + row = cur.fetchone() + return float(row['balance']) if row else None + except Exception: + logger.exception("casino: failed to get balance") return None def update_user_length(user_id: int, delta: float) -> float | None: try: - with sqlite3.connect(config.PENIS_DB_PATH) as conn: - row = conn.execute( - "SELECT length FROM penis_stats WHERE user_id = ?", - (user_id,), - ).fetchone() + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,)) + row = cur.fetchone() if row is None: return None - new_length = round(float(row[0]) + delta, 1) - # Не даём уйти ниже 0 при проигрыше - if delta < 0 and new_length < 0: - new_length = 0.0 - conn.execute( - "UPDATE penis_stats SET length = ? WHERE user_id = ?", - (new_length, user_id), + new_balance = round(float(row['balance']) + delta, 1) + if delta < 0 and new_balance < 0: + new_balance = 0.0 + wcur = conn.cursor() + wcur.execute( + 'UPDATE user_balances SET balance = %s WHERE user_id = %s', + (new_balance, user_id), ) - conn.commit() - return new_length - except sqlite3.Error: - logger.exception("casino: failed to update length") + return new_balance + except Exception: + logger.exception("casino: failed to update balance") return None - def play_casino(user_id: int, bet: float) -> str: current = economy.get_user_balance(user_id) - - # Проверяем максимум ставки + if bet > config.CASINO_MAX_BET: return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай." - if bet <= 0: return "Ставка должна быть больше нуля, фраер." - - # Проверяем хватает ли if bet > current: return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер." - # Крутим reels, matches = spin_slots() slots_display = " | ".join(reels) @@ -95,10 +85,7 @@ def play_casino(user_id: int, bet: float) -> str: min_mult, max_mult = MULTIPLIERS[matches] multiplier = round(random.uniform(min_mult, max_mult), 1) winnings = round(bet * multiplier, 1) - - # Обновляем баланс в экономической системе economy.update_balance(user_id, winnings, 'casino_win', f'Выигрыш в казино x{multiplier}') - new_balance = economy.get_user_balance(user_id) if matches == 3: @@ -107,22 +94,21 @@ def play_casino(user_id: int, bet: float) -> str: f"🎉 ДЖЕКПОТ!!! Три одинаковых!\n" f"Множитель: x{multiplier}\n" f"Выигрыш: +{winnings:.1f} см\n" - f"� Баланс: {new_balance:.1f} см" + f"💰 Баланс: {new_balance:.1f} см" ) return ( f"🎰 {slots_display}\n\n" f"✅ Выигрыш! Две совпали.\n" f"Множитель: x{multiplier}\n" f"Выигрыш: +{winnings:.1f} см\n" - f"� Баланс: {new_balance:.1f} см" + f"💰 Баланс: {new_balance:.1f} см" ) else: - # Обновляем баланс в экономической системе economy.update_balance(user_id, -bet, 'casino_loss', f'Проигрыш в казино -{bet} см') new_balance = economy.get_user_balance(user_id) return ( f"🎰 {slots_display}\n\n" f"❌ Мимо, братуха.\n" f"Проигрыш: -{bet:.1f} см\n" - f"� Баланс: {new_balance:.1f} см" + f"💰 Баланс: {new_balance:.1f} см" ) diff --git a/games/economy.py b/games/economy.py index e5445a8..4308875 100644 --- a/games/economy.py +++ b/games/economy.py @@ -1,795 +1,669 @@ -import random -import sqlite3 -import json import logging -import os +import random from datetime import datetime, timedelta -from pathlib import Path -from typing import Dict, List, Optional, Tuple -import requests +from typing import Dict + +import psycopg2.extras + +from db import get_conn logger = logging.getLogger(__name__) -# Пути к базам данных -ECONOMY_DB_PATH = os.getenv("ECONOMY_DB_PATH", "/db/economy.sqlite3") class EconomyManager: def __init__(self): self.init_db() - - def init_db(self): - """Инициализация базы данных экономики""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Таблица балансов пользователей - cursor.execute(''' - CREATE TABLE IF NOT EXISTS user_balances ( - user_id INTEGER PRIMARY KEY, - balance REAL DEFAULT 0.0, - daily_income REAL DEFAULT 0.0, - last_daily_reset DATE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # Таблица вкладов - cursor.execute(''' - CREATE TABLE IF NOT EXISTS deposits ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - amount REAL, - interest_rate REAL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - matures_at TIMESTAMP, - is_active BOOLEAN DEFAULT 1 - ) - ''') - - # Таблица кредитов - cursor.execute(''' - CREATE TABLE IF NOT EXISTS loans ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - amount REAL, - interest_rate REAL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - due_at TIMESTAMP, - is_repaid BOOLEAN DEFAULT 0 - ) - ''') - - # Таблица транзакций - cursor.execute(''' - CREATE TABLE IF NOT EXISTS transactions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - from_user_id INTEGER, - to_user_id INTEGER, - amount REAL, - transaction_type TEXT, - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # Таблица налогов - cursor.execute(''' - CREATE TABLE IF NOT EXISTS taxes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - amount REAL, - tax_date DATE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # Таблица глобальных настроек экономики - cursor.execute(''' - CREATE TABLE IF NOT EXISTS economy_settings ( - key TEXT PRIMARY KEY, - value TEXT, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # Таблица ежедневной активности пользователей - cursor.execute(''' - CREATE TABLE IF NOT EXISTS daily_activity ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - activity_date DATE, - message_count INTEGER DEFAULT 0, - last_activity_time TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, activity_date) - ) - ''') - - # Таблица Центрального Банка - cursor.execute(''' - CREATE TABLE IF NOT EXISTS central_bank ( - id INTEGER PRIMARY KEY DEFAULT 1, - capital REAL DEFAULT 1000.0, - total_taxes_collected REAL DEFAULT 0.0, - total_inflation_adjustments REAL DEFAULT 0.0, - total_emissions REAL DEFAULT 0.0, - last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(id) - ) - ''') - # Кулдауны /penis (длина хранится в user_balances.balance) - cursor.execute(''' - CREATE TABLE IF NOT EXISTS penis_stats ( - user_id INTEGER PRIMARY KEY, - display_name TEXT NOT NULL DEFAULT '', - last_used_ts INTEGER - ) - ''') - - # Вставка настроек по умолчанию - default_settings = { - 'central_bank_rate': '7.5', # Ставка ЦБ РФ - 'deposit_min_amount': '10.0', - 'deposit_min_days': '7', - 'loan_max_amount': '100.0', - 'loan_interest_rate': '15.0', - 'tax_rate': '13.0', - 'transfer_limit_percent': '20.0', - 'inflation_rate': '5.0', - 'vanomasa_cooldown': '30', # дней между ваномаса - 'message_reward_rate': '0.01', # см за сообщение - 'last_activity_payout': '' # дата последней выплаты за активность - } - - for key, value in default_settings.items(): - cursor.execute('INSERT OR IGNORE INTO economy_settings (key, value) VALUES (?, ?)', (key, value)) - - # Инициализация Центрального Банка - cursor.execute('INSERT OR IGNORE INTO central_bank (id, capital) VALUES (1, 1000.0)') - - conn.commit() - conn.close() - + def init_db(self): + with get_conn() as conn: + cur = conn.cursor() + + cur.execute(''' + CREATE TABLE IF NOT EXISTS user_balances ( + user_id BIGINT PRIMARY KEY, + balance REAL DEFAULT 0.0, + daily_income REAL DEFAULT 0.0, + last_daily_reset DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + cur.execute(''' + CREATE TABLE IF NOT EXISTS deposits ( + id SERIAL PRIMARY KEY, + user_id BIGINT, + amount REAL, + interest_rate REAL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + matures_at TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE + ) + ''') + + cur.execute(''' + CREATE TABLE IF NOT EXISTS loans ( + id SERIAL PRIMARY KEY, + user_id BIGINT, + amount REAL, + interest_rate REAL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + due_at TIMESTAMP, + is_repaid BOOLEAN DEFAULT FALSE + ) + ''') + + cur.execute(''' + CREATE TABLE IF NOT EXISTS transactions ( + id SERIAL PRIMARY KEY, + from_user_id BIGINT, + to_user_id BIGINT, + amount REAL, + transaction_type TEXT, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + cur.execute(''' + CREATE TABLE IF NOT EXISTS taxes ( + id SERIAL PRIMARY KEY, + user_id BIGINT, + amount REAL, + tax_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + cur.execute(''' + CREATE TABLE IF NOT EXISTS economy_settings ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + cur.execute(''' + CREATE TABLE IF NOT EXISTS daily_activity ( + id SERIAL PRIMARY KEY, + user_id BIGINT, + activity_date DATE, + message_count INTEGER DEFAULT 0, + last_activity_time TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, activity_date) + ) + ''') + + cur.execute(''' + CREATE TABLE IF NOT EXISTS central_bank ( + id INTEGER PRIMARY KEY DEFAULT 1, + capital REAL DEFAULT 1000.0, + total_taxes_collected REAL DEFAULT 0.0, + total_inflation_adjustments REAL DEFAULT 0.0, + total_emissions REAL DEFAULT 0.0, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (id) + ) + ''') + + cur.execute(''' + CREATE TABLE IF NOT EXISTS penis_stats ( + user_id BIGINT PRIMARY KEY, + display_name TEXT NOT NULL DEFAULT '', + last_used_ts BIGINT + ) + ''') + + default_settings = { + 'central_bank_rate': '7.5', + 'deposit_min_amount': '10.0', + 'deposit_min_days': '7', + 'loan_max_amount': '100.0', + 'loan_interest_rate': '15.0', + 'tax_rate': '13.0', + 'transfer_limit_percent': '20.0', + 'inflation_rate': '5.0', + 'vanomasa_cooldown': '30', + 'message_reward_rate': '0.01', + 'last_activity_payout': '', + } + + for key, value in default_settings.items(): + cur.execute(''' + INSERT INTO economy_settings (key, value) VALUES (%s, %s) + ON CONFLICT (key) DO NOTHING + ''', (key, value)) + + cur.execute(''' + INSERT INTO central_bank (id, capital) VALUES (1, 1000.0) + ON CONFLICT (id) DO NOTHING + ''') + def get_user_balance(self, user_id: int) -> float: - """Получить баланс пользователя""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - cursor.execute('SELECT balance FROM user_balances WHERE user_id = ?', (user_id,)) - result = cursor.fetchone() - conn.close() - return result[0] if result else 0.0 - + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,)) + result = cur.fetchone() + return result['balance'] if result else 0.0 + def update_balance(self, user_id: int, amount: float, transaction_type: str, description: str = ""): - """Обновить баланс пользователя""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Вставляем или обновляем баланс - cursor.execute(''' - INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) - VALUES (?, ?, 0.0, date('now')) - ON CONFLICT(user_id) DO UPDATE SET balance = balance + ? - ''', (user_id, amount, amount)) - - # Записываем транзакцию - cursor.execute(''' - INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) - VALUES (?, ?, ?, ?, ?) - ''', (user_id, user_id, amount, transaction_type, description)) - - conn.commit() - conn.close() - + with get_conn() as conn: + cur = conn.cursor() + cur.execute(''' + INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) + VALUES (%s, %s, 0.0, CURRENT_DATE) + ON CONFLICT (user_id) DO UPDATE SET balance = user_balances.balance + %s + ''', (user_id, amount, amount)) + + cur.execute(''' + INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) + VALUES (%s, %s, %s, %s, %s) + ''', (user_id, user_id, amount, transaction_type, description)) + logger.info(f"Balance updated for user {user_id}: +{amount} ({transaction_type})") - + def transfer_money(self, from_user_id: int, to_user_id: int, amount: float, description: str = "") -> bool: - """Перевод денег между пользователями""" if amount <= 0: return False - + from_balance = self.get_user_balance(from_user_id) - transfer_limit = from_balance * 0.2 # 20% от капитала - - if amount > transfer_limit: + if amount > from_balance * 0.2 or from_balance < amount: return False - - if from_balance < amount: - return False - - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Обновляем балансы - cursor.execute(''' - UPDATE user_balances SET balance = balance - ? WHERE user_id = ? - ''', (amount, from_user_id)) - - cursor.execute(''' - INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) - VALUES (?, ?, 0.0, date('now')) - ON CONFLICT(user_id) DO UPDATE SET balance = balance + ? - ''', (to_user_id, amount, amount)) - - # Записываем транзакцию - cursor.execute(''' - INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) - VALUES (?, ?, ?, ?, ?) - ''', (from_user_id, to_user_id, amount, 'transfer', description)) - - conn.commit() - conn.close() - + + with get_conn() as conn: + cur = conn.cursor() + cur.execute( + 'UPDATE user_balances SET balance = balance - %s WHERE user_id = %s', + (amount, from_user_id), + ) + cur.execute(''' + INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) + VALUES (%s, %s, 0.0, CURRENT_DATE) + ON CONFLICT (user_id) DO UPDATE SET balance = user_balances.balance + %s + ''', (to_user_id, amount, amount)) + cur.execute(''' + INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) + VALUES (%s, %s, %s, 'transfer', %s) + ''', (from_user_id, to_user_id, amount, description)) + logger.info(f"Transfer: {amount} from user {from_user_id} to user {to_user_id}") return True - + def create_deposit(self, user_id: int, amount: float, days: int) -> bool: - """Создать вклад""" if amount <= 0: return False - + balance = self.get_user_balance(user_id) min_deposit = float(self.get_setting('deposit_min_amount')) - - if amount < min_deposit: + + if amount < min_deposit or balance < amount: return False - - if balance < amount: - return False - + central_bank_rate = float(self.get_setting('central_bank_rate')) matures_at = datetime.now() + timedelta(days=days) - - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Списываем деньги со счета пользователя → передаём в ЦБ - cursor.execute('UPDATE user_balances SET balance = balance - ? WHERE user_id = ?', (amount, user_id)) - cursor.execute(''' - UPDATE central_bank SET - capital = capital + ?, - last_updated = CURRENT_TIMESTAMP - WHERE id = 1 - ''', (amount,)) - # Создаем вклад - cursor.execute(''' - INSERT INTO deposits (user_id, amount, interest_rate, matures_at) - VALUES (?, ?, ?, ?) - ''', (user_id, amount, central_bank_rate, matures_at)) - - conn.commit() - conn.close() + with get_conn() as conn: + cur = conn.cursor() + cur.execute( + 'UPDATE user_balances SET balance = balance - %s WHERE user_id = %s', + (amount, user_id), + ) + cur.execute(''' + UPDATE central_bank SET capital = capital + %s, last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (amount,)) + cur.execute(''' + INSERT INTO deposits (user_id, amount, interest_rate, matures_at) + VALUES (%s, %s, %s, %s) + ''', (user_id, amount, central_bank_rate, matures_at)) logger.info(f"Deposit created: user {user_id}, amount {amount}, rate {central_bank_rate}%") return True - + def create_loan(self, user_id: int, amount: float, days: int) -> bool: - """Создать кредит""" if amount <= 0: return False - + max_loan = float(self.get_setting('loan_max_amount')) loan_rate = float(self.get_setting('loan_interest_rate')) - + if amount > max_loan: return False - + due_at = datetime.now() + timedelta(days=days) - - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # ЦБ выдаёт кредит: уменьшаем капитал ЦБ, зачисляем пользователю - cursor.execute(''' - UPDATE central_bank SET - capital = capital - ?, - last_updated = CURRENT_TIMESTAMP - WHERE id = 1 - ''', (amount,)) - cursor.execute(''' - INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) - VALUES (?, ?, 0.0, date('now')) - ON CONFLICT(user_id) DO UPDATE SET balance = balance + ? - ''', (user_id, amount, amount)) - # Создаем кредит - cursor.execute(''' - INSERT INTO loans (user_id, amount, interest_rate, due_at) - VALUES (?, ?, ?, ?) - ''', (user_id, amount, loan_rate, due_at)) - - conn.commit() - conn.close() + with get_conn() as conn: + cur = conn.cursor() + cur.execute(''' + UPDATE central_bank SET capital = capital - %s, last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (amount,)) + cur.execute(''' + INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) + VALUES (%s, %s, 0.0, CURRENT_DATE) + ON CONFLICT (user_id) DO UPDATE SET balance = user_balances.balance + %s + ''', (user_id, amount, amount)) + cur.execute(''' + INSERT INTO loans (user_id, amount, interest_rate, due_at) + VALUES (%s, %s, %s, %s) + ''', (user_id, amount, loan_rate, due_at)) logger.info(f"Loan created: user {user_id}, amount {amount}, rate {loan_rate}%") return True - + def repay_loan(self, user_id: int, loan_id: int) -> bool: - """Погасить кредит""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - cursor.execute('SELECT amount, interest_rate, is_repaid FROM loans WHERE id = ? AND user_id = ?', (loan_id, user_id)) - loan = cursor.fetchone() - - if not loan or loan[2]: # Кредит не найден или уже погашен - conn.close() - return False - - amount, rate, _ = loan - total_repayment = amount * (1 + rate / 100) - - balance = self.get_user_balance(user_id) - if balance < total_repayment: - conn.close() - return False - - # Списываем деньги у пользователя → возвращаем в ЦБ - cursor.execute('UPDATE user_balances SET balance = balance - ? WHERE user_id = ?', (total_repayment, user_id)) - cursor.execute(''' - UPDATE central_bank SET - capital = capital + ?, - last_updated = CURRENT_TIMESTAMP - WHERE id = 1 - ''', (total_repayment,)) + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute( + 'SELECT amount, interest_rate, is_repaid FROM loans WHERE id = %s AND user_id = %s', + (loan_id, user_id), + ) + loan = cur.fetchone() - # Отмечаем кредит как погашенный - cursor.execute('UPDATE loans SET is_repaid = 1 WHERE id = ?', (loan_id,)) + if not loan or loan['is_repaid']: + return False - conn.commit() - conn.close() + total_repayment = loan['amount'] * (1 + loan['interest_rate'] / 100) + + balance = self.get_user_balance(user_id) + if balance < total_repayment: + return False + + wcur = conn.cursor() + wcur.execute( + 'UPDATE user_balances SET balance = balance - %s WHERE user_id = %s', + (total_repayment, user_id), + ) + wcur.execute(''' + UPDATE central_bank SET capital = capital + %s, last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (total_repayment,)) + wcur.execute('UPDATE loans SET is_repaid = TRUE WHERE id = %s', (loan_id,)) logger.info(f"Loan repaid: user {user_id}, loan {loan_id}, amount {total_repayment}") return True - + def collect_daily_taxes(self): - """Сбор ежедневных налогов""" tax_rate = float(self.get_setting('tax_rate')) / 100 - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Получаем всех пользователей с ежедневным доходом - cursor.execute(''' - SELECT user_id, daily_income FROM user_balances - WHERE daily_income > 0 AND (last_daily_reset != date('now') OR last_daily_reset IS NULL) - ''') - - users = cursor.fetchall() - - total_taxes_collected = 0.0 - - for user_id, daily_income in users: - tax_amount = daily_income * tax_rate - if tax_amount > 0: - # Списываем налог - cursor.execute( - 'UPDATE user_balances SET balance = balance - ?, daily_income = 0, last_daily_reset = date(\'now\') WHERE user_id = ?', - (tax_amount, user_id) + + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute(''' + SELECT user_id, daily_income FROM user_balances + WHERE daily_income > 0 AND (last_daily_reset != CURRENT_DATE OR last_daily_reset IS NULL) + ''') + users = cur.fetchall() + + total_taxes_collected = 0.0 + wcur = conn.cursor() + + for row in users: + user_id = row['user_id'] + daily_income = row['daily_income'] + tax_amount = daily_income * tax_rate + if tax_amount <= 0: + continue + + wcur.execute(''' + UPDATE user_balances + SET balance = balance - %s, daily_income = 0, last_daily_reset = CURRENT_DATE + WHERE user_id = %s + ''', (tax_amount, user_id)) + wcur.execute( + 'INSERT INTO taxes (user_id, amount, tax_date) VALUES (%s, %s, CURRENT_DATE)', + (user_id, tax_amount), ) - - # Записываем налог - cursor.execute('INSERT INTO taxes (user_id, amount, tax_date) VALUES (?, ?, date("now"))', (user_id, tax_amount)) - total_taxes_collected += tax_amount - - # Переводим собранные налоги в Центральный Банк - if total_taxes_collected > 0: - cursor.execute('UPDATE central_bank SET capital = capital + ?, total_taxes_collected = total_taxes_collected + ? WHERE id = 1', - (total_taxes_collected, total_taxes_collected)) - - conn.commit() - conn.close() - + + if total_taxes_collected > 0: + wcur.execute(''' + UPDATE central_bank + SET capital = capital + %s, total_taxes_collected = total_taxes_collected + %s + WHERE id = 1 + ''', (total_taxes_collected, total_taxes_collected)) + logger.info(f"Daily taxes collected: {total_taxes_collected:.2f} cm from {len(users)} users") - + def count_message(self, user_id: int): - """Учесть сообщение пользователя""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - today = datetime.now().strftime('%Y-%m-%d') - - # Вставляем или обновляем счетчик сообщений за сегодня - cursor.execute(''' - INSERT INTO daily_activity (user_id, activity_date, message_count, last_activity_time) - VALUES (?, ?, 1, CURRENT_TIMESTAMP) - ON CONFLICT(user_id, activity_date) - DO UPDATE SET - message_count = message_count + 1, - last_activity_time = CURRENT_TIMESTAMP - ''', (user_id, today)) - - conn.commit() - conn.close() - + with get_conn() as conn: + cur = conn.cursor() + cur.execute(''' + INSERT INTO daily_activity (user_id, activity_date, message_count, last_activity_time) + VALUES (%s, %s, 1, CURRENT_TIMESTAMP) + ON CONFLICT (user_id, activity_date) DO UPDATE SET + message_count = daily_activity.message_count + 1, + last_activity_time = CURRENT_TIMESTAMP + ''', (user_id, today)) + def pay_daily_activity_rewards(self) -> int: - """Начислить вознаграждения за ежедневную активность""" reward_rate = float(self.get_setting('message_reward_rate')) last_payout = self.get_setting('last_activity_payout') today = datetime.now().strftime('%Y-%m-%d') - - # Проверяем, не выплачивали ли уже сегодня + if last_payout == today: return 0 - - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Получаем всех пользователей с активностью за вчерашний день + yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') - cursor.execute(''' - SELECT user_id, message_count FROM daily_activity - WHERE activity_date = ? AND message_count > 0 - ''', (yesterday,)) - - users = cursor.fetchall() - total_paid = 0 - - for user_id, message_count in users: - reward = message_count * reward_rate - if reward > 0: - # Обновляем баланс - cursor.execute(''' + + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute(''' + SELECT user_id, message_count FROM daily_activity + WHERE activity_date = %s AND message_count > 0 + ''', (yesterday,)) + users = cur.fetchall() + + total_paid = 0.0 + wcur = conn.cursor() + + for row in users: + user_id = row['user_id'] + message_count = row['message_count'] + reward = message_count * reward_rate + if reward <= 0: + continue + + wcur.execute(''' INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) - VALUES (?, ?, 0.0, date('now')) - ON CONFLICT(user_id) DO UPDATE SET - balance = balance + ?, - daily_income = daily_income + ? + VALUES (%s, %s, 0.0, CURRENT_DATE) + ON CONFLICT (user_id) DO UPDATE SET + balance = user_balances.balance + %s, + daily_income = user_balances.daily_income + %s ''', (user_id, reward, reward, reward)) - - # Записываем транзакцию - cursor.execute(''' + wcur.execute(''' INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) - VALUES (?, ?, ?, ?, ?) - ''', (user_id, user_id, reward, 'activity_reward', f'Вознаграждение за {message_count} сообщений')) - + VALUES (%s, %s, %s, 'activity_reward', %s) + ''', (user_id, user_id, reward, f'Вознаграждение за {message_count} сообщений')) total_paid += reward - - # Обновляем дату последней выплаты - cursor.execute('UPDATE economy_settings SET value = ? WHERE key = ?', (today, 'last_activity_payout')) - - conn.commit() - conn.close() - + + wcur.execute( + 'UPDATE economy_settings SET value = %s WHERE key = %s', + (today, 'last_activity_payout'), + ) + logger.info(f"Activity rewards paid: {total_paid:.2f} cm to {len(users)} users") return len(users) - + def get_central_bank_stats(self) -> Dict: - """Получить статистику Центрального Банка""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - cursor.execute('SELECT capital, total_taxes_collected, total_inflation_adjustments, total_emissions FROM central_bank WHERE id = 1') - result = cursor.fetchone() - - conn.close() - + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute(''' + SELECT capital, total_taxes_collected, total_inflation_adjustments, total_emissions + FROM central_bank WHERE id = 1 + ''') + result = cur.fetchone() + if result: return { - 'capital': result[0], - 'total_taxes_collected': result[1], - 'total_inflation_adjustments': result[2], - 'total_emissions': result[3] + 'capital': result['capital'], + 'total_taxes_collected': result['total_taxes_collected'], + 'total_inflation_adjustments': result['total_inflation_adjustments'], + 'total_emissions': result['total_emissions'], } - else: - return { - 'capital': 1000.0, - 'total_taxes_collected': 0.0, - 'total_inflation_adjustments': 0.0, - 'total_emissions': 0.0 - } - + return { + 'capital': 1000.0, + 'total_taxes_collected': 0.0, + 'total_inflation_adjustments': 0.0, + 'total_emissions': 0.0, + } + def update_central_bank(self, capital_change: float, transaction_type: str, description: str = ""): - """Обновить капитал Центрального Банка""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Обновляем капитал - cursor.execute(''' - UPDATE central_bank SET - capital = capital + ?, - last_updated = CURRENT_TIMESTAMP - WHERE id = 1 - ''', (capital_change,)) - - # Обновляем соответствующую статистику - if transaction_type == 'tax': - cursor.execute('UPDATE central_bank SET total_taxes_collected = total_taxes_collected + ? WHERE id = 1', (abs(capital_change),)) - elif transaction_type == 'inflation': - cursor.execute('UPDATE central_bank SET total_inflation_adjustments = total_inflation_adjustments + ? WHERE id = 1', (abs(capital_change),)) - elif transaction_type == 'emission': - cursor.execute('UPDATE central_bank SET total_emissions = total_emissions + ? WHERE id = 1', (abs(capital_change),)) - - conn.commit() - conn.close() - + with get_conn() as conn: + cur = conn.cursor() + cur.execute(''' + UPDATE central_bank SET capital = capital + %s, last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (capital_change,)) + + if transaction_type == 'tax': + cur.execute(''' + UPDATE central_bank + SET total_taxes_collected = total_taxes_collected + %s WHERE id = 1 + ''', (abs(capital_change),)) + elif transaction_type == 'inflation': + cur.execute(''' + UPDATE central_bank + SET total_inflation_adjustments = total_inflation_adjustments + %s WHERE id = 1 + ''', (abs(capital_change),)) + elif transaction_type == 'emission': + cur.execute(''' + UPDATE central_bank + SET total_emissions = total_emissions + %s WHERE id = 1 + ''', (abs(capital_change),)) + logger.info(f"Central bank updated: {capital_change} ({transaction_type})") - + def emit_money(self, amount: float, reason: str = "") -> bool: - """Эмиссия денег - добавление денег в систему из ЦБ""" - cb_stats = self.get_central_bank_stats() - - if cb_stats['capital'] < amount: + if self.get_central_bank_stats()['capital'] < amount: return False - - # Уменьшаем капитал ЦБ self.update_central_bank(-amount, 'emission', reason) - - # Здесь можно добавить логику распределения денег - # Например, пропорционально текущим балансам или равными долями - logger.info(f"Money emitted: {amount} cm ({reason})") return True - + def vanomasa(self) -> bool: - """Полное обнуление всех балансов""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Проверяем кулдаун - cursor.execute('SELECT value FROM economy_settings WHERE key = "last_vanomasa"') - last_vanomasa = cursor.fetchone() - - if last_vanomasa: - last_date = datetime.strptime(last_vanomasa[0], '%Y-%m-%d') - cooldown_days = int(self.get_setting('vanomasa_cooldown')) - if datetime.now() - last_date < timedelta(days=cooldown_days): - conn.close() - return False - - # Обнуляем все балансы - cursor.execute('UPDATE user_balances SET balance = 0, daily_income = 0') - - # Записываем дату ваномаса - cursor.execute('INSERT OR REPLACE INTO economy_settings (key, value, updated_at) VALUES ("last_vanomasa", date("now"), CURRENT_TIMESTAMP)') - - conn.commit() - conn.close() - + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute("SELECT value FROM economy_settings WHERE key = %s", ('last_vanomasa',)) + last_vanomasa = cur.fetchone() + + if last_vanomasa and last_vanomasa['value']: + try: + last_date = datetime.strptime(last_vanomasa['value'], '%Y-%m-%d') + cooldown_days = int(self.get_setting('vanomasa_cooldown')) + if datetime.now() - last_date < timedelta(days=cooldown_days): + return False + except ValueError: + pass + + today = datetime.now().strftime('%Y-%m-%d') + wcur = conn.cursor() + wcur.execute('UPDATE user_balances SET balance = 0, daily_income = 0') + wcur.execute(''' + INSERT INTO economy_settings (key, value, updated_at) VALUES (%s, %s, CURRENT_TIMESTAMP) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP + ''', ('last_vanomasa', today)) + logger.warning("VANOMASA: All balances reset to zero!") return True - + def get_setting(self, key: str) -> str: - """Получить настройку экономики""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - cursor.execute('SELECT value FROM economy_settings WHERE key = ?', (key,)) - result = cursor.fetchone() - conn.close() - return result[0] if result else '0' - + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute('SELECT value FROM economy_settings WHERE key = %s', (key,)) + result = cur.fetchone() + return result['value'] if result else '0' + def update_setting(self, key: str, value: str): - """Обновить настройку экономики""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - cursor.execute('INSERT OR REPLACE INTO economy_settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)', (key, value)) - conn.commit() - conn.close() - + with get_conn() as conn: + cur = conn.cursor() + cur.execute(''' + INSERT INTO economy_settings (key, value, updated_at) VALUES (%s, %s, CURRENT_TIMESTAMP) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP + ''', (key, value)) + def get_user_stats(self, user_id: int) -> Dict: - """Получить статистику пользователя""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Баланс - cursor.execute('SELECT balance FROM user_balances WHERE user_id = ?', (user_id,)) - row = cursor.fetchone() - balance = row[0] if row else 0.0 - - # Активные вклады - cursor.execute('SELECT COUNT(*), SUM(amount) FROM deposits WHERE user_id = ? AND is_active = 1', (user_id,)) - deposits = cursor.fetchone() - - # Активные кредиты - cursor.execute('SELECT COUNT(*), SUM(amount) FROM loans WHERE user_id = ? AND is_repaid = 0', (user_id,)) - loans = cursor.fetchone() - - # Получаем статистику сообщений за сегодня today = datetime.now().strftime('%Y-%m-%d') - cursor.execute('SELECT message_count FROM daily_activity WHERE user_id = ? AND activity_date = ?', (user_id, today)) - today_messages = cursor.fetchone() - - # Получаем статистику сообщений за вчера yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') - cursor.execute('SELECT message_count FROM daily_activity WHERE user_id = ? AND activity_date = ?', (user_id, yesterday)) - yesterday_messages = cursor.fetchone() - conn.close() - + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + + cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,)) + row = cur.fetchone() + balance = row['balance'] if row else 0.0 + + cur.execute(''' + SELECT COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total + FROM deposits WHERE user_id = %s AND is_active = TRUE + ''', (user_id,)) + deposits = cur.fetchone() + + cur.execute(''' + SELECT COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total + FROM loans WHERE user_id = %s AND is_repaid = FALSE + ''', (user_id,)) + loans = cur.fetchone() + + cur.execute( + 'SELECT message_count FROM daily_activity WHERE user_id = %s AND activity_date = %s', + (user_id, today), + ) + today_row = cur.fetchone() + + cur.execute( + 'SELECT message_count FROM daily_activity WHERE user_id = %s AND activity_date = %s', + (user_id, yesterday), + ) + yesterday_row = cur.fetchone() + return { 'balance': balance, - 'active_deposits_count': deposits[0] or 0, - 'active_deposits_sum': deposits[1] or 0.0, - 'active_loans_count': loans[0] or 0, - 'active_loans_sum': loans[1] or 0.0, - 'today_messages': today_messages[0] if today_messages else 0, - 'yesterday_messages': yesterday_messages[0] if yesterday_messages else 0 + 'active_deposits_count': deposits['cnt'] or 0, + 'active_deposits_sum': deposits['total'] or 0.0, + 'active_loans_count': loans['cnt'] or 0, + 'active_loans_sum': loans['total'] or 0.0, + 'today_messages': today_row['message_count'] if today_row else 0, + 'yesterday_messages': yesterday_row['message_count'] if yesterday_row else 0, } - + def regulate_inflation(self): - """Регулирование инфляции через Центральный Банк""" inflation_rate = float(self.get_setting('inflation_rate')) - total_supply = 0.0 - user_count = 0 - - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - - # Получаем общую массу денег и количество пользователей - cursor.execute('SELECT SUM(balance), COUNT(*) FROM user_balances') - result = cursor.fetchone() - if result[0]: - total_supply = result[0] - user_count = result[1] - - # Получаем статистику ЦБ - cursor.execute('SELECT capital FROM central_bank WHERE id = 1') - cb_result = cursor.fetchone() - cb_capital = cb_result[0] if cb_result else 1000.0 - - conn.close() - - # Если инфляция высокая, ЦБ может скупать деньги с рынка + + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute('SELECT SUM(balance) AS total_balance, COUNT(*) AS user_count FROM user_balances') + result = cur.fetchone() + total_supply = result['total_balance'] or 0.0 + + cur.execute('SELECT capital FROM central_bank WHERE id = 1') + cb_result = cur.fetchone() + cb_capital = cb_result['capital'] if cb_result else 1000.0 + if inflation_rate > 10.0 and total_supply > 1000 and cb_capital > 100: - # ЦБ скупает деньги для борьбы с инфляцией - buyback_amount = min(total_supply * 0.05, cb_capital * 0.1) # 5% от массы или 10% от капитала ЦБ - - # Уменьшаем капитал ЦБ (скупка денег) - self.update_central_bank(-buyback_amount, 'inflation', f'Anti-inflation measures: buying back {buyback_amount:.2f} cm') - - # Увеличиваем налоги для дополнительной борьбы с инфляцией + buyback_amount = min(total_supply * 0.05, cb_capital * 0.1) + self.update_central_bank(-buyback_amount, 'inflation', f'Anti-inflation: {buyback_amount:.2f} cm') new_tax_rate = min(20.0, 13.0 + (inflation_rate - 10.0) * 0.5) self.update_setting('tax_rate', str(new_tax_rate)) - - logger.info(f"Inflation regulation: CB bought back {buyback_amount:.2f} cm, tax rate increased to {new_tax_rate}%") - - # Если дефляция (низкая инфляция) и много денег в ЦБ, можно делать эмиссию + logger.info(f"Inflation regulation: bought back {buyback_amount:.2f} cm, tax={new_tax_rate}%") + elif inflation_rate < 2.0 and total_supply < 500 and cb_capital > 200: - emission_amount = cb_capital * 0.05 # 5% от капитала ЦБ - - # Эмиссия денег через ЦБ - self.update_central_bank(-emission_amount, 'emission', f'Economic stimulus: emitting {emission_amount:.2f} cm') - - # Уменьшаем налоги для стимуляции экономики + emission_amount = cb_capital * 0.05 + self.update_central_bank(-emission_amount, 'emission', f'Stimulus: {emission_amount:.2f} cm') new_tax_rate = max(10.0, 13.0 - (2.0 - inflation_rate) * 1.5) self.update_setting('tax_rate', str(new_tax_rate)) - - logger.info(f"Economic stimulus: CB emitted {emission_amount:.2f} cm, tax rate decreased to {new_tax_rate}%") + logger.info(f"Economic stimulus: emitted {emission_amount:.2f} cm, tax={new_tax_rate}%") def play_penis(self, user_id: int, display_name: str, now_ts: int, start_length: float, cooldown_seconds: int, min_delta: float, max_delta: float) -> dict: - """ - Возвращает dict: - на кулдауне: {'allowed': False, 'left_seconds': N} - успех: {'allowed': True, 'new_balance': X, 'delta': Y, 'sign': 1 или -1} - """ - conn = sqlite3.connect(ECONOMY_DB_PATH) - try: - conn.row_factory = sqlite3.Row - cursor = conn.cursor() + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - # Текущий баланс (он же «длина») — стартуем с start_length для новых - cursor.execute('SELECT balance FROM user_balances WHERE user_id = ?', (user_id,)) - row = cursor.fetchone() + cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,)) + row = cur.fetchone() current_balance = row['balance'] if row else start_length - # Кулдаун из penis_stats - cursor.execute('SELECT last_used_ts, display_name FROM penis_stats WHERE user_id = ?', (user_id,)) - ps_row = cursor.fetchone() + cur.execute( + 'SELECT last_used_ts, display_name FROM penis_stats WHERE user_id = %s', + (user_id,), + ) + ps_row = cur.fetchone() last_used_ts = ps_row['last_used_ts'] if ps_row else None if last_used_ts is not None: next_ts = int(last_used_ts) + cooldown_seconds if now_ts < next_ts: if ps_row and ps_row['display_name'] != display_name: - cursor.execute( - 'UPDATE penis_stats SET display_name = ? WHERE user_id = ?', - (display_name, user_id) + wcur = conn.cursor() + wcur.execute( + 'UPDATE penis_stats SET display_name = %s WHERE user_id = %s', + (display_name, user_id), ) - conn.commit() return {'allowed': False, 'left_seconds': next_ts - now_ts} delta = round(random.uniform(min_delta, max_delta), 1) sign = random.choice([-1, 1]) new_balance = round(max(0.1, current_balance + sign * delta), 1) - # Обновляем или создаём запись в user_balances - cursor.execute(''' + wcur = conn.cursor() + wcur.execute(''' INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) - VALUES (?, ?, 0.0, date('now')) - ON CONFLICT(user_id) DO UPDATE SET balance = ? + VALUES (%s, %s, 0.0, CURRENT_DATE) + ON CONFLICT (user_id) DO UPDATE SET balance = %s ''', (user_id, new_balance, new_balance)) sign_label = f'+{delta:.1f}' if sign > 0 else f'-{delta:.1f}' - cursor.execute(''' + wcur.execute(''' INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) - VALUES (?, ?, ?, 'penis_game', ?) + VALUES (%s, %s, %s, 'penis_game', %s) ''', (user_id, user_id, sign * delta, f'Игра /penis: {sign_label} см')) - # Обновляем кулдаун if ps_row is None: - cursor.execute( - 'INSERT INTO penis_stats (user_id, display_name, last_used_ts) VALUES (?, ?, ?)', - (user_id, display_name, now_ts) + wcur.execute( + 'INSERT INTO penis_stats (user_id, display_name, last_used_ts) VALUES (%s, %s, %s)', + (user_id, display_name, now_ts), ) else: - cursor.execute( - 'UPDATE penis_stats SET display_name = ?, last_used_ts = ? WHERE user_id = ?', - (display_name, now_ts, user_id) + wcur.execute( + 'UPDATE penis_stats SET display_name = %s, last_used_ts = %s WHERE user_id = %s', + (display_name, now_ts, user_id), ) - conn.commit() return {'allowed': True, 'new_balance': new_balance, 'delta': delta, 'sign': sign} - except Exception: - conn.rollback() - raise - finally: - conn.close() def get_penis_top(self, limit: int) -> list: - """Топ пользователей по балансу (только игравшие в /penis).""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute(''' - SELECT ub.user_id, ps.display_name, ub.balance - FROM user_balances ub - INNER JOIN penis_stats ps ON ub.user_id = ps.user_id - ORDER BY ub.balance DESC, ub.user_id ASC - LIMIT ? - ''', (limit,)) - rows = cursor.fetchall() - conn.close() + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute(''' + SELECT ub.user_id, ps.display_name, ub.balance + FROM user_balances ub + INNER JOIN penis_stats ps ON ub.user_id = ps.user_id + ORDER BY ub.balance DESC, ub.user_id ASC + LIMIT %s + ''', (limit,)) + rows = cur.fetchall() return [{'user_id': r['user_id'], 'display_name': r['display_name'], 'balance': r['balance']} for r in rows] def process_matured_deposits(self) -> int: - """Выплачивает созревшие вклады из капитала ЦБ. Возвращает кол-во выплат.""" - conn = sqlite3.connect(ECONOMY_DB_PATH) - cursor = conn.cursor() - now = datetime.now().isoformat() + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute(''' + SELECT id, user_id, amount, interest_rate + FROM deposits + WHERE is_active = TRUE AND matures_at <= NOW() + ''') + matured = cur.fetchall() - cursor.execute(''' - SELECT id, user_id, amount, interest_rate - FROM deposits - WHERE is_active = 1 AND matures_at <= ? - ''', (now,)) - matured = cursor.fetchall() + paid = 0 + wcur = conn.cursor() + for row in matured: + dep_id = row['id'] + user_id = row['user_id'] + amount = row['amount'] + rate = row['interest_rate'] + payout = round(amount * (1 + rate / 100 * 1 / 365 * 30), 2) - paid = 0 - for dep_id, user_id, amount, rate in matured: - payout = round(amount * (1 + rate / 100 * 1 / 365 * 30), 2) # ~месячный доход - - # ЦБ выплачивает пользователю основную сумму + проценты - cursor.execute(''' - INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) - VALUES (?, ?, 0.0, date('now')) - ON CONFLICT(user_id) DO UPDATE SET balance = balance + ? - ''', (user_id, payout, payout)) - cursor.execute(''' - UPDATE central_bank SET - capital = capital - ?, - last_updated = CURRENT_TIMESTAMP - WHERE id = 1 - ''', (payout,)) - cursor.execute('UPDATE deposits SET is_active = 0 WHERE id = ?', (dep_id,)) - - cursor.execute(''' - INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) - VALUES (0, ?, ?, 'deposit_payout', ?) - ''', (user_id, payout, f'Выплата вклада #{dep_id}: {amount:.1f} + проценты = {payout:.2f} см')) - - paid += 1 - - conn.commit() - conn.close() + wcur.execute(''' + INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) + VALUES (%s, %s, 0.0, CURRENT_DATE) + ON CONFLICT (user_id) DO UPDATE SET balance = user_balances.balance + %s + ''', (user_id, payout, payout)) + wcur.execute(''' + UPDATE central_bank SET capital = capital - %s, last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (payout,)) + wcur.execute('UPDATE deposits SET is_active = FALSE WHERE id = %s', (dep_id,)) + wcur.execute(''' + INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) + VALUES (0, %s, %s, 'deposit_payout', %s) + ''', (user_id, payout, f'Выплата вклада #{dep_id}: {amount:.1f} + проценты = {payout:.2f} см')) + paid += 1 if paid: logger.info(f"Matured deposits paid out: {paid}") return paid -# Глобальный экземпляр economy = EconomyManager() diff --git a/main.py b/main.py index 5ddc76f..7e20f49 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ # Системные импорты -import asyncio, json, logging, os, re, random, sqlite3, threading +import asyncio, json, logging, os, re, random, threading from dataclasses import dataclass from datetime import datetime, time, timedelta from io import BytesIO @@ -139,23 +139,6 @@ class LessonSlot: # --- Инициализация и вспомогательные функции --- -def init_penis_db() -> None: - """Инициализация БД для миниигры.""" - Path(config.PENIS_DB_PATH).parent.mkdir(parents=True, exist_ok=True) - with sqlite3.connect(config.PENIS_DB_PATH) as conn: - conn.execute( - """ - CREATE TABLE IF NOT EXISTS penis_stats ( - user_id INTEGER PRIMARY KEY, - display_name TEXT NOT NULL, - length REAL NOT NULL, - last_used_ts INTEGER - ) - """ - ) - conn.execute("CREATE INDEX IF NOT EXISTS idx_penis_stats_length ON penis_stats(length DESC)") - conn.commit() - def init_polychaetsi_storage() -> None: """Инициализация json-хранилища статистики слова 'получается'.""" path = config.POLYCHAETSI_STATS_PATH @@ -1198,9 +1181,6 @@ async def handle_keywords(message: Message): # Записываем каждое обычное сообщение в историю чата await asyncio.to_thread(push_message, chat_id, "user", user_name, message.text) - if chat_id in _autoreply_disabled_chats: - return - await handle_chat_message(message, store_message=False, allow_autonomous=True) async def handle_penis_casino_cmd(message: Message): diff --git a/requirements.txt b/requirements.txt index 8a88b7b..3a0a60e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ pillow>=10.3.0 requests>=2.31.0 aiohttp>=3.9.5 tzdata>=2024.1 -beautifulsoup4 +beautifulsoup4 lxml -pysqlite3-binary>=0.5.3 +psycopg2-binary>=2.9.9 diff --git a/scripts/migrate_sqlite_to_pg.py b/scripts/migrate_sqlite_to_pg.py new file mode 100644 index 0000000..66732ed --- /dev/null +++ b/scripts/migrate_sqlite_to_pg.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +""" +Migrate data from SQLite databases to PostgreSQL. + +Usage (from repo root): + docker compose run --rm bot python3 scripts/migrate_sqlite_to_pg.py + +Or locally (PostgreSQL must be reachable): + DATABASE_URL=postgresql://bot:botpass@localhost:5432/botdb python3 scripts/migrate_sqlite_to_pg.py + +SQLite sources expected in ./db/: + economy.sqlite3 → user_balances, deposits, loans, transactions, taxes, + economy_settings, daily_activity, central_bank, penis_stats + bets.sqlite3 → bets + chat_history.sqlite3→ chat_history, chat_state, talk_history, talk_state + penis_stats.sqlite3 → (legacy) length column → user_balances.balance + penis_stats +""" + +import os +import sqlite3 +import sys +from pathlib import Path + +import psycopg2 +import psycopg2.extras + +DB_DIR = Path(os.getenv("SQLITE_DIR", "/db")) +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://bot:botpass@postgres:5432/botdb") + +ECONOMY_DB = DB_DIR / "economy.sqlite3" +BETS_DB = DB_DIR / "bets.sqlite3" +CHAT_DB = DB_DIR / "chat_history.sqlite3" +PENIS_LEGACY_DB = DB_DIR / "penis_stats.sqlite3" + + +def open_sqlite(path: Path) -> sqlite3.Connection | None: + if not path.exists(): + print(f" [skip] {path} not found") + return None + conn = sqlite3.connect(path) + conn.row_factory = sqlite3.Row + return conn + + +def migrate_economy(pg: psycopg2.extensions.connection) -> None: + src = open_sqlite(ECONOMY_DB) + if src is None: + return + + cur = pg.cursor() + + # --- user_balances --- + rows = src.execute("SELECT * FROM user_balances").fetchall() + print(f" user_balances: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset, created_at) + VALUES (%s, %s, %s, %s, %s) + ON CONFLICT (user_id) DO UPDATE SET + balance = EXCLUDED.balance, + daily_income = EXCLUDED.daily_income, + last_daily_reset = EXCLUDED.last_daily_reset + """, ( + r["user_id"], + r["balance"], + r["daily_income"], + r["last_daily_reset"] or None, + r["created_at"] or None, + )) + + # --- deposits --- + rows = src.execute("SELECT * FROM deposits").fetchall() + print(f" deposits: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO deposits (id, user_id, amount, interest_rate, created_at, matures_at, is_active) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO NOTHING + """, ( + r["id"], r["user_id"], r["amount"], r["interest_rate"], + r["created_at"] or None, r["matures_at"] or None, + bool(r["is_active"]), + )) + _reset_sequence(cur, "deposits", "id") + + # --- loans --- + rows = src.execute("SELECT * FROM loans").fetchall() + print(f" loans: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO loans (id, user_id, amount, interest_rate, created_at, due_at, is_repaid) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO NOTHING + """, ( + r["id"], r["user_id"], r["amount"], r["interest_rate"], + r["created_at"] or None, r["due_at"] or None, + bool(r["is_repaid"]), + )) + _reset_sequence(cur, "loans", "id") + + # --- transactions --- + rows = src.execute("SELECT * FROM transactions").fetchall() + print(f" transactions: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO transactions (id, from_user_id, to_user_id, amount, transaction_type, description, created_at) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO NOTHING + """, ( + r["id"], r["from_user_id"], r["to_user_id"], r["amount"], + r["transaction_type"], r["description"], r["created_at"] or None, + )) + _reset_sequence(cur, "transactions", "id") + + # --- taxes --- + rows = src.execute("SELECT * FROM taxes").fetchall() + print(f" taxes: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO taxes (id, user_id, amount, tax_date, created_at) + VALUES (%s, %s, %s, %s, %s) + ON CONFLICT (id) DO NOTHING + """, ( + r["id"], r["user_id"], r["amount"], + r["tax_date"] or None, r["created_at"] or None, + )) + _reset_sequence(cur, "taxes", "id") + + # --- economy_settings --- + rows = src.execute("SELECT * FROM economy_settings").fetchall() + print(f" economy_settings: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO economy_settings (key, value, updated_at) + VALUES (%s, %s, %s) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value + """, (r["key"], r["value"], r["updated_at"] if "updated_at" in r.keys() else None)) + + # --- daily_activity --- + rows = src.execute("SELECT * FROM daily_activity").fetchall() + print(f" daily_activity: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO daily_activity (id, user_id, activity_date, message_count, last_activity_time, created_at) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (user_id, activity_date) DO UPDATE SET + message_count = EXCLUDED.message_count, + last_activity_time = EXCLUDED.last_activity_time + """, ( + r["id"], r["user_id"], + r["activity_date"] or None, r["message_count"], + r["last_activity_time"] or None, r["created_at"] or None, + )) + _reset_sequence(cur, "daily_activity", "id") + + # --- central_bank --- + rows = src.execute("SELECT * FROM central_bank").fetchall() + print(f" central_bank: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO central_bank (id, capital, total_taxes_collected, + total_inflation_adjustments, total_emissions, last_updated) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO UPDATE SET + capital = EXCLUDED.capital, + total_taxes_collected = EXCLUDED.total_taxes_collected, + total_inflation_adjustments = EXCLUDED.total_inflation_adjustments, + total_emissions = EXCLUDED.total_emissions, + last_updated = EXCLUDED.last_updated + """, ( + r["id"], r["capital"], + r["total_taxes_collected"], r["total_inflation_adjustments"], + r["total_emissions"], r["last_updated"] or None, + )) + + # --- penis_stats (new schema: user_id, display_name, last_used_ts) --- + try: + rows = src.execute("SELECT * FROM penis_stats").fetchall() + print(f" penis_stats: {len(rows)} rows") + cols = rows[0].keys() if rows else [] + for r in rows: + if "last_used_ts" in cols: + cur.execute(""" + INSERT INTO penis_stats (user_id, display_name, last_used_ts) + VALUES (%s, %s, %s) + ON CONFLICT (user_id) DO UPDATE SET + display_name = EXCLUDED.display_name, + last_used_ts = EXCLUDED.last_used_ts + """, (r["user_id"], r["display_name"], r["last_used_ts"])) + else: + # old schema without last_used_ts + cur.execute(""" + INSERT INTO penis_stats (user_id, display_name) + VALUES (%s, %s) + ON CONFLICT (user_id) DO UPDATE SET display_name = EXCLUDED.display_name + """, (r["user_id"], r.get("display_name", ""))) + except sqlite3.OperationalError as e: + print(f" [warn] penis_stats in economy.sqlite3: {e}") + + src.close() + pg.commit() + print(" economy.sqlite3 done") + + +def migrate_legacy_penis(pg: psycopg2.extensions.connection) -> None: + """Old penis_stats.sqlite3 had a `length` column — map it to user_balances.balance.""" + src = open_sqlite(PENIS_LEGACY_DB) + if src is None: + return + + cur = pg.cursor() + try: + rows = src.execute("SELECT user_id, length, display_name FROM penis_stats").fetchall() + except sqlite3.OperationalError: + rows = src.execute("SELECT * FROM penis_stats").fetchall() + + print(f" penis_stats.sqlite3 (legacy): {len(rows)} rows") + for r in rows: + uid = r["user_id"] + length = r["length"] if "length" in r.keys() else 0.0 + name = r["display_name"] if "display_name" in r.keys() else "" + last_ts = r["last_used_ts"] if "last_used_ts" in r.keys() else None + + # upsert into user_balances; don't overwrite if already migrated from economy.sqlite3 + cur.execute(""" + INSERT INTO user_balances (user_id, balance) + VALUES (%s, %s) + ON CONFLICT (user_id) DO NOTHING + """, (uid, float(length))) + + cur.execute(""" + INSERT INTO penis_stats (user_id, display_name, last_used_ts) + VALUES (%s, %s, %s) + ON CONFLICT (user_id) DO UPDATE SET + display_name = EXCLUDED.display_name, + last_used_ts = COALESCE(EXCLUDED.last_used_ts, penis_stats.last_used_ts) + """, (uid, name, last_ts)) + + src.close() + pg.commit() + print(" penis_stats.sqlite3 done") + + +def migrate_bets(pg: psycopg2.extensions.connection) -> None: + src = open_sqlite(BETS_DB) + if src is None: + return + + cur = pg.cursor() + + rows = src.execute("SELECT * FROM bets").fetchall() + print(f" bets: {len(rows)} rows") + cols = rows[0].keys() if rows else [] + for r in rows: + cur.execute(""" + INSERT INTO bets (id, user_id, match_id, sport, home_team, away_team, + chosen_team, amount, odds, status, payout, created_ts, resolved_ts) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO NOTHING + """, ( + r["id"], r["user_id"], r["match_id"], r["sport"], + r["home_team"], r["away_team"], r["chosen_team"], + r["amount"], r["odds"], r["status"], + r["payout"] if "payout" in cols else 0, + r["created_ts"] if "created_ts" in cols else None, + r["resolved_ts"] if "resolved_ts" in cols else None, + )) + _reset_sequence(cur, "bets", "id") + + src.close() + pg.commit() + print(" bets.sqlite3 done") + + +def migrate_chat(pg: psycopg2.extensions.connection) -> None: + src = open_sqlite(CHAT_DB) + if src is None: + return + + cur = pg.cursor() + + # --- chat_history --- + rows = src.execute("SELECT * FROM chat_history").fetchall() + print(f" chat_history: {len(rows)} rows") + cols = rows[0].keys() if rows else [] + for r in rows: + cur.execute(""" + INSERT INTO chat_history (id, chat_id, role, name, text, created_at) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO NOTHING + """, ( + r["id"], r["chat_id"], r["role"], r["name"], r["text"], + r["created_at"] if "created_at" in cols else 0, + )) + _reset_sequence(cur, "chat_history", "id") + + # --- chat_state --- + rows = src.execute("SELECT * FROM chat_state").fetchall() + print(f" chat_state: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO chat_state (chat_id, key, value) + VALUES (%s, %s, %s) + ON CONFLICT (chat_id, key) DO UPDATE SET value = EXCLUDED.value + """, (r["chat_id"], r["key"], r["value"])) + + # --- talk_history --- + rows = src.execute("SELECT * FROM talk_history").fetchall() + print(f" talk_history: {len(rows)} rows") + cols = rows[0].keys() if rows else [] + for r in rows: + cur.execute(""" + INSERT INTO talk_history (id, chat_id, user_id, role, name, text, created_at) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (id) DO NOTHING + """, ( + r["id"], r["chat_id"], r["user_id"], r["role"], r["name"], r["text"], + r["created_at"] if "created_at" in cols else 0, + )) + _reset_sequence(cur, "talk_history", "id") + + # --- talk_state --- + rows = src.execute("SELECT * FROM talk_state").fetchall() + print(f" talk_state: {len(rows)} rows") + for r in rows: + cur.execute(""" + INSERT INTO talk_state (chat_id, user_id, key, value) + VALUES (%s, %s, %s, %s) + ON CONFLICT (chat_id, user_id, key) DO UPDATE SET value = EXCLUDED.value + """, (r["chat_id"], r["user_id"], r["key"], r["value"])) + + src.close() + pg.commit() + print(" chat_history.sqlite3 done") + + +def _reset_sequence(cur, table: str, col: str) -> None: + """Reset SERIAL sequence to max(col) so future inserts don't collide.""" + cur.execute(f"SELECT setval(pg_get_serial_sequence('{table}', '{col}'), COALESCE(MAX({col}), 1)) FROM {table}") + + +def main() -> None: + print(f"Connecting to PostgreSQL: {DATABASE_URL}") + try: + pg = psycopg2.connect(DATABASE_URL) + except psycopg2.OperationalError as e: + print(f"ERROR: cannot connect to PostgreSQL: {e}", file=sys.stderr) + sys.exit(1) + + pg.autocommit = False + + print("\n=== economy.sqlite3 ===") + migrate_economy(pg) + + print("\n=== penis_stats.sqlite3 (legacy) ===") + migrate_legacy_penis(pg) + + print("\n=== bets.sqlite3 ===") + migrate_bets(pg) + + print("\n=== chat_history.sqlite3 ===") + migrate_chat(pg) + + pg.close() + print("\nMigration complete.") + + +if __name__ == "__main__": + main() diff --git a/webapp/api.py b/webapp/api.py index 93185a3..c85a18a 100644 --- a/webapp/api.py +++ b/webapp/api.py @@ -145,15 +145,16 @@ async def get_profile(user: dict = Depends(get_current_user)) -> ProfileResponse user_id = user["user_id"] length = await asyncio.to_thread(get_user_length, user_id) - # Считаем активные ставки - import sqlite3 active_bets = 0 try: - with sqlite3.connect(config.BET_DB_PATH) as conn: - row = conn.execute( - "SELECT COUNT(*) FROM bets WHERE user_id = ? AND status = 'pending'", + from db import get_conn + with get_conn() as conn: + cur = conn.cursor() + cur.execute( + "SELECT COUNT(*) FROM bets WHERE user_id = %s AND status = 'pending'", (user_id,), - ).fetchone() + ) + row = cur.fetchone() active_bets = row[0] if row else 0 except Exception: pass