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:
parent
b0e03f8e23
commit
add3553ce0
12 changed files with 1161 additions and 1015 deletions
9
.env
9
.env
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
WHERE chat_id = ? AND user_id = ?
|
|
||||||
ORDER BY id ASC
|
|
||||||
""",
|
|
||||||
(chat_id, user_id),
|
(chat_id, user_id),
|
||||||
).fetchall()
|
)
|
||||||
|
return cur.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",
|
||||||
|
|
|
||||||
|
|
@ -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
35
db.py
Normal 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)
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
206
games/betting.py
206
games/betting.py
|
|
@ -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('''
|
||||||
|
INSERT INTO bets
|
||||||
|
(user_id, match_id, sport, home_team, away_team,
|
||||||
chosen_team, amount, odds, status, payout, created_ts)
|
chosen_team, amount, odds, status, payout, created_ts)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)""",
|
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),
|
user_id, match["id"], sport, match["home"], match["away"],
|
||||||
)
|
chosen_team, amount, team_odds, now_ts,
|
||||||
conn.commit()
|
))
|
||||||
except sqlite3.Error:
|
except psycopg2.Error:
|
||||||
logger.exception("Failed to place bet")
|
logger.exception("Failed to place bet")
|
||||||
update_user_length(user_id, amount)
|
update_user_length(user_id, amount)
|
||||||
return "Ошибка БД."
|
return "Ошибка БД."
|
||||||
|
|
@ -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", "")
|
||||||
|
|
|
||||||
|
|
@ -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} см"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
772
games/economy.py
772
games/economy.py
File diff suppressed because it is too large
Load diff
22
main.py
22
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 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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
369
scripts/migrate_sqlite_to_pg.py
Normal file
369
scripts/migrate_sqlite_to_pg.py
Normal 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()
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue