1
0
Fork 0
forked from zovos/bot_tg

feat: переход с SQLite на PostgreSQL + скрипт миграции

- Все 4 SQLite базы объединены в одну PostgreSQL
- Новый db.py с ThreadedConnectionPool (psycopg2)
- docker-compose: сервис postgres с healthcheck, данные в ./db/postgres
- scripts/migrate_sqlite_to_pg.py — миграция существующих данных
- Убраны мёртвый код и дублирующий import в main.py и talk_handler.py
- .env удалён из git-индекса

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
q 2026-05-01 23:25:22 +03:00
parent b0e03f8e23
commit add3553ce0
12 changed files with 1161 additions and 1015 deletions

9
.env
View file

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

View file

@ -7,21 +7,19 @@ import mimetypes
import os import os
import random import random
import re import re
import sqlite3
import time import time
from io import BytesIO from io import BytesIO
import re
from pathlib import Path
from typing import Any from typing import Any
import aiohttp import aiohttp
import psycopg2.extras
from aiogram.types import Message from aiogram.types import Message
from db import get_conn
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Основные настройки поведения и стиля бота редактируются здесь.
LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-ai/") 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", "бот") BOT_MEMORY_NAME = os.getenv("BOT_MEMORY_NAME", "бот")
SKIP_TOKEN = "<skip>" SKIP_TOKEN = "<skip>"
FORCE_DISABLE_THINKING = os.getenv("LLAMA_FORCE_DISABLE_THINKING", "1").lower() not in {"0", "false", "no"} 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 = "" _cached_bot_username: str = ""
def _connect_db() -> sqlite3.Connection: def _get_state_from_conn(conn, chat_id: int, key: str, default: str = "") -> str:
conn = sqlite3.connect(DB_PATH, timeout=30) cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
conn.row_factory = sqlite3.Row cur.execute(
conn.execute("PRAGMA busy_timeout = 30000") "SELECT value FROM chat_state WHERE chat_id = %s AND key = %s",
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 = ?",
(chat_id, key), (chat_id, key),
).fetchone() )
row = cur.fetchone()
return row["value"] if row else default return row["value"] if row else default
def _set_state_from_conn(conn: sqlite3.Connection, chat_id: int, key: str, value: str) -> None: def _set_state_from_conn(conn, chat_id: int, key: str, value: str) -> None:
conn.execute( cur = conn.cursor()
cur.execute(
""" """
INSERT INTO chat_state (chat_id, key, value) INSERT INTO chat_state (chat_id, key, value)
VALUES (?, ?, ?) VALUES (%s, %s, %s)
ON CONFLICT(chat_id, key) DO UPDATE SET value = excluded.value ON CONFLICT (chat_id, key) DO UPDATE SET value = EXCLUDED.value
""", """,
(chat_id, key, value), (chat_id, key, value),
) )
def _get_talk_state_from_conn( def _get_talk_state_from_conn(conn, chat_id: int, user_id: int, key: str, default: str = "") -> str:
conn: sqlite3.Connection, cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
chat_id: int, cur.execute(
user_id: int, "SELECT value FROM talk_state WHERE chat_id = %s AND user_id = %s AND key = %s",
key: str,
default: str = "",
) -> str:
row = conn.execute(
"SELECT value FROM talk_state WHERE chat_id = ? AND user_id = ? AND key = ?",
(chat_id, user_id, key), (chat_id, user_id, key),
).fetchone() )
row = cur.fetchone()
return row["value"] if row else default return row["value"] if row else default
def _set_talk_state_from_conn( def _set_talk_state_from_conn(conn, chat_id: int, user_id: int, key: str, value: str) -> None:
conn: sqlite3.Connection, cur = conn.cursor()
chat_id: int, cur.execute(
user_id: int,
key: str,
value: str,
) -> None:
conn.execute(
""" """
INSERT INTO talk_state (chat_id, user_id, key, value) INSERT INTO talk_state (chat_id, user_id, key, value)
VALUES (?, ?, ?, ?) VALUES (%s, %s, %s, %s)
ON CONFLICT(chat_id, user_id, key) DO UPDATE SET value = excluded.value ON CONFLICT (chat_id, user_id, key) DO UPDATE SET value = EXCLUDED.value
""", """,
(chat_id, user_id, key, value), (chat_id, user_id, key, value),
) )
def _init_db() -> None: def _init_db() -> None:
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True) with get_conn() as conn:
with _connect_db() as conn: cur = conn.cursor()
conn.execute("PRAGMA journal_mode = WAL") cur.execute("""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS chat_history ( CREATE TABLE IF NOT EXISTS chat_history (
id INTEGER PRIMARY KEY AUTOINCREMENT, id BIGSERIAL PRIMARY KEY,
chat_id INTEGER NOT NULL, chat_id BIGINT NOT NULL,
role TEXT NOT NULL, role TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
text TEXT NOT NULL, text TEXT NOT NULL,
created_at REAL NOT NULL DEFAULT 0 created_at DOUBLE PRECISION NOT NULL DEFAULT 0
) )
""" """)
) cur.execute("ALTER TABLE chat_history ADD COLUMN IF NOT EXISTS created_at DOUBLE PRECISION NOT NULL DEFAULT 0")
if not _column_exists(conn, "chat_history", "created_at"): cur.execute("""
conn.execute("ALTER TABLE chat_history ADD COLUMN created_at REAL NOT NULL DEFAULT 0")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS chat_state ( CREATE TABLE IF NOT EXISTS chat_state (
chat_id INTEGER NOT NULL, chat_id BIGINT NOT NULL,
key TEXT NOT NULL, key TEXT NOT NULL,
value TEXT NOT NULL, value TEXT NOT NULL,
PRIMARY KEY (chat_id, key) PRIMARY KEY (chat_id, key)
) )
""" """)
) cur.execute("""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS talk_history ( CREATE TABLE IF NOT EXISTS talk_history (
id INTEGER PRIMARY KEY AUTOINCREMENT, id BIGSERIAL PRIMARY KEY,
chat_id INTEGER NOT NULL, chat_id BIGINT NOT NULL,
user_id INTEGER NOT NULL, user_id BIGINT NOT NULL,
role TEXT NOT NULL, role TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
text TEXT NOT NULL, text TEXT NOT NULL,
created_at REAL NOT NULL DEFAULT 0 created_at DOUBLE PRECISION NOT NULL DEFAULT 0
) )
""" """)
) cur.execute("""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS talk_state ( CREATE TABLE IF NOT EXISTS talk_state (
chat_id INTEGER NOT NULL, chat_id BIGINT NOT NULL,
user_id INTEGER NOT NULL, user_id BIGINT NOT NULL,
key TEXT NOT NULL, key TEXT NOT NULL,
value TEXT NOT NULL, value TEXT NOT NULL,
PRIMARY KEY (chat_id, user_id, key) PRIMARY KEY (chat_id, user_id, key)
) )
""" """)
) cur.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_id ON chat_history(chat_id, id)")
conn.execute( cur.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_role_id ON chat_history(chat_id, role, id)")
"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_talk_history_chat_user_id ON talk_history(chat_id, user_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()
def _clean_text(text: str) -> str: def _clean_text(text: str) -> str:
@ -422,78 +382,59 @@ def push_message(
cleaned_text = _clean_text(text) cleaned_text = _clean_text(text)
if not cleaned_text: if not cleaned_text:
return return
with _connect_db() as conn: with get_conn() as conn:
cur = conn.cursor()
if user_id is None: if user_id is None:
conn.execute( cur.execute(
""" "INSERT INTO chat_history (chat_id, role, name, text, created_at) VALUES (%s, %s, %s, %s, %s)",
INSERT INTO chat_history (chat_id, role, name, text, created_at)
VALUES (?, ?, ?, ?, ?)
""",
(chat_id, role, name, cleaned_text, time.time()), (chat_id, role, name, cleaned_text, time.time()),
) )
else: else:
conn.execute( cur.execute(
""" "INSERT INTO talk_history (chat_id, user_id, role, name, text, created_at) VALUES (%s, %s, %s, %s, %s, %s)",
INSERT INTO talk_history (chat_id, user_id, role, name, text, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(chat_id, user_id, role, name, cleaned_text, time.time()), (chat_id, user_id, role, name, cleaned_text, time.time()),
) )
conn.commit()
def _get_summary(chat_id: int, user_id: int | None = None) -> str: 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: if user_id is None:
return _get_state_from_conn(conn, chat_id, "summary", "") return _get_state_from_conn(conn, chat_id, "summary", "")
return _get_talk_state_from_conn(conn, chat_id, user_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]: def _get_history_rows(chat_id: int, user_id: int | None = None) -> list:
with _connect_db() as conn: with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
if user_id is None: if user_id is None:
return conn.execute( cur.execute(
""" "SELECT id, role, name, text, created_at FROM chat_history WHERE chat_id = %s ORDER BY id ASC",
SELECT id, role, name, text, created_at
FROM chat_history
WHERE chat_id = ?
ORDER BY id ASC
""",
(chat_id,), (chat_id,),
).fetchall() )
return conn.execute( else:
""" cur.execute(
SELECT id, role, name, text, created_at "SELECT id, role, name, text, created_at FROM talk_history WHERE chat_id = %s AND user_id = %s ORDER BY id ASC",
FROM talk_history (chat_id, user_id),
WHERE chat_id = ? AND user_id = ? )
ORDER BY id ASC return cur.fetchall()
""",
(chat_id, user_id),
).fetchall()
def _latest_reply_stats(chat_id: int) -> tuple[float, int]: def _latest_reply_stats(chat_id: int) -> tuple[float, int]:
with _connect_db() as conn: with get_conn() as conn:
last_assistant = conn.execute( cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
""" cur.execute(
SELECT id, created_at "SELECT id, created_at FROM chat_history WHERE chat_id = %s AND role = 'assistant' ORDER BY id DESC LIMIT 1",
FROM chat_history
WHERE chat_id = ? AND role = 'assistant'
ORDER BY id DESC
LIMIT 1
""",
(chat_id,), (chat_id,),
).fetchone() )
last_assistant = cur.fetchone()
if not last_assistant: if not last_assistant:
return 0.0, 10_000 return 0.0, 10_000
user_messages_since_reply = conn.execute( cnt_cur = conn.cursor()
""" cnt_cur.execute(
SELECT COUNT(*) "SELECT COUNT(*) FROM chat_history WHERE chat_id = %s AND role = 'user' AND id > %s",
FROM chat_history
WHERE chat_id = ? AND role = 'user' AND id > ?
""",
(chat_id, last_assistant["id"]), (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) 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, last_row_id: int,
user_id: int | None = None, user_id: int | None = None,
) -> None: ) -> None:
with _connect_db() as conn: with get_conn() as conn:
cur = conn.cursor()
if user_id is None: if user_id is None:
_set_state_from_conn(conn, chat_id, "summary", summary) _set_state_from_conn(conn, chat_id, "summary", summary)
conn.execute( cur.execute(
"DELETE FROM chat_history WHERE chat_id = ? AND id <= ?", "DELETE FROM chat_history WHERE chat_id = %s AND id <= %s",
(chat_id, last_row_id), (chat_id, last_row_id),
) )
else: else:
_set_talk_state_from_conn(conn, chat_id, user_id, "summary", summary) _set_talk_state_from_conn(conn, chat_id, user_id, "summary", summary)
conn.execute( cur.execute(
"DELETE FROM talk_history WHERE chat_id = ? AND user_id = ? AND id <= ?", "DELETE FROM talk_history WHERE chat_id = %s AND user_id = %s AND id <= %s",
(chat_id, user_id, last_row_id), (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": if row["role"] == "user":
return { return {
"role": "user", "role": "user",

View file

@ -7,10 +7,9 @@ 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")
# --- Настройки /penis --- # --- Настройки /penis ---
PENIS_DB_PATH = "/db/penis_stats.sqlite3"
CHAT_HISTORY_DB_PATH = "/db/chat_history.sqlite3"
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
@ -34,6 +33,7 @@ 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
ODDS_SPORTS = { ODDS_SPORTS = {
"epl": "soccer_epl", "epl": "soccer_epl",
"ucl": "soccer_uefa_champs_league", "ucl": "soccer_uefa_champs_league",
@ -43,8 +43,6 @@ ODDS_SPORTS = {
"ligue1": "soccer_france_ligue_one", "ligue1": "soccer_france_ligue_one",
"europa": "soccer_uefa_europa_league", "europa": "soccer_uefa_europa_league",
} }
BET_MAX_AMOUNT = 3.0
BET_DB_PATH = "/db/bets.sqlite3"
ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно) ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно)
# --- Nude --- # --- Nude ---

35
db.py Normal file
View file

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

View file

@ -1,4 +1,19 @@
services: 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: bot:
build: build:
context: . context: .
@ -8,14 +23,15 @@ services:
- .env - .env
environment: environment:
TZ: Europe/Moscow TZ: Europe/Moscow
PENIS_DB_PATH: /db/penis_stats.sqlite3 DATABASE_URL: postgresql://bot:${POSTGRES_PASSWORD:-botpass}@postgres:5432/botdb
CHAT_HISTORY_DB_PATH: /db/chat_history.sqlite3
POLYCHAETSI_STATS_PATH: /db/polychaetsi_stats.json POLYCHAETSI_STATS_PATH: /db/polychaetsi_stats.json
BETS_DB_PATH: /db/bets.sqlite3 NUDE_HISTORY_PATH: /db/nude_history.json
ECONOMY_DB_PATH: /db/economy.sqlite3
LLAMA_API_URL: ${LLAMA_API_URL:-https://mirror.porno4free.ru/zovos-ai/} LLAMA_API_URL: ${LLAMA_API_URL:-https://mirror.porno4free.ru/zovos-ai/}
LLAMA_FORCE_DISABLE_THINKING: ${LLAMA_FORCE_DISABLE_THINKING:-0} LLAMA_FORCE_DISABLE_THINKING: ${LLAMA_FORCE_DISABLE_THINKING:-0}
LLAMA_LOG_THINKING: ${LLAMA_LOG_THINKING:-1} LLAMA_LOG_THINKING: ${LLAMA_LOG_THINKING:-1}
depends_on:
postgres:
condition: service_healthy
init: true init: true
restart: unless-stopped restart: unless-stopped
@ -29,14 +45,16 @@ services:
- .env - .env
environment: environment:
TZ: Europe/Moscow TZ: Europe/Moscow
PENIS_DB_PATH: /db/penis_stats.sqlite3 DATABASE_URL: postgresql://bot:${POSTGRES_PASSWORD:-botpass}@postgres:5432/botdb
CHAT_HISTORY_DB_PATH: /db/chat_history.sqlite3
POLYCHAETSI_STATS_PATH: /db/polychaetsi_stats.json POLYCHAETSI_STATS_PATH: /db/polychaetsi_stats.json
BET_DB_PATH: /db/bets.sqlite3 NUDE_HISTORY_PATH: /db/nude_history.json
API_PORT: "8080" API_PORT: "8080"
LLAMA_API_URL: ${LLAMA_API_URL:-https://mirror.porno4free.ru/zovos-ai/} LLAMA_API_URL: ${LLAMA_API_URL:-https://mirror.porno4free.ru/zovos-ai/}
LLAMA_FORCE_DISABLE_THINKING: ${LLAMA_FORCE_DISABLE_THINKING:-0} LLAMA_FORCE_DISABLE_THINKING: ${LLAMA_FORCE_DISABLE_THINKING:-0}
LLAMA_LOG_THINKING: ${LLAMA_LOG_THINKING:-1} LLAMA_LOG_THINKING: ${LLAMA_LOG_THINKING:-1}
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped restart: unless-stopped
webapp-frontend: webapp-frontend:
@ -59,5 +77,6 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
postgres_data:
certbot_etc: certbot_etc:
certbot_www: certbot_www:

View file

@ -1,13 +1,14 @@
import time as _time
import sqlite3
import logging import logging
from datetime import datetime, timedelta import time as _time
from pathlib import Path from datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import aiohttp import aiohttp
import psycopg2
import psycopg2.extras
import config import config
from db import get_conn
from games.casino import get_user_length, update_user_length from games.casino import get_user_length, update_user_length
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,18 +18,16 @@ _sports_cache: tuple[float, list] | None = None
_last_viewed_sport_by_user: dict[int, str] = {} _last_viewed_sport_by_user: dict[int, str] = {}
_DRAW_NAMES = {"draw", "tie", "ничья"} _DRAW_NAMES = {"draw", "tie", "ничья"}
# Сколько дней хранить историю ставок
BET_HISTORY_DAYS = 30 BET_HISTORY_DAYS = 30
def _init_bets_db() -> None: def _init_bets_db() -> None:
Path(config.BET_DB_PATH).parent.mkdir(parents=True, exist_ok=True) with get_conn() as conn:
with sqlite3.connect(config.BET_DB_PATH) as conn: cur = conn.cursor()
conn.execute( cur.execute('''
"""
CREATE TABLE IF NOT EXISTS bets ( CREATE TABLE IF NOT EXISTS bets (
id INTEGER PRIMARY KEY AUTOINCREMENT, id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL, user_id BIGINT NOT NULL,
match_id TEXT NOT NULL, match_id TEXT NOT NULL,
sport TEXT NOT NULL, sport TEXT NOT NULL,
home_team TEXT NOT NULL, home_team TEXT NOT NULL,
@ -38,25 +37,15 @@ def _init_bets_db() -> None:
odds REAL NOT NULL, odds REAL NOT NULL,
status TEXT DEFAULT 'pending', status TEXT DEFAULT 'pending',
payout REAL DEFAULT 0, payout REAL DEFAULT 0,
created_ts INTEGER, created_ts BIGINT,
resolved_ts INTEGER resolved_ts BIGINT
) )
""" ''')
) cur.execute("CREATE INDEX IF NOT EXISTS idx_bets_user ON bets(user_id, status)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_user ON bets(user_id, status)") cur.execute("CREATE INDEX IF NOT EXISTS idx_bets_match ON bets(match_id, status)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_bets_match ON bets(match_id, status)") cur.execute("CREATE INDEX IF NOT EXISTS idx_bets_created ON bets(created_ts)")
conn.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")
# Миграция: добавить столбцы если их нет
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()
try: try:
@ -79,7 +68,6 @@ def get_user_sport_context(user_id: int) -> str | None:
async def _fetch_available_sports() -> list[dict]: async def _fetch_available_sports() -> list[dict]:
"""Получить список доступных видов спорта из API."""
global _sports_cache global _sports_cache
now = _time.time() now = _time.time()
if _sports_cache and (now - _sports_cache[0]) < 3600: 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] sport_key = config.ODDS_SPORTS[sport_alias]
matches = await fetch_matches(sport_key) matches = await fetch_matches(sport_key)
if not matches: if not matches:
# Попробуем проверить, существует ли вообще этот спорт
sports = await _fetch_available_sports() sports = await _fetch_available_sports()
valid_keys = {s.get("key") for s in sports} valid_keys = {s.get("key") for s in sports}
if sport_key not in valid_keys: if sport_key not in valid_keys:
# Ищем похожий ключ
suggestions = [s for s in sports if sport_alias in s.get("key", "").lower() suggestions = [s for s in sports if sport_alias in s.get("key", "").lower()
or sport_alias in s.get("title", "").lower()] or sport_alias in s.get("title", "").lower()]
if suggestions: if suggestions:
@ -304,10 +290,7 @@ async def get_formatted_matches(sport_alias: str | None) -> str:
f"Похожие виды спорта:\n{suggest_text}\n\n" f"Похожие виды спорта:\n{suggest_text}\n\n"
f"Обнови sport keys в config.py" f"Обнови sport keys в config.py"
) )
return ( return f"⚠️ Ключ {sport_key} не найден в API. Проверь ODDS_SPORTS в config.py"
f"⚠️ Ключ {sport_key} не найден в API. "
f"Проверь ODDS_SPORTS в config.py"
)
return format_matches(matches, sport_alias, _get_sport_label(sport_alias)) return format_matches(matches, sport_alias, _get_sport_label(sport_alias))
parts = [] parts = []
@ -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) current = get_user_length(user_id)
if current is None: if current is None:
return "Сначала заведи счёт через /penis, братуха." return "Сначала заведи счёт через /penis, братуха."
if current <= 0: if current <= 0:
return "🚫 С кредитом ставки не принимаем." return "🚫 С кредитом ставки не принимаем."
if amount <= 0: if amount <= 0:
return "Ставка должна быть больше нуля." return "Ставка должна быть больше нуля."
if amount > config.BET_MAX_AMOUNT: if amount > config.BET_MAX_AMOUNT:
return f"Максимальная ставка — {config.BET_MAX_AMOUNT} см." return f"Максимальная ставка — {config.BET_MAX_AMOUNT} см."
if amount > current: if amount > current:
return f"У тебя {current:.1f} см, а ставишь {amount:.1f}. Не хватает, фраер." 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()) now_ts = int(_time.time())
try: try:
with sqlite3.connect(config.BET_DB_PATH) as conn: with get_conn() as conn:
conn.execute( cur = conn.cursor()
"""INSERT INTO bets (user_id, match_id, sport, home_team, away_team, cur.execute('''
chosen_team, amount, odds, status, payout, created_ts) INSERT INTO bets
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)""", (user_id, match_id, sport, home_team, away_team,
(user_id, match["id"], sport, match["home"], match["away"], chosen_team, amount, odds, status, payout, created_ts)
chosen_team, amount, team_odds, now_ts), VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'pending', 0, %s)
) ''', (
conn.commit() user_id, match["id"], sport, match["home"], match["away"],
except sqlite3.Error: chosen_team, amount, team_odds, now_ts,
))
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 "Ошибка БД."
@ -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: def get_user_bets(user_id: int) -> str:
try: try:
with sqlite3.connect(config.BET_DB_PATH) as conn: with get_conn() as conn:
conn.row_factory = sqlite3.Row cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
rows = conn.execute( cur.execute('''
"SELECT * FROM bets WHERE user_id = ? AND status = 'pending' ORDER BY created_ts DESC LIMIT 10", SELECT * FROM bets
(user_id,), WHERE user_id = %s AND status = 'pending'
).fetchall() ORDER BY created_ts DESC LIMIT 10
except sqlite3.Error: ''', (user_id,))
rows = cur.fetchall()
except psycopg2.Error:
return "Ошибка БД." return "Ошибка БД."
if not rows: if not rows:
@ -413,34 +396,26 @@ def get_user_bets(user_id: int) -> str:
def get_user_bet_history(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: try:
with sqlite3.connect(config.BET_DB_PATH) as conn: with get_conn() as conn:
conn.row_factory = sqlite3.Row cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
rows = conn.execute( cur.execute('''
"""SELECT * FROM bets SELECT * FROM bets
WHERE user_id = ? AND created_ts >= ? WHERE user_id = %s AND created_ts >= %s
ORDER BY created_ts DESC ORDER BY created_ts DESC LIMIT 50
LIMIT 50""", ''', (user_id, cutoff_ts))
(user_id, cutoff_ts), rows = cur.fetchall()
).fetchall() except psycopg2.Error:
except sqlite3.Error:
logger.exception("Failed to get bet history") logger.exception("Failed to get bet history")
return "Ошибка БД." return "Ошибка БД."
if not rows: if not rows:
return "📋 За последний месяц ставок не было." return "📋 За последний месяц ставок не было."
# Считаем статистику total_bet = total_won = total_lost = 0.0
total_bet = 0.0 wins = losses = pending = 0
total_won = 0.0
total_lost = 0.0
wins = 0
losses = 0
pending = 0
lines = [f"📋 История ставок за {BET_HISTORY_DAYS} дней:\n"] lines = [f"📋 История ставок за {BET_HISTORY_DAYS} дней:\n"]
for r in rows: for r in rows:
@ -453,7 +428,6 @@ def get_user_bet_history(user_id: int) -> str:
if status == "won": if status == "won":
wins += 1 wins += 1
# Для старых ставок payout может быть 0, пересчитываем
actual_payout = payout_raw if payout_raw > 0 else round(amount * odds_val, 1) actual_payout = payout_raw if payout_raw > 0 else round(amount * odds_val, 1)
total_won += actual_payout total_won += actual_payout
status_icon = "" status_icon = ""
@ -466,8 +440,7 @@ def get_user_bet_history(user_id: int) -> str:
else: else:
pending += 1 pending += 1
status_icon = "" status_icon = ""
potential = round(amount * odds_val, 1) result_text = f"ожидание → {round(amount * odds_val, 1):.1f} см"
result_text = f"ожидание → {potential:.1f} см"
created = "" created = ""
if r["created_ts"]: if r["created_ts"]:
@ -480,10 +453,8 @@ def get_user_bet_history(user_id: int) -> str:
f" 📅 {created}" f" 📅 {created}"
) )
# Итоговая статистика
net = total_won - total_lost net = total_won - total_lost
net_sign = "+" if net >= 0 else "" net_sign = "+" if net >= 0 else ""
lines.append("") lines.append("")
lines.append("━━━ 📊 СТАТИСТИКА ━━━") lines.append("━━━ 📊 СТАТИСТИКА ━━━")
lines.append(f"🎰 Всего ставок: {len(rows)}") 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_won:.1f} см")
lines.append(f"💸 Проиграно: -{total_lost:.1f} см") lines.append(f"💸 Проиграно: -{total_lost:.1f} см")
lines.append(f"📈 Итого: {net_sign}{net:.1f} см") lines.append(f"📈 Итого: {net_sign}{net:.1f} см")
if wins + losses > 0: if wins + losses > 0:
winrate = wins / (wins + losses) * 100 lines.append(f"📊 Винрейт: {wins / (wins + losses) * 100:.0f}%")
lines.append(f"📊 Винрейт: {winrate:.0f}%")
return "\n".join(lines) return "\n".join(lines)
def cleanup_old_bets() -> int: def cleanup_old_bets() -> int:
"""Удалить завершённые ставки старше BET_HISTORY_DAYS дней."""
cutoff_ts = int(_time.time() - BET_HISTORY_DAYS * 86400) cutoff_ts = int(_time.time() - BET_HISTORY_DAYS * 86400)
try: try:
with sqlite3.connect(config.BET_DB_PATH) as conn: with get_conn() as conn:
cursor = conn.execute( cur = conn.cursor()
"DELETE FROM bets WHERE status != 'pending' AND created_ts < ?", cur.execute(
"DELETE FROM bets WHERE status != 'pending' AND created_ts < %s",
(cutoff_ts,), (cutoff_ts,),
) )
conn.commit() deleted = cur.rowcount
deleted = cursor.rowcount
if deleted > 0: if deleted > 0:
logger.info(f"Cleaned up {deleted} old bets") logger.info(f"Cleaned up {deleted} old bets")
return deleted return deleted
except sqlite3.Error: except psycopg2.Error:
logger.exception("Failed to cleanup old bets") logger.exception("Failed to cleanup old bets")
return 0 return 0
@ -525,12 +493,11 @@ def cleanup_old_bets() -> int:
async def settle_bets() -> list[str]: async def settle_bets() -> list[str]:
notifications = [] notifications = []
try: try:
with sqlite3.connect(config.BET_DB_PATH) as conn: with get_conn() as conn:
conn.row_factory = sqlite3.Row cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
pending = conn.execute( cur.execute("SELECT DISTINCT match_id, sport FROM bets WHERE status = 'pending'")
"SELECT DISTINCT match_id, sport FROM bets WHERE status = 'pending'" pending = cur.fetchall()
).fetchall() except psycopg2.Error:
except sqlite3.Error:
return notifications return notifications
if not pending: if not pending:
@ -557,29 +524,22 @@ async def settle_bets() -> list[str]:
continue continue
for event in scores_data: for event in scores_data:
if event.get("id") != match_id: if event.get("id") != match_id or not event.get("completed"):
continue
if not event.get("completed"):
continue continue
scores = event.get("scores") scores = event.get("scores")
if not scores: if not scores:
continue continue
score_values = [(s.get("name", ""), int(s.get("score", 0))) for s in scores]
winner = None winner = None
max_score = -1
is_draw = False 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]: if len(score_values) >= 2 and score_values[0][1] == score_values[1][1]:
is_draw = True is_draw = True
# Для ничьи ищем "Draw" среди ставок
winner = "Draw" winner = "Draw"
else: else:
max_score = -1
for name, score_val in score_values: for name, score_val in score_values:
if score_val > max_score: if score_val > max_score:
max_score = score_val max_score = score_val
@ -589,19 +549,18 @@ async def settle_bets() -> list[str]:
continue continue
try: try:
with sqlite3.connect(config.BET_DB_PATH) as conn: with get_conn() as conn:
conn.row_factory = sqlite3.Row cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
bets = conn.execute( cur.execute(
"SELECT * FROM bets WHERE match_id = ? AND status = 'pending'", "SELECT * FROM bets WHERE match_id = %s AND status = 'pending'",
(match_id,), (match_id,),
).fetchall() )
bets = cur.fetchall()
wcur = conn.cursor()
for bet in bets: for bet in bets:
chosen = bet["chosen_team"] chosen = bet["chosen_team"]
bet_won = False
if is_draw: if is_draw:
# Ставка на ничью выигрывает
bet_won = chosen.casefold() in _DRAW_NAMES or chosen == "Draw" bet_won = chosen.casefold() in _DRAW_NAMES or chosen == "Draw"
else: else:
bet_won = chosen == winner bet_won = chosen == winner
@ -609,8 +568,8 @@ async def settle_bets() -> list[str]:
if bet_won: if bet_won:
winnings = round(bet["amount"] * bet["odds"], 1) winnings = round(bet["amount"] * bet["odds"], 1)
update_user_length(bet["user_id"], winnings) update_user_length(bet["user_id"], winnings)
conn.execute( wcur.execute(
"UPDATE bets SET status = 'won', payout = ?, resolved_ts = ? WHERE id = ?", "UPDATE bets SET status = 'won', payout = %s, resolved_ts = %s WHERE id = %s",
(winnings, now_ts, bet["id"]), (winnings, now_ts, bet["id"]),
) )
notifications.append( notifications.append(
@ -618,26 +577,22 @@ async def settle_bets() -> list[str]:
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})"
) )
else: else:
conn.execute( wcur.execute(
"UPDATE bets SET status = 'lost', payout = 0, resolved_ts = ? WHERE id = ?", "UPDATE bets SET status = 'lost', payout = 0, resolved_ts = %s WHERE id = %s",
(now_ts, bet["id"]), (now_ts, bet["id"]),
) )
notifications.append( notifications.append(
f"❌ user_id={bet['user_id']}: проиграл {bet['amount']:.1f} см " f"❌ user_id={bet['user_id']}: проиграл {bet['amount']:.1f} см "
f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})"
) )
conn.commit() except psycopg2.Error:
except sqlite3.Error:
logger.exception("Failed to settle bets") logger.exception("Failed to settle bets")
# Очистка старых ставок
cleanup_old_bets() cleanup_old_bets()
return notifications return notifications
async def debug_sports() -> str: async def debug_sports() -> str:
"""Диагностика: показать доступные виды спорта из API."""
if not config.ODDS_API_KEY: if not config.ODDS_API_KEY:
return "⚠️ ODDS_API_KEY не задан." return "⚠️ ODDS_API_KEY не задан."
@ -645,17 +600,14 @@ async def debug_sports() -> str:
if not sports: if not sports:
return "Не удалось получить список спортов от API." return "Не удалось получить список спортов от API."
# Показываем текущие ключи и проверяем их
current_keys = set(config.ODDS_SPORTS.values()) current_keys = set(config.ODDS_SPORTS.values())
valid_keys = {s.get("key") for s in sports} valid_keys = {s.get("key") for s in sports}
lines = ["🔍 Диагностика sport keys:\n"] lines = ["🔍 Диагностика sport keys:\n"]
for alias, key in config.ODDS_SPORTS.items(): for alias, key in config.ODDS_SPORTS.items():
status = "" if key in valid_keys else "НЕ НАЙДЕН" status = "" if key in valid_keys else "НЕ НАЙДЕН"
lines.append(f" {alias}{key} {status}") lines.append(f" {alias}{key} {status}")
# Показать доступные esports и football
lines.append("\n📋 Доступные eSports и футбол:") lines.append("\n📋 Доступные eSports и футбол:")
for s in sports: for s in sports:
key = s.get("key", "") key = s.get("key", "")

View file

@ -1,20 +1,20 @@
import random
import sqlite3
import logging import logging
import random
import psycopg2.extras
import config import config
from db import get_conn
from .economy import economy from .economy import economy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Символы слотов и их веса (чем выше вес — тем чаще выпадает)
SLOT_SYMBOLS = ["🍒", "🍋", "🔔", "💎", "7", "🍀"] SLOT_SYMBOLS = ["🍒", "🍋", "🔔", "💎", "7", "🍀"]
SLOT_WEIGHTS = [30, 25, 20, 12, 8, 5] # 🍒 чаще всего, 🍀 реже всего SLOT_WEIGHTS = [30, 25, 20, 12, 8, 5]
# Множители для выигрышей: {кол-во совпадений: (мин, макс)}
MULTIPLIERS = { MULTIPLIERS = {
2: (1.2, 1.8), # две одинаковых 2: (1.2, 1.8),
3: (2.0, 3.0), # три одинаковых (джекпот, редко) 3: (2.0, 3.0),
} }
CASINO_WIN_CHANCE = 0.35 CASINO_WIN_CHANCE = 0.35
@ -31,63 +31,53 @@ def spin_slots() -> tuple[list[str], int]:
counts = {} counts = {}
for s in reels: for s in reels:
counts[s] = counts.get(s, 0) + 1 counts[s] = counts.get(s, 0) + 1
max_match = max(counts.values()) return reels, max(counts.values())
return reels, max_match
def get_user_length(user_id: int) -> float | None: def get_user_length(user_id: int) -> float | None:
try: try:
with sqlite3.connect(config.PENIS_DB_PATH) as conn: with get_conn() as conn:
row = conn.execute( cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
"SELECT length FROM penis_stats WHERE user_id = ?", cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,))
(user_id,), row = cur.fetchone()
).fetchone() return float(row['balance']) if row else None
return float(row[0]) if row else None except Exception:
except sqlite3.Error: logger.exception("casino: failed to get balance")
logger.exception("casino: failed to get length")
return None return None
def update_user_length(user_id: int, delta: float) -> float | None: def update_user_length(user_id: int, delta: float) -> float | None:
try: try:
with sqlite3.connect(config.PENIS_DB_PATH) as conn: with get_conn() as conn:
row = conn.execute( cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
"SELECT length FROM penis_stats WHERE user_id = ?", cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,))
(user_id,), row = cur.fetchone()
).fetchone()
if row is None: if row is None:
return None return None
new_length = round(float(row[0]) + delta, 1) new_balance = round(float(row['balance']) + delta, 1)
# Не даём уйти ниже 0 при проигрыше if delta < 0 and new_balance < 0:
if delta < 0 and new_length < 0: new_balance = 0.0
new_length = 0.0 wcur = conn.cursor()
conn.execute( wcur.execute(
"UPDATE penis_stats SET length = ? WHERE user_id = ?", 'UPDATE user_balances SET balance = %s WHERE user_id = %s',
(new_length, user_id), (new_balance, user_id),
) )
conn.commit() return new_balance
return new_length except Exception:
except sqlite3.Error: logger.exception("casino: failed to update balance")
logger.exception("casino: failed to update length")
return None return None
def play_casino(user_id: int, bet: float) -> str: def play_casino(user_id: int, bet: float) -> str:
current = economy.get_user_balance(user_id) current = economy.get_user_balance(user_id)
# Проверяем максимум ставки
if bet > config.CASINO_MAX_BET: if bet > config.CASINO_MAX_BET:
return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай." return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай."
if bet <= 0: if bet <= 0:
return "Ставка должна быть больше нуля, фраер." return "Ставка должна быть больше нуля, фраер."
# Проверяем хватает ли
if bet > current: if bet > current:
return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер." return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер."
# Крутим
reels, matches = spin_slots() reels, matches = spin_slots()
slots_display = " | ".join(reels) slots_display = " | ".join(reels)
@ -95,10 +85,7 @@ def play_casino(user_id: int, bet: float) -> str:
min_mult, max_mult = MULTIPLIERS[matches] min_mult, max_mult = MULTIPLIERS[matches]
multiplier = round(random.uniform(min_mult, max_mult), 1) multiplier = round(random.uniform(min_mult, max_mult), 1)
winnings = round(bet * multiplier, 1) winnings = round(bet * multiplier, 1)
# Обновляем баланс в экономической системе
economy.update_balance(user_id, winnings, 'casino_win', f'Выигрыш в казино x{multiplier}') economy.update_balance(user_id, winnings, 'casino_win', f'Выигрыш в казино x{multiplier}')
new_balance = economy.get_user_balance(user_id) new_balance = economy.get_user_balance(user_id)
if matches == 3: if matches == 3:
@ -107,22 +94,21 @@ def play_casino(user_id: int, bet: float) -> str:
f"🎉 ДЖЕКПОТ!!! Три одинаковых!\n" f"🎉 ДЖЕКПОТ!!! Три одинаковых!\n"
f"Множитель: x{multiplier}\n" f"Множитель: x{multiplier}\n"
f"Выигрыш: +{winnings:.1f} см\n" f"Выигрыш: +{winnings:.1f} см\n"
f"<EFBFBD> Баланс: {new_balance:.1f} см" f"💰 Баланс: {new_balance:.1f} см"
) )
return ( return (
f"🎰 {slots_display}\n\n" f"🎰 {slots_display}\n\n"
f"✅ Выигрыш! Две совпали.\n" f"✅ Выигрыш! Две совпали.\n"
f"Множитель: x{multiplier}\n" f"Множитель: x{multiplier}\n"
f"Выигрыш: +{winnings:.1f} см\n" f"Выигрыш: +{winnings:.1f} см\n"
f"<EFBFBD> Баланс: {new_balance:.1f} см" f"💰 Баланс: {new_balance:.1f} см"
) )
else: else:
# Обновляем баланс в экономической системе
economy.update_balance(user_id, -bet, 'casino_loss', f'Проигрыш в казино -{bet} см') economy.update_balance(user_id, -bet, 'casino_loss', f'Проигрыш в казино -{bet} см')
new_balance = economy.get_user_balance(user_id) new_balance = economy.get_user_balance(user_id)
return ( return (
f"🎰 {slots_display}\n\n" f"🎰 {slots_display}\n\n"
f"❌ Мимо, братуха.\n" f"❌ Мимо, братуха.\n"
f"Проигрыш: -{bet:.1f} см\n" f"Проигрыш: -{bet:.1f} см\n"
f"<EFBFBD> Баланс: {new_balance:.1f} см" f"💰 Баланс: {new_balance:.1f} см"
) )

