1082 lines
46 KiB
Python
1082 lines
46 KiB
Python
import aiosqlite
|
|
import logging
|
|
from typing import Optional, Dict, Any, List, Tuple
|
|
from datetime import datetime, timezone
|
|
|
|
from config.settings import get_settings
|
|
|
|
try:
|
|
settings = get_settings()
|
|
DB_NAME = settings.DB_NAME
|
|
except Exception as e:
|
|
logging.critical(f"Could not load settings for database.py: {e}",
|
|
exc_info=True)
|
|
DB_NAME = "bot_database.sqlite3"
|
|
|
|
DB_BUSY_TIMEOUT_SECONDS = 15.0
|
|
|
|
|
|
def get_db_connection_manager():
|
|
"""
|
|
Возвращает awaitable/async context manager для соединения с SQLite.
|
|
"""
|
|
return aiosqlite.connect(DB_NAME, timeout=DB_BUSY_TIMEOUT_SECONDS)
|
|
|
|
|
|
async def _setup_db_connection(db: aiosqlite.Connection):
|
|
"""Применяет необходимые PRAGMA и row_factory к установленному соединению."""
|
|
try:
|
|
await db.execute("PRAGMA journal_mode=WAL;")
|
|
await db.execute(
|
|
f"PRAGMA busy_timeout = {int(DB_BUSY_TIMEOUT_SECONDS * 1000)};")
|
|
db.row_factory = aiosqlite.Row
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Failed to set PRAGMAs or row_factory on connection: {e}",
|
|
exc_info=True)
|
|
|
|
|
|
async def init_db():
|
|
"""Инициализирует схему БД."""
|
|
try:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
user_id INTEGER PRIMARY KEY,
|
|
username TEXT,
|
|
first_name TEXT,
|
|
last_name TEXT,
|
|
language_code TEXT DEFAULT 'en',
|
|
registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, /* TEXT as ISO8601 string, e.g. YYYY-MM-DD HH:MM:SS */
|
|
is_banned INTEGER DEFAULT 0,
|
|
panel_user_uuid TEXT UNIQUE,
|
|
referred_by_id INTEGER,
|
|
FOREIGN KEY (referred_by_id) REFERENCES users(user_id)
|
|
)
|
|
""")
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_users_panel_user_uuid ON users (panel_user_uuid);"
|
|
)
|
|
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
subscription_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER,
|
|
panel_user_uuid TEXT NOT NULL,
|
|
panel_subscription_uuid TEXT UNIQUE,
|
|
start_date TEXT, /* Storing as ISO TEXT */
|
|
end_date TEXT NOT NULL, /* Storing as ISO TEXT */
|
|
duration_months INTEGER,
|
|
is_active INTEGER DEFAULT 1,
|
|
status_from_panel TEXT,
|
|
traffic_limit_bytes INTEGER,
|
|
traffic_used_bytes INTEGER,
|
|
last_notification_sent TEXT /* Storing as TEXT YYYY-MM-DD */
|
|
)""")
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions (user_id);"
|
|
)
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_panel_user_uuid ON subscriptions (panel_user_uuid);"
|
|
)
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_end_date ON subscriptions (end_date);"
|
|
)
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_is_active ON subscriptions (is_active);"
|
|
)
|
|
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS payments (
|
|
payment_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
yookassa_payment_id TEXT UNIQUE,
|
|
idempotence_key TEXT UNIQUE,
|
|
amount REAL NOT NULL,
|
|
currency TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
description TEXT,
|
|
subscription_duration_months INTEGER,
|
|
promo_code_id INTEGER,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, /* TEXT as ISO8601 string */
|
|
updated_at TIMESTAMP, /* TEXT as ISO8601 string */
|
|
FOREIGN KEY (user_id) REFERENCES users(user_id),
|
|
FOREIGN KEY (promo_code_id) REFERENCES promo_codes(promo_code_id)
|
|
)""")
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments (user_id);"
|
|
)
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_payments_yookassa_payment_id ON payments (yookassa_payment_id);"
|
|
)
|
|
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS promo_codes (
|
|
promo_code_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
code TEXT UNIQUE NOT NULL,
|
|
bonus_days INTEGER NOT NULL,
|
|
max_activations INTEGER NOT NULL,
|
|
current_activations INTEGER DEFAULT 0,
|
|
is_active INTEGER DEFAULT 1,
|
|
created_by_admin_id INTEGER NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, /* TEXT as ISO8601 string */
|
|
valid_until TEXT NULL /* Storing as ISO TEXT YYYY-MM-DD HH:MM:SS */
|
|
)""")
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_promo_codes_code ON promo_codes (code);"
|
|
)
|
|
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS promo_code_activations (
|
|
activation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
promo_code_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, /* TEXT as ISO8601 string */
|
|
payment_id INTEGER,
|
|
FOREIGN KEY (promo_code_id) REFERENCES promo_codes(promo_code_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(user_id),
|
|
FOREIGN KEY (payment_id) REFERENCES payments(payment_id),
|
|
UNIQUE (promo_code_id, user_id)
|
|
)""")
|
|
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS message_logs (
|
|
log_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER,
|
|
telegram_username TEXT,
|
|
telegram_first_name TEXT,
|
|
event_type TEXT NOT NULL,
|
|
content TEXT,
|
|
raw_update_preview TEXT,
|
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, /* TEXT as ISO8601 string */
|
|
is_admin_event INTEGER DEFAULT 0,
|
|
target_user_id INTEGER,
|
|
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
|
)
|
|
""")
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_message_logs_user_id ON message_logs (user_id);"
|
|
)
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_message_logs_event_type ON message_logs (event_type);"
|
|
)
|
|
await db.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_message_logs_timestamp ON message_logs (timestamp);"
|
|
)
|
|
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS panel_sync_status (
|
|
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
last_sync_time TIMESTAMP, /* TEXT as ISO8601 string */
|
|
status TEXT,
|
|
details TEXT,
|
|
users_processed_from_panel INTEGER DEFAULT 0,
|
|
subscriptions_synced INTEGER DEFAULT 0
|
|
)""")
|
|
await db.execute(
|
|
"INSERT OR IGNORE INTO panel_sync_status (id, status, details) VALUES (1, 'never_run', 'System initialized')"
|
|
)
|
|
|
|
await db.commit()
|
|
logging.info("Database initialized/checked successfully.")
|
|
except Exception as e:
|
|
logging.error(f"Failed to initialize database: {e}", exc_info=True)
|
|
raise
|
|
|
|
|
|
async def add_user_if_not_exists(
|
|
user_id: int,
|
|
username: Optional[str],
|
|
first_name: Optional[str],
|
|
last_name: Optional[str],
|
|
lang_code: str = 'en',
|
|
referred_by_id: Optional[int] = None,
|
|
panel_user_uuid: Optional[str] = None) -> Tuple[bool, bool]:
|
|
"""Adds user if not exists. Sets referred_by_id for new users. Returns (success, was_new_user_flag)."""
|
|
was_new_user = False
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
try:
|
|
cursor = await db.execute(
|
|
"SELECT user_id, username, first_name, last_name, language_code, referred_by_id, panel_user_uuid, is_banned FROM users WHERE user_id = ?",
|
|
(user_id, ))
|
|
existing_user = await cursor.fetchone()
|
|
await cursor.close()
|
|
if existing_user:
|
|
update_fields = {}
|
|
if username is not None and username != existing_user[
|
|
'username']:
|
|
update_fields['username'] = username
|
|
if first_name is not None and first_name != existing_user[
|
|
'first_name']:
|
|
update_fields['first_name'] = first_name
|
|
if last_name is not None and last_name != existing_user[
|
|
'last_name']:
|
|
update_fields['last_name'] = last_name
|
|
|
|
if panel_user_uuid and existing_user['panel_user_uuid'] is None:
|
|
update_fields['panel_user_uuid'] = panel_user_uuid
|
|
|
|
if lang_code != existing_user['language_code']:
|
|
update_fields['language_code'] = lang_code
|
|
if update_fields:
|
|
set_clause = ", ".join(
|
|
[f"{field} = ?" for field in update_fields.keys()])
|
|
params = list(update_fields.values()) + [user_id]
|
|
await db.execute(
|
|
f"UPDATE users SET {set_clause} WHERE user_id = ?",
|
|
tuple(params))
|
|
else:
|
|
await db.execute(
|
|
"""INSERT INTO users (user_id, username, first_name, last_name, language_code, referred_by_id, panel_user_uuid, is_banned) VALUES (?, ?, ?, ?, ?, ?, ?, 0)""",
|
|
(user_id, username, first_name, last_name, lang_code,
|
|
referred_by_id, panel_user_uuid))
|
|
logging.info(
|
|
f"New user {user_id} added. Referred by: {referred_by_id or 'N/A'}."
|
|
)
|
|
was_new_user = True
|
|
await db.commit()
|
|
return True, was_new_user
|
|
except aiosqlite.IntegrityError as e:
|
|
if "UNIQUE constraint failed: users.panel_user_uuid" in str(
|
|
e) and panel_user_uuid:
|
|
res = await db.execute(
|
|
"UPDATE users SET user_id = ?, username = ?, first_name = ?, last_name = ?, language_code = COALESCE(?, language_code) WHERE panel_user_uuid = ? AND user_id IS NULL",
|
|
(user_id, username, first_name, last_name, lang_code,
|
|
panel_user_uuid))
|
|
if res.rowcount > 0:
|
|
await db.commit()
|
|
logging.info(
|
|
f"Linked panel user {panel_user_uuid} to TG {user_id}."
|
|
)
|
|
return True, False
|
|
else:
|
|
logging.error(
|
|
f"Conflict: Panel UUID {panel_user_uuid} exists.")
|
|
await db.rollback()
|
|
return False, False
|
|
else:
|
|
logging.error(f"DB integrity error for user {user_id}: {e}")
|
|
await db.rollback()
|
|
return False, False
|
|
except Exception as e:
|
|
logging.error(
|
|
f"DB error in add_user_if_not_exists for user {user_id}: {e}",
|
|
exc_info=True)
|
|
await db.rollback()
|
|
return False, False
|
|
|
|
|
|
async def get_user(
|
|
user_id: int,
|
|
db_conn: Optional[aiosqlite.Connection] = None
|
|
) -> Optional[aiosqlite.Row]:
|
|
sql = "SELECT user_id, username, first_name, last_name, language_code, referred_by_id, panel_user_uuid, is_banned, strftime('%Y-%m-%d %H:%M:%S', registration_date) as registration_date_str FROM users WHERE user_id = ?"
|
|
if db_conn:
|
|
cursor = await db_conn.execute(sql, (user_id, ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(sql, (user_id, ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row
|
|
|
|
|
|
async def get_user_by_telegram_username(
|
|
username: str,
|
|
db_conn: Optional[aiosqlite.Connection] = None
|
|
) -> Optional[aiosqlite.Row]:
|
|
sql = "SELECT user_id, username, first_name, last_name, language_code, referred_by_id, panel_user_uuid, is_banned, strftime('%Y-%m-%d %H:%M:%S', registration_date) as registration_date_str FROM users WHERE LOWER(username) = LOWER(?)"
|
|
clean_username = username.lstrip('@')
|
|
if db_conn:
|
|
cursor = await db_conn.execute(sql, (clean_username, ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(sql, (clean_username, ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row
|
|
|
|
|
|
async def get_user_by_panel_uuid(
|
|
panel_user_uuid: str,
|
|
db_conn: Optional[aiosqlite.Connection] = None
|
|
) -> Optional[aiosqlite.Row]:
|
|
sql = "SELECT user_id, username, first_name, last_name, language_code, referred_by_id, panel_user_uuid, is_banned, strftime('%Y-%m-%d %H:%M:%S', registration_date) as registration_date_str FROM users WHERE panel_user_uuid = ?"
|
|
if db_conn:
|
|
cursor = await db_conn.execute(sql, (panel_user_uuid, ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(sql, (panel_user_uuid, ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row
|
|
|
|
|
|
async def get_banned_users_list_paginated(
|
|
limit: int, offset: int) -> Tuple[List[aiosqlite.Row], int]:
|
|
sql_users = "SELECT user_id, username, first_name, last_name FROM users WHERE is_banned = 1 ORDER BY registration_date DESC LIMIT ? OFFSET ?"
|
|
sql_count = "SELECT COUNT(*) as total_banned FROM users WHERE is_banned = 1"
|
|
users_list: List[aiosqlite.Row] = []
|
|
total_banned = 0
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor_users = await db.execute(sql_users, (limit, offset))
|
|
users_list = await cursor_users.fetchall()
|
|
await cursor_users.close()
|
|
cursor_count = await db.execute(sql_count)
|
|
count_row = await cursor_count.fetchone()
|
|
await cursor_count.close()
|
|
if count_row: total_banned = count_row['total_banned']
|
|
return users_list, total_banned
|
|
|
|
|
|
async def get_user_active_subscription_end_date(
|
|
user_id: int,
|
|
db_conn: Optional[aiosqlite.Connection] = None) -> Optional[str]:
|
|
sql = "SELECT strftime('%Y-%m-%d', end_date) as end_date_str FROM subscriptions WHERE user_id = ? AND is_active = 1 AND DATETIME(end_date) > DATETIME('now', 'localtime') ORDER BY end_date DESC LIMIT 1"
|
|
if db_conn:
|
|
cursor = await db_conn.execute(sql, (user_id, ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row['end_date_str'] if row else None
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(sql, (user_id, ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row['end_date_str'] if row else None
|
|
|
|
|
|
async def update_user_language_code(user_id: int, lang_code: str):
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
await db.execute(
|
|
"UPDATE users SET language_code = ? WHERE user_id = ?",
|
|
(lang_code, user_id))
|
|
await db.commit()
|
|
|
|
|
|
async def set_user_ban_status_db(user_id: int, is_banned: bool) -> bool:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
try:
|
|
await db.execute(
|
|
"UPDATE users SET is_banned = ? WHERE user_id = ?",
|
|
(1 if is_banned else 0, user_id))
|
|
await db.commit()
|
|
return True
|
|
except Exception as e:
|
|
logging.error(f"Error setting ban status for {user_id}: {e}")
|
|
await db.rollback()
|
|
return False
|
|
|
|
|
|
async def update_user_panel_uuid(
|
|
user_id: int,
|
|
panel_user_uuid: str,
|
|
db_conn: Optional[aiosqlite.Connection] = None):
|
|
|
|
async def _operation(db_op: aiosqlite.Connection):
|
|
try:
|
|
await db_op.execute(
|
|
"UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",
|
|
(panel_user_uuid, user_id))
|
|
await db_op.commit()
|
|
except aiosqlite.IntegrityError:
|
|
logging.error(
|
|
f"Failed to update panel_uuid for user {user_id} (UNIQUE constraint)."
|
|
)
|
|
await db_op.rollback()
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Error in update_user_panel_uuid for {user_id}: {e}")
|
|
await db_op.rollback()
|
|
|
|
if db_conn:
|
|
await _setup_db_connection(db_conn
|
|
) if not db_conn.row_factory else None
|
|
await _operation(db_conn)
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
await _operation(db)
|
|
|
|
|
|
async def update_or_create_user_from_panel(
|
|
panel_data: Dict[str, Any],
|
|
db_conn: Optional[aiosqlite.Connection] = None) -> bool:
|
|
panel_uuid = panel_data.get('uuid')
|
|
username = panel_data.get('username')
|
|
if not panel_uuid:
|
|
logging.warning(
|
|
"update_or_create_user_from_panel: panel_uuid missing.")
|
|
return False
|
|
|
|
async def _operation(db_op: aiosqlite.Connection):
|
|
existing_user = await get_user_by_panel_uuid(panel_uuid, db_conn=db_op)
|
|
if not existing_user:
|
|
reg_date_str = panel_data.get('createdAt',
|
|
datetime.utcnow().isoformat())
|
|
if isinstance(reg_date_str, datetime):
|
|
reg_date_str = reg_date_str.isoformat()
|
|
await db_op.execute(
|
|
"INSERT INTO users (panel_user_uuid, username, registration_date, is_banned) VALUES (?, ?, ?, 0)",
|
|
(panel_uuid, username, reg_date_str))
|
|
await db_op.commit()
|
|
return True
|
|
|
|
try:
|
|
if db_conn:
|
|
await _setup_db_connection(db_conn
|
|
) if not db_conn.row_factory else None
|
|
return await _operation(db_conn)
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
return await _operation(db)
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Error in update_or_create_user_from_panel panel_uuid {panel_uuid}: {e}",
|
|
exc_info=True)
|
|
return False
|
|
|
|
|
|
async def update_or_create_subscription_from_panel(
|
|
panel_user_uuid: str,
|
|
sub_data: Dict[str, Any],
|
|
db_conn: Optional[aiosqlite.Connection] = None) -> bool:
|
|
|
|
async def _operation(db_op: aiosqlite.Connection):
|
|
cursor = await db_op.execute(
|
|
"SELECT user_id FROM users WHERE panel_user_uuid = ?",
|
|
(panel_user_uuid, ))
|
|
user_row = await cursor.fetchone()
|
|
await cursor.close()
|
|
bot_user_id = user_row['user_id'] if user_row else None
|
|
panel_sub_link_uuid = sub_data.get('subscriptionUuid') or sub_data.get(
|
|
'shortUuid')
|
|
if not panel_sub_link_uuid:
|
|
logging.warning(
|
|
f"No panel_subscription_uuid or shortUuid for panel user {panel_user_uuid}"
|
|
)
|
|
return False
|
|
end_date_str = sub_data.get('expireAt')
|
|
end_date = None
|
|
if end_date_str:
|
|
try:
|
|
end_date = datetime.fromisoformat(
|
|
end_date_str.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
logging.warning(
|
|
f"Bad expireAt: {end_date_str} for {panel_user_uuid}")
|
|
return False
|
|
if not end_date:
|
|
logging.warning(f"No end_date for {panel_user_uuid}")
|
|
return False
|
|
status_panel = sub_data.get('status', 'UNKNOWN').upper()
|
|
is_active_panel = 1 if status_panel == 'ACTIVE' else 0
|
|
traffic_limit = sub_data.get('trafficLimitBytes')
|
|
traffic_used = sub_data.get('usedTrafficBytes',
|
|
sub_data.get('lifetimeUsedTrafficBytes'))
|
|
start_date_iso = sub_data.get('createdAt',
|
|
datetime.utcnow().isoformat())
|
|
if isinstance(start_date_iso, datetime):
|
|
start_date_iso = start_date_iso.isoformat()
|
|
duration_months_val = sub_data.get('duration_months')
|
|
upsert_sql = """ INSERT INTO subscriptions (user_id, panel_user_uuid, panel_subscription_uuid, start_date, end_date, duration_months, is_active, status_from_panel, traffic_limit_bytes, traffic_used_bytes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(panel_subscription_uuid) DO UPDATE SET user_id = excluded.user_id, panel_user_uuid = excluded.panel_user_uuid, start_date = excluded.start_date, end_date = excluded.end_date, duration_months = excluded.duration_months, is_active = excluded.is_active, status_from_panel = excluded.status_from_panel, traffic_limit_bytes = excluded.traffic_limit_bytes, traffic_used_bytes = excluded.traffic_used_bytes, last_notification_sent = NULL; """
|
|
params = (bot_user_id, panel_user_uuid, panel_sub_link_uuid,
|
|
start_date_iso, end_date.isoformat(), duration_months_val,
|
|
is_active_panel, status_panel, traffic_limit, traffic_used)
|
|
await db_op.execute(upsert_sql, params)
|
|
await db_op.commit()
|
|
return True
|
|
|
|
try:
|
|
if db_conn:
|
|
await _setup_db_connection(db_conn
|
|
) if not db_conn.row_factory else None
|
|
return await _operation(db_conn)
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
return await _operation(db)
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Error in update_or_create_subscription_from_panel for {panel_user_uuid}: {e}",
|
|
exc_info=True)
|
|
return False
|
|
|
|
|
|
async def update_sync_status(status: str,
|
|
details: str,
|
|
users_processed: int = 0,
|
|
subs_synced: int = 0):
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
await db.execute(
|
|
"UPDATE panel_sync_status SET last_sync_time=CURRENT_TIMESTAMP, status=?, details=?, users_processed_from_panel=?, subscriptions_synced=? WHERE id=1",
|
|
(status, details, users_processed, subs_synced))
|
|
await db.commit()
|
|
|
|
|
|
async def get_last_sync_status() -> Optional[aiosqlite.Row]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(
|
|
"SELECT * FROM panel_sync_status WHERE id = 1")
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
return row
|
|
|
|
|
|
async def get_all_users_for_broadcast() -> List[aiosqlite.Row]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(
|
|
"SELECT user_id FROM users WHERE user_id IS NOT NULL AND is_banned = 0"
|
|
)
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
return rows
|
|
|
|
|
|
async def get_user_count_stats() -> Dict[str, int]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(DISTINCT user_id) as count FROM users WHERE user_id IS NOT NULL"
|
|
)
|
|
total_users_c_row = await cursor.fetchone()
|
|
await cursor.close()
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(DISTINCT user_id) as count FROM users WHERE user_id IS NOT NULL AND is_banned = 1"
|
|
)
|
|
banned_users_c_row = await cursor.fetchone()
|
|
await cursor.close()
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(DISTINCT user_id) as count FROM subscriptions WHERE user_id IS NOT NULL AND is_active = 1 AND DATETIME(end_date) > DATETIME('now', 'localtime')"
|
|
)
|
|
active_subs_c_row = await cursor.fetchone()
|
|
await cursor.close()
|
|
return {
|
|
"total_users":
|
|
total_users_c_row['count'] if total_users_c_row else 0,
|
|
"banned_users":
|
|
banned_users_c_row['count'] if banned_users_c_row else 0,
|
|
"users_with_active_subscriptions":
|
|
active_subs_c_row['count'] if active_subs_c_row else 0
|
|
}
|
|
|
|
|
|
async def get_payment_logs(limit: int = 20,
|
|
offset: int = 0) -> List[aiosqlite.Row]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(
|
|
"""SELECT p.payment_id, p.user_id, u.username, p.amount, p.currency, p.status, p.description, strftime('%Y-%m-%d %H:%M:%S', p.created_at) as created_at FROM payments p LEFT JOIN users u ON p.user_id = u.user_id ORDER BY p.created_at DESC LIMIT ? OFFSET ?""",
|
|
(limit, offset))
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
return rows
|
|
|
|
|
|
async def log_user_action(user_id: Optional[int],
|
|
event_type: str,
|
|
content: Optional[str] = None,
|
|
telegram_username: Optional[str] = None,
|
|
telegram_first_name: Optional[str] = None,
|
|
is_admin_event: bool = False,
|
|
target_user_id: Optional[int] = None,
|
|
raw_update_preview: Optional[str] = None):
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
try:
|
|
await db.execute(
|
|
"""INSERT INTO message_logs (user_id, telegram_username, telegram_first_name, event_type, content, raw_update_preview, is_admin_event, target_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(user_id, telegram_username, telegram_first_name, event_type,
|
|
content, raw_update_preview, 1 if is_admin_event else 0,
|
|
target_user_id))
|
|
await db.commit()
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Failed to log action for user {user_id}, type {event_type}: {e}",
|
|
exc_info=True)
|
|
|
|
|
|
async def get_message_logs_db(limit: int = 20,
|
|
offset: int = 0) -> List[aiosqlite.Row]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(
|
|
"""SELECT log_id, user_id, telegram_username, telegram_first_name, event_type, content, strftime('%Y-%m-%d %H:%M:%S', timestamp) as timestamp_str, is_admin_event FROM message_logs ORDER BY timestamp DESC LIMIT ? OFFSET ?""",
|
|
(limit, offset))
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
return rows
|
|
|
|
|
|
async def count_all_message_logs() -> int:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute("SELECT COUNT(*) as count FROM message_logs")
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
return row['count'] if row else 0
|
|
|
|
|
|
async def get_user_message_logs_paginated(user_id_to_search: int, limit: int,
|
|
offset: int) -> List[aiosqlite.Row]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(
|
|
"""SELECT log_id, user_id, telegram_username, telegram_first_name, event_type, content, strftime('%Y-%m-%d %H:%M:%S', timestamp) as timestamp_str FROM message_logs WHERE user_id = ? OR target_user_id = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?""",
|
|
(user_id_to_search, user_id_to_search, limit, offset))
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
return rows
|
|
|
|
|
|
async def count_user_message_logs(user_id_to_search: int) -> int:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(
|
|
"SELECT COUNT(*) as count FROM message_logs WHERE user_id = ? OR target_user_id = ?",
|
|
(user_id_to_search, user_id_to_search))
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
return row['count'] if row else 0
|
|
|
|
|
|
async def add_payment_record(user_id: int,
|
|
yookassa_payment_id: Optional[str],
|
|
idempotence_key: Optional[str],
|
|
amount: float,
|
|
currency: str,
|
|
status: str,
|
|
description: str,
|
|
sub_months: int,
|
|
promo_id: Optional[int] = None) -> Optional[int]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
try:
|
|
cursor = await db.execute(
|
|
"""INSERT INTO payments (user_id, yookassa_payment_id, idempotence_key, amount, currency, status, description, subscription_duration_months, promo_code_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(user_id, yookassa_payment_id, idempotence_key, amount,
|
|
currency, status, description, sub_months, promo_id))
|
|
await db.commit()
|
|
return cursor.lastrowid
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Failed to add payment record user {user_id}, yk_id {yookassa_payment_id}: {e}"
|
|
)
|
|
await db.rollback()
|
|
return None
|
|
|
|
|
|
async def update_payment_status(
|
|
payment_db_id: Optional[int] = None,
|
|
yookassa_payment_id: Optional[str] = None,
|
|
new_status: Optional[str] = None,
|
|
db_conn: Optional[aiosqlite.Connection] = None):
|
|
|
|
async def _operation(db_op: aiosqlite.Connection):
|
|
if new_status is None: return
|
|
if payment_db_id:
|
|
await db_op.execute(
|
|
"UPDATE payments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE payment_id = ?",
|
|
(new_status, payment_db_id))
|
|
elif yookassa_payment_id:
|
|
await db_op.execute(
|
|
"UPDATE payments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE yookassa_payment_id = ?",
|
|
(new_status, yookassa_payment_id))
|
|
else:
|
|
logging.warning(
|
|
"update_payment_status called without payment_id or yookassa_payment_id"
|
|
)
|
|
return
|
|
await db_op.commit()
|
|
|
|
if db_conn:
|
|
await _setup_db_connection(db_conn
|
|
) if not db_conn.row_factory else None
|
|
await _operation(db_conn)
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
await _operation(db)
|
|
|
|
|
|
async def get_payment_by_yookassa_id(
|
|
yookassa_payment_id: str) -> Optional[aiosqlite.Row]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(
|
|
"SELECT * FROM payments WHERE yookassa_payment_id = ?",
|
|
(yookassa_payment_id, ))
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
return row
|
|
|
|
|
|
async def create_promo_code_db(
|
|
code: str,
|
|
bonus_days: int,
|
|
max_activations: int,
|
|
admin_id: int,
|
|
valid_until_dt: Optional[datetime] = None) -> Optional[int]:
|
|
valid_until_iso: Optional[str] = valid_until_dt.isoformat(
|
|
) if valid_until_dt else None
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
try:
|
|
cursor = await db.execute(
|
|
"INSERT INTO promo_codes (code, bonus_days, max_activations, created_by_admin_id, valid_until) VALUES (?, ?, ?, ?, ?)",
|
|
(code.upper(), bonus_days, max_activations, admin_id,
|
|
valid_until_iso))
|
|
await db.commit()
|
|
return cursor.lastrowid
|
|
except aiosqlite.IntegrityError:
|
|
logging.warning(f"Promo code {code.upper()} exists.")
|
|
await db.rollback()
|
|
return None
|
|
except Exception as e:
|
|
logging.error(f"Error creating promo {code.upper()}: {e}")
|
|
await db.rollback()
|
|
return None
|
|
|
|
|
|
async def get_promo_codes_db(is_active_only: bool = True,
|
|
limit: int = 20,
|
|
offset: int = 0) -> List[aiosqlite.Row]:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
query = "SELECT promo_code_id, code, bonus_days, max_activations, current_activations, is_active, created_by_admin_id, created_at, valid_until FROM promo_codes"
|
|
params = []
|
|
conditions = []
|
|
if is_active_only: conditions.append("is_active = 1")
|
|
conditions.append(
|
|
"(valid_until IS NULL OR DATETIME(valid_until) > DATETIME('now', 'localtime'))"
|
|
)
|
|
if conditions: query += " WHERE " + " AND ".join(conditions)
|
|
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
params.extend([limit, offset])
|
|
cursor = await db.execute(query, tuple(params))
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
return rows
|
|
|
|
|
|
async def get_promo_code_by_code(
|
|
code: str,
|
|
db_conn: Optional[aiosqlite.Connection] = None
|
|
) -> Optional[aiosqlite.Row]:
|
|
sql = "SELECT * FROM promo_codes WHERE code = ? AND is_active = 1 AND (DATETIME(valid_until) IS NULL OR DATETIME(valid_until) > DATETIME('now', 'localtime'))"
|
|
if db_conn:
|
|
cursor = await db_conn.execute(sql, (code.upper(), ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(sql, (code.upper(), ))
|
|
row = await cursor.fetchone()
|
|
if cursor:
|
|
await cursor.close()
|
|
return row
|
|
|
|
|
|
async def increment_promo_activation(promo_code_id: int,
|
|
user_id: int,
|
|
db_conn: aiosqlite.Connection,
|
|
payment_id: Optional[int] = None) -> bool:
|
|
|
|
db = db_conn
|
|
try:
|
|
cursor = await db.execute(
|
|
"SELECT 1 FROM promo_code_activations WHERE promo_code_id = ? AND user_id = ?",
|
|
(promo_code_id, user_id))
|
|
existing = await cursor.fetchone()
|
|
await cursor.close()
|
|
if existing:
|
|
logging.info(f"User {user_id} already used promo {promo_code_id}.")
|
|
return False
|
|
await db.execute(
|
|
"INSERT INTO promo_code_activations (promo_code_id, user_id, payment_id) VALUES (?, ?, ?)",
|
|
(promo_code_id, user_id, payment_id))
|
|
await db.execute(
|
|
"UPDATE promo_codes SET current_activations = current_activations + 1 WHERE promo_code_id = ?",
|
|
(promo_code_id, ))
|
|
return True
|
|
except aiosqlite.IntegrityError:
|
|
logging.warning(
|
|
f"IntegrityError on promo activation p:{promo_code_id} u:{user_id}."
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Error promo activation p:{promo_code_id} u:{user_id}: {e}",
|
|
exc_info=True)
|
|
return False
|
|
|
|
|
|
async def get_all_message_logs_paginated(limit: int,
|
|
offset: int) -> List[aiosqlite.Row]:
|
|
"""
|
|
Fetches a paginated list of all message logs, ordered by the newest first.
|
|
"""
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
|
|
sql = """
|
|
SELECT
|
|
log_id,
|
|
user_id,
|
|
telegram_username,
|
|
telegram_first_name,
|
|
event_type,
|
|
content,
|
|
strftime('%Y-%m-%d %H:%M:%S', timestamp) as timestamp_str,
|
|
is_admin_event,
|
|
target_user_id
|
|
FROM message_logs
|
|
ORDER BY timestamp DESC
|
|
LIMIT ? OFFSET ?
|
|
"""
|
|
try:
|
|
cursor = await db.execute(sql, (limit, offset))
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
return rows
|
|
except Exception as e:
|
|
logging.error(f"Error fetching all message logs: {e}",
|
|
exc_info=True)
|
|
return []
|
|
|
|
|
|
async def has_had_any_subscription(
|
|
user_id: int, db_conn: Optional[aiosqlite.Connection] = None) -> bool:
|
|
"""Checks if a user has ever had any subscription (trial or paid)."""
|
|
sql = "SELECT 1 FROM subscriptions WHERE user_id = ? LIMIT 1"
|
|
if db_conn:
|
|
cursor = await db_conn.execute(sql, (user_id, ))
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
return row is not None
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
cursor = await db.execute(sql, (user_id, ))
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
return row is not None
|
|
|
|
|
|
async def sync_panel_user_data(
|
|
panel_user_dict: Dict[str, Any],
|
|
db_conn_passed: Optional[aiosqlite.Connection] = None) -> bool:
|
|
"""
|
|
Synchronizes a single panel user's data (user info and subscription) with the local bot database.
|
|
Prioritizes matching/creating bot user by panel_user_dict['telegramId'].
|
|
Manages its own transaction if db_conn_passed is None.
|
|
"""
|
|
panel_uuid = panel_user_dict.get('uuid')
|
|
telegram_id_from_panel = panel_user_dict.get('telegramId')
|
|
panel_username = panel_user_dict.get('username')
|
|
|
|
if not panel_uuid:
|
|
logging.warning(
|
|
f"Sync: Panel user data missing 'uuid'. Data: {panel_user_dict}")
|
|
return False
|
|
if not telegram_id_from_panel:
|
|
logging.info(
|
|
f"Sync: Panel user {panel_uuid} (username: {panel_username}) has no 'telegramId'. Skipping TG ID based sync for this user."
|
|
)
|
|
|
|
return False
|
|
|
|
async def _operation(db: aiosqlite.Connection):
|
|
|
|
bot_user_id = int(telegram_id_from_panel)
|
|
|
|
existing_bot_user = await get_user(bot_user_id, db_conn=db)
|
|
|
|
if existing_bot_user:
|
|
|
|
if existing_bot_user['panel_user_uuid'] != panel_uuid:
|
|
if existing_bot_user['panel_user_uuid'] is not None:
|
|
logging.warning(
|
|
f"Sync: TG User {bot_user_id} already linked to panel_uuid {existing_bot_user['panel_user_uuid']}, but panel now provides {panel_uuid} for this TG ID. Updating to new panel_uuid."
|
|
)
|
|
|
|
cursor_conflict = await db.execute(
|
|
"SELECT user_id FROM users WHERE panel_user_uuid = ? AND user_id != ?",
|
|
(panel_uuid, bot_user_id))
|
|
conflicting_user = await cursor_conflict.fetchone()
|
|
await cursor_conflict.close()
|
|
if conflicting_user:
|
|
logging.error(
|
|
f"Sync: CRITICAL CONFLICT! New panel_uuid {panel_uuid} (for TG ID {bot_user_id}) is already linked to different TG User {conflicting_user['user_id']}. Skipping user update for panel_uuid."
|
|
)
|
|
else:
|
|
await db.execute(
|
|
"UPDATE users SET panel_user_uuid = ?, username = ? WHERE user_id = ?",
|
|
(panel_uuid, panel_username, bot_user_id))
|
|
logging.info(
|
|
f"Sync: Updated panel_uuid for existing TG user {bot_user_id} to {panel_uuid}."
|
|
)
|
|
else:
|
|
|
|
if panel_username and existing_bot_user[
|
|
'username'] != panel_username:
|
|
await db.execute(
|
|
"UPDATE users SET username = ? WHERE user_id = ?",
|
|
(panel_username, bot_user_id))
|
|
logging.info(
|
|
f"Sync: Updated username for TG user {bot_user_id} from panel username {panel_username}."
|
|
)
|
|
else:
|
|
|
|
cursor_conflict = await db.execute(
|
|
"SELECT user_id FROM users WHERE panel_user_uuid = ?",
|
|
(panel_uuid, ))
|
|
conflicting_user = await cursor_conflict.fetchone()
|
|
await cursor_conflict.close()
|
|
if conflicting_user:
|
|
logging.error(
|
|
f"Sync: CRITICAL CONFLICT! Panel UUID {panel_uuid} (for new TG ID {bot_user_id}) is already linked to existing TG User {conflicting_user['user_id']}. Skipping new user creation."
|
|
)
|
|
return False
|
|
|
|
reg_date_str = panel_user_dict.get(
|
|
'createdAt',
|
|
datetime.now(timezone.utc).isoformat())
|
|
if isinstance(reg_date_str, datetime):
|
|
reg_date_str = reg_date_str.isoformat()
|
|
await db.execute(
|
|
"""INSERT INTO users (user_id, username, panel_user_uuid, registration_date, is_banned, language_code)
|
|
VALUES (?, ?, ?, ?, 0, ?)""",
|
|
(bot_user_id, panel_username, panel_uuid, reg_date_str,
|
|
settings.DEFAULT_LANGUAGE))
|
|
logging.info(
|
|
f"Sync: New user created in bot DB from panel data: TG ID {bot_user_id}, Panel UUID {panel_uuid}, Panel Username {panel_username}"
|
|
)
|
|
|
|
panel_sub_link_uuid = panel_user_dict.get(
|
|
'subscriptionUuid') or panel_user_dict.get('shortUuid')
|
|
if not panel_sub_link_uuid:
|
|
logging.warning(
|
|
f"Sync: Panel user {panel_uuid} (TG ID: {bot_user_id}) has no 'subscriptionUuid' or 'shortUuid'. Cannot sync subscription link."
|
|
)
|
|
|
|
await db.commit()
|
|
return True
|
|
|
|
end_date_str = panel_user_dict.get('expireAt')
|
|
end_date_obj = None
|
|
if end_date_str:
|
|
try:
|
|
end_date_obj = datetime.fromisoformat(
|
|
end_date_str.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
logging.warning(
|
|
f"Sync: Bad expireAt '{end_date_str}' for panel user {panel_uuid}. Skipping subscription update."
|
|
)
|
|
return True
|
|
|
|
if not end_date_obj:
|
|
logging.warning(
|
|
f"Sync: No valid end_date for panel user {panel_uuid}. Skipping subscription update."
|
|
)
|
|
return True
|
|
|
|
status_panel = panel_user_dict.get('status', 'UNKNOWN').upper()
|
|
is_active_panel = 1 if status_panel == 'ACTIVE' else 0
|
|
traffic_limit = panel_user_dict.get('trafficLimitBytes')
|
|
traffic_used = panel_user_dict.get(
|
|
'usedTrafficBytes',
|
|
panel_user_dict.get('lifetimeUsedTrafficBytes'))
|
|
|
|
start_date_iso = panel_user_dict.get(
|
|
'createdAt',
|
|
datetime.now(timezone.utc).isoformat())
|
|
if isinstance(start_date_iso, datetime):
|
|
start_date_iso = start_date_iso.isoformat()
|
|
|
|
duration_months_val = None
|
|
|
|
await db.execute(
|
|
"UPDATE subscriptions SET is_active = 0 WHERE user_id = ? AND panel_user_uuid = ? AND is_active = 1 AND panel_subscription_uuid != ?",
|
|
(bot_user_id, panel_uuid, panel_sub_link_uuid))
|
|
|
|
upsert_sub_sql = """
|
|
INSERT INTO subscriptions (
|
|
user_id, panel_user_uuid, panel_subscription_uuid,
|
|
start_date, end_date, duration_months,
|
|
is_active, status_from_panel, traffic_limit_bytes, traffic_used_bytes,
|
|
last_notification_sent
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
ON CONFLICT(panel_subscription_uuid) DO UPDATE SET
|
|
user_id = excluded.user_id,
|
|
panel_user_uuid = excluded.panel_user_uuid,
|
|
start_date = excluded.start_date,
|
|
end_date = excluded.end_date,
|
|
duration_months = excluded.duration_months,
|
|
is_active = excluded.is_active,
|
|
status_from_panel = excluded.status_from_panel,
|
|
traffic_limit_bytes = excluded.traffic_limit_bytes,
|
|
traffic_used_bytes = excluded.traffic_used_bytes,
|
|
last_notification_sent = NULL;
|
|
"""
|
|
sub_params = (bot_user_id, panel_uuid, panel_sub_link_uuid,
|
|
start_date_iso, end_date_obj.isoformat(),
|
|
duration_months_val, is_active_panel, status_panel,
|
|
traffic_limit, traffic_used)
|
|
await db.execute(upsert_sub_sql, sub_params)
|
|
logging.info(
|
|
f"Sync: Subscription upserted for TG ID {bot_user_id}, Panel UUID {panel_uuid}, Link ID {panel_sub_link_uuid}"
|
|
)
|
|
|
|
await db.commit()
|
|
return True
|
|
|
|
if db_conn_passed:
|
|
return await _operation(db_conn_passed)
|
|
else:
|
|
async with get_db_connection_manager() as db:
|
|
await _setup_db_connection(db)
|
|
try:
|
|
return await _operation(db)
|
|
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Sync: General DB error during sync_panel_user_data for panel UUID {panel_uuid}: {e}",
|
|
exc_info=True)
|
|
await db.rollback()
|
|
return False
|