File diff suppressed because it is too large Load diff

22
main.py
View file

@ -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 dataclasses import dataclass
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from io import BytesIO 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: def init_polychaetsi_storage() -> None:
"""Инициализация json-хранилища статистики слова 'получается'.""" """Инициализация json-хранилища статистики слова 'получается'."""
path = config.POLYCHAETSI_STATS_PATH 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) 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) await handle_chat_message(message, store_message=False, allow_autonomous=True)
async def handle_penis_casino_cmd(message: Message): async def handle_penis_casino_cmd(message: Message):

View file

@ -5,4 +5,4 @@ aiohttp>=3.9.5
tzdata>=2024.1 tzdata>=2024.1
beautifulsoup4 beautifulsoup4
lxml lxml
pysqlite3-binary>=0.5.3 psycopg2-binary>=2.9.9

View file

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

View file

@ -145,15 +145,16 @@ async def get_profile(user: dict = Depends(get_current_user)) -> ProfileResponse
user_id = user["user_id"] user_id = user["user_id"]
length = await asyncio.to_thread(get_user_length, user_id) length = await asyncio.to_thread(get_user_length, user_id)
# Считаем активные ставки
import sqlite3
active_bets = 0 active_bets = 0
try: try:
with sqlite3.connect(config.BET_DB_PATH) as conn: from db import get_conn
row = conn.execute( with get_conn() as conn:
"SELECT COUNT(*) FROM bets WHERE user_id = ? AND status = 'pending'", cur = conn.cursor()
cur.execute(
"SELECT COUNT(*) FROM bets WHERE user_id = %s AND status = 'pending'",
(user_id,), (user_id,),
).fetchone() )
row = cur.fetchone()
active_bets = row[0] if row else 0 active_bets = row[0] if row else 0
except Exception: except Exception:
pass pass