diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 00000000..410b1bac
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -0,0 +1,35 @@
+name: Code Formatting
+
+on: [push, pull_request]
+
+jobs:
+ format:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install ruff
+
+ - name: Run Ruff formatter with pyproject.toml
+ run: ruff format . --config pyproject.toml --exclude main.py,handlers/payments
+
+ - name: Check for changes
+ id: check_changes
+ run: |
+ git diff --exit-code || echo "changes_detected=true" >> $GITHUB_OUTPUT
+
+ - name: Commit and push changes
+ if: steps.check_changes.outputs.changes_detected == 'true'
+ run: |
+ git config --global user.name 'GitHub Actions'
+ git config --global user.email 'actions@github.com'
+ git add .
+ git commit -m "Auto-format code with Ruff using pyproject.toml"
+ git push
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index fcb54e4b..148fafa2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,5 +54,6 @@ scripts
models.py
Dockerfile
.csv
-
-setup.py
\ No newline at end of file
+/logs
+setup.py
+.ruff_cache
\ No newline at end of file
diff --git a/Makefile b/Makefile
index aef0b00f..d949db7d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
formatting:
- @echo "Running Ruff..." && ruff check . --fix
- @echo "Running Ruff format..." && ruff format .
+ @echo "Running Ruff format..." && ruff format . --config pyproject.toml --exclude main.py,handlers/payments
+
+ @echo "Running Ruff..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments --fix
lint:
- @echo "Running Ruff checks..." && ruff check .
\ No newline at end of file
+ @echo "Running Ruff checks..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments
\ No newline at end of file
diff --git a/backup.py b/backup.py
index 10135d71..8550e619 100644
--- a/backup.py
+++ b/backup.py
@@ -29,8 +29,12 @@ async def backup_database() -> Exception | None:
return e
-def _create_database_backup() -> (str | None, Exception | None):
+def _create_database_backup() -> tuple[str | None, Exception | None]:
date_formatted = datetime.now().strftime("%Y-%m-%d-%H%M%S")
+
+ if not os.path.exists(BACK_DIR):
+ os.makedirs(BACK_DIR)
+
filename = os.path.join(BACK_DIR, f"{DB_NAME}-backup-{date_formatted}.sql")
try:
@@ -93,18 +97,16 @@ async def create_backup_and_send_to_admins(xui) -> None:
async def _send_backup_to_admins(backup_file_path: str) -> None:
try:
+ import aiofiles
+
from bot import bot
- with open(backup_file_path, "rb") as backup_file:
- backup_input_file = BufferedInputFile(
- file=backup_file.read(),
- filename=os.path.basename(backup_file_path)
- )
+
+ async with aiofiles.open(backup_file_path, "rb") as backup_file:
+ backup_data = await backup_file.read()
+ backup_input_file = BufferedInputFile(file=backup_data, filename=os.path.basename(backup_file_path))
admin_ids = ADMIN_ID if isinstance(ADMIN_ID, list) else [ADMIN_ID]
for admin_id in admin_ids:
- await bot.send_document(
- chat_id=admin_id,
- document=backup_input_file
- )
+ await bot.send_document(chat_id=admin_id, document=backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {admin_id}")
except Exception as e:
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
diff --git a/bot.py b/bot.py
index 947f5b42..027d2604 100644
--- a/bot.py
+++ b/bot.py
@@ -3,16 +3,16 @@ import traceback
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
+from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
+from aiogram.filters import ExceptionTypeFilter
from aiogram.fsm.storage.memory import MemoryStorage
-from aiogram.types import ErrorEvent
+from aiogram.types import BufferedInputFile, ErrorEvent
+from aiogram.utils.markdown import hbold
-from config import API_TOKEN
+from config import ADMIN_ID, API_TOKEN
+from filters.private import IsPrivateFilter
from logger import logger
-from middlewares.admin import AdminMiddleware
-from middlewares.database import DatabaseMiddleware
-from middlewares.delete import DeleteMessageMiddleware
-from middlewares.logging import LoggingMiddleware
-from middlewares.user import UserMiddleware
+from middlewares import register_middleware
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
storage = MemoryStorage()
@@ -20,30 +20,35 @@ dp = Dispatcher(bot=bot, storage=storage)
version = "4.0.0-preAlpha(14-dev)"
-dp.message.middleware(LoggingMiddleware())
-dp.callback_query.middleware(LoggingMiddleware())
+register_middleware(dp)
-dp.message.middleware(AdminMiddleware())
-dp.callback_query.middleware(AdminMiddleware())
-dp.message.middleware(UserMiddleware())
-dp.callback_query.middleware(UserMiddleware())
-
-dp.message.middleware(DatabaseMiddleware())
-dp.callback_query.middleware(DatabaseMiddleware())
-
-# dp.message.middleware(ThrottlingMiddleware(limit=1))
-# dp.callback_query.middleware(ThrottlingMiddleware(limit=1))
-
-dp.message.outer_middleware(DeleteMessageMiddleware())
-dp.callback_query.outer_middleware(DeleteMessageMiddleware())
+dp.message.filter(IsPrivateFilter())
+dp.callback_query.filter(IsPrivateFilter())
-@dp.error()
-async def error_handler(event: ErrorEvent):
- logger.error(
- "Ошибка в боте:\n"
- f"Исключение: {event.exception}\n"
- f"Тип: {type(event.exception)}\n"
- f"Update: {event.update}\n"
- f"Трассировка:\n{traceback.format_exc()}"
- )
+@dp.errors(ExceptionTypeFilter(Exception))
+async def errors_handler(
+ event: ErrorEvent,
+ bot: Bot,
+) -> bool:
+ if isinstance(event.exception, TelegramForbiddenError):
+ logger.info(f"User {event.update.message.from_user.id} blocked the bot.")
+ return True
+ logger.exception(f"Update: {event.update}\nException: {event.exception}")
+ if not ADMIN_ID:
+ return True
+ try:
+ for admin_id in ADMIN_ID:
+ await bot.send_document(
+ chat_id=admin_id,
+ document=BufferedInputFile(
+ traceback.format_exc().encode(),
+ filename=f"error_{event.update.update_id}.txt",
+ ),
+ caption=f"{hbold(type(event.exception).__name__)}: {str(event.exception)[:1021]}...",
+ )
+ except TelegramBadRequest as exception:
+ logger.warning(f"Failed to send error details: {exception}")
+ except Exception as exception:
+ logger.error(f"Unexpected error in error handler: {exception}")
+ return True
diff --git a/client.py b/client.py
index 10953586..219ccca5 100644
--- a/client.py
+++ b/client.py
@@ -1,80 +1,101 @@
+from dataclasses import dataclass
+from typing import Any
+
import py3xui
from config import LIMIT_IP, SUPERNODE
from logger import logger
-async def add_client(
- xui,
- client_id: str,
- email: str,
- tg_id: str,
- limit_ip: int,
- total_gb: int,
- expiry_time: int,
- enable: bool,
- flow: str,
- inbound_id: int,
- sub_id
-):
+@dataclass
+class ClientConfig:
+ """Конфигурация клиента для добавления/обновления."""
+
+ client_id: str
+ email: str
+ tg_id: str
+ limit_ip: int
+ total_gb: int
+ expiry_time: int
+ enable: bool
+ flow: str
+ inbound_id: int
+ sub_id: str
+
+
+async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, Any]:
"""
- Adds a client to the server via 3x-ui.
+ Добавляет клиента на сервер через 3x-ui.
+
+ Args:
+ xui: Экземпляр API клиента
+ config: Конфигурация клиента
+
+ Returns:
+ Dict[str, Any]: Результат операции в формате
+ {'status': 'success'|'failed'|'duplicate', 'error': str, 'email': str}
"""
try:
await xui.login()
client = py3xui.Client(
- id=client_id,
- email=email.lower(),
- limit_ip=limit_ip,
- total_gb=total_gb,
- expiry_time=expiry_time,
- enable=enable,
- tg_id=tg_id,
- sub_id=sub_id,
- flow=flow,
+ id=config.client_id,
+ email=config.email.lower(),
+ limit_ip=config.limit_ip,
+ total_gb=config.total_gb,
+ expiry_time=config.expiry_time,
+ enable=config.enable,
+ tg_id=config.tg_id,
+ sub_id=config.sub_id,
+ flow=config.flow,
)
- response = await xui.client.add(inbound_id, [client])
-
- logger.info(f"Клиент {email} успешно добавлен с ID {client_id}.")
+ response = await xui.client.add(config.inbound_id, [client])
+ logger.info(f"Клиент {config.email} успешно добавлен с ID {config.client_id}")
return response if response else {"status": "failed"}
except Exception as e:
error_message = str(e)
-
if "Duplicate email" in error_message:
- logger.warning(
- f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}"
- )
- return {"status": "duplicate", "email": email}
+ logger.warning(f"Дублированный email: {config.email}. Пропуск. Сообщение: {error_message}")
+ return {"status": "duplicate", "email": config.email}
- logger.error(f"Ошибка при добавлении клиента {email}: {error_message}")
+ logger.error(f"Ошибка при добавлении клиента {config.email}: {error_message}")
return {"status": "failed", "error": error_message}
async def extend_client_key(
- xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id = str
-):
+ xui: py3xui.AsyncApi, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str
+) -> bool | None:
"""
- Функция для обновления срока действия ключа клиента по email.
+ Обновляет срок действия ключа клиента.
+
+ Args:
+ xui: Экземпляр API клиента
+ inbound_id: ID входящего соединения
+ email: Email клиента
+ new_expiry_time: Новое время истечения
+ client_id: ID клиента
+ total_gb: Общий объем трафика
+ sub_id: ID подписки
+
+ Returns:
+ Optional[bool]: True если успешно, False если ошибка, None если клиент не найден
"""
- await xui.login()
try:
+ await xui.login()
client = await xui.client.get_by_email(email)
if not client:
- logger.warning(f"Клиент с email {email} не найден.")
- return
+ logger.warning(f"Клиент с email {email} не найден")
+ return None
if not client.id:
- logger.warning(f"Ошибка: клиент {email} не имеет действительного ID.")
- return
+ logger.warning(f"Ошибка: клиент {email} не имеет действительного ID")
+ return None
- logger.info(
- f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}"
- )
+ logger.info(f"Обновление ключа клиента {email} с ID {client.id} до {new_expiry_time}")
client.id = client_id
client.expiry_time = new_expiry_time
@@ -87,41 +108,48 @@ async def extend_client_key(
await xui.client.update(client.id, client)
await xui.client.reset_stats(inbound_id, email)
- logger.info(
- f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}."
- )
+ logger.info(f"Ключ клиента {email} успешно продлён до {new_expiry_time}")
+ return True
except Exception as e:
logger.error(f"Ошибка при обновлении клиента с email {email}: {e}")
+ return False
async def delete_client(
- xui,
+ xui: py3xui.AsyncApi,
inbound_id: int,
email: str,
client_id: str,
) -> bool:
"""
- Функция для удаления клиента с сервера 3x-ui.
- Возвращает True при успешном удалении, иначе False.
+ Удаляет клиента с сервера 3x-ui.
+
+ Args:
+ xui: Экземпляр API клиента
+ inbound_id: ID входящего соединения
+ email: Email клиента
+ client_id: ID клиента
+
+ Returns:
+ bool: True если удаление успешно, False в противном случае
"""
- await xui.login()
try:
+ await xui.login()
+
if SUPERNODE:
await xui.client.delete(inbound_id, client_id)
- logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE).")
+ logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE)")
return True
client = await xui.client.get_by_email(email)
-
if not client:
- logger.warning(f"Клиент с email {email} и ID {client_id} не найден.")
+ logger.warning(f"Клиент с email {email} и ID {client_id} не найден")
return False
client.id = client_id
-
await xui.client.delete(inbound_id, client.id)
- logger.info(f"Клиент с ID {client_id} был удален успешно.")
+ logger.info(f"Клиент с ID {client_id} был удален успешно")
return True
except Exception as e:
diff --git a/database.py b/database.py
index cbd7cec2..ea1b5cab 100644
--- a/database.py
+++ b/database.py
@@ -3,12 +3,13 @@ from datetime import datetime
from typing import Any
import asyncpg
+import pytz
from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from logger import logger
-async def save_temporary_data(session, tg_id: int, state: str, data: dict):
+async def create_temporary_data(session, tg_id: int, state: str, data: dict):
"""Сохраняет временные данные пользователя."""
await session.execute(
"""
@@ -26,27 +27,39 @@ async def save_temporary_data(session, tg_id: int, state: str, data: dict):
async def get_temporary_data(session, tg_id: int) -> dict | None:
"""Извлекает временные данные пользователя."""
- result = await session.fetchrow(
- "SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id
- )
+ result = await session.fetchrow("SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id)
if result:
return {"state": result["state"], "data": json.loads(result["data"])}
return None
async def clear_temporary_data(session, tg_id: int):
+ # TODO rename delete_temporary_data
await session.execute("DELETE FROM temporary_data WHERE tg_id = $1", tg_id)
-async def add_blocked_user(tg_id: int, conn: asyncpg.Connection):
+async def create_blocked_user(tg_id: int, conn: asyncpg.Connection):
await conn.execute(
"INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING",
tg_id,
)
+async def delete_blocked_user(tg_id: int | list[int], conn: asyncpg.Connection):
+ """
+ Удаляет пользователя или список пользователей из списка заблокированных.
+
+ :param tg_id: ID пользователя Telegram или список ID
+ :param conn: Подключение к базе данных
+ """
+ if isinstance(tg_id, list):
+ await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", tg_id)
+ else:
+ await conn.execute("DELETE FROM blocked_users WHERE tg_id = $1", tg_id)
+
+
async def init_db(file_path: str = "assets/schema.sql"):
- with open(file_path, mode="r") as file:
+ with open(file_path) as file:
sql_content = file.read()
statements = [stmt.strip() for stmt in sql_content.split(";") if stmt.strip()]
@@ -62,27 +75,57 @@ async def init_db(file_path: str = "assets/schema.sql"):
await conn.close()
-async def check_unique_server_name(server_name: str) -> bool:
+async def check_unique_server_name(server_name: str, session: Any, cluster_name: str | None = None) -> bool:
"""
Проверяет уникальность имени сервера.
:param server_name: Имя сервера.
+ :param session: Сессия базы данных.
+ :param cluster_name: Имя кластера (опционально).
:return: True, если имя сервера уникально, False, если уже существует.
"""
- conn = await asyncpg.connect(DATABASE_URL)
-
- result = await conn.fetchrow(
- "SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name
- )
-
- await conn.close()
+ if cluster_name:
+ result = await session.fetchrow(
+ "SELECT 1 FROM servers WHERE server_name = $1 AND cluster_name = $2 LIMIT 1", server_name, cluster_name
+ )
+ else:
+ result = await session.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name)
return result is None
-async def create_coupon(
- coupon_code: str, amount: float, usage_limit: int, session: Any
-):
+async def check_server_name_by_cluster(server_name: str, session: Any) -> dict | None:
+ """
+ Проверяет принадлежность сервера к кластеру.
+
+ Args:
+ server_name (str): Имя сервера для проверки
+ session (Any): Сессия базы данных
+
+ Returns:
+ dict | None: Словарь с информацией о кластере или None, если сервер не найден
+ - cluster_name (str): Название кластера
+ """
+ try:
+ cluster_info = await session.fetchrow(
+ """
+ SELECT cluster_name
+ FROM servers
+ WHERE server_name = $1
+ """,
+ server_name,
+ )
+ if cluster_info:
+ logger.info(f"Найден кластер для сервера {server_name}")
+ return dict(cluster_info)
+ logger.info(f"Кластер для сервера {server_name} не найден")
+ return None
+ except Exception as e:
+ logger.error(f"Ошибка при поиске кластера для сервера {server_name}: {e}")
+ raise
+
+
+async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any):
"""
Создает новый купон в базе данных.
@@ -114,38 +157,85 @@ async def create_coupon(
raise
-async def get_all_coupons(session: Any):
+async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None:
"""
- Получает список всех купонов из базы данных.
+ Получает информацию о купоне по его коду.
+
+ Args:
+ coupon_code (str): Код купона для поиска
+ session (Any): Сессия базы данных
Returns:
- list: Список словарей с информацией о купонах, каждый словарь содержит:
- - code (str): Код купона
- - amount (int): Сумма купона
- - usage_limit (int): Максимальное количество использований
- - usage_count (int): Текущее количество использований купона
+ dict | None: Словарь с информацией о купоне или None, если купон не найден
+ - id (int): ID купона
+ - usage_limit (int): Лимит использований
+ - usage_count (int): Текущее количество использований
+ - is_used (bool): Флаг использования
+ - amount (float): Сумма купона
+
+ Raises:
+ Exception: В случае ошибки при выполнении запроса
+ """
+ try:
+ result = await session.fetchrow(
+ """
+ SELECT id, usage_limit, usage_count, is_used, amount
+ FROM coupons
+ WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
+ """,
+ coupon_code,
+ )
+ return dict(result) if result else None
+ except Exception as e:
+ logger.error(f"Ошибка при получении купона {coupon_code}: {e}")
+ raise
+
+
+async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10):
+ """
+ Получает список купонов из базы данных с пагинацией.
+
+ Args:
+ session (Any): Сессия базы данных для выполнения запроса
+ page (int): Номер страницы (по умолчанию 1)
+ per_page (int): Количество купонов на странице (по умолчанию 10)
+
+ Returns:
+ dict: Словарь с информацией о купонах и пагинации:
+ - coupons (list): Список словарей с информацией о купонах
+ - total (int): Общее количество купонов
+ - pages (int): Общее количество страниц
+ - current_page (int): Текущая страница
Raises:
Exception: В случае ошибки при получении данных из базы
"""
try:
+ offset = (page - 1) * per_page
coupons = await session.fetch(
"""
SELECT code, amount, usage_limit, usage_count
FROM coupons
- """
+ ORDER BY id
+ LIMIT $1 OFFSET $2
+ """,
+ per_page,
+ offset,
)
- logger.info(f"Успешно получено {len(coupons)} купонов из базы данных")
+ total_count = await session.fetchval("SELECT COUNT(*) FROM coupons")
+ total_pages = -(-total_count // per_page) # Округление вверх
- return coupons
+ logger.info(f"Успешно получено {len(coupons)} купонов из базы данных (страница {page})")
+
+ return {"coupons": coupons, "total": total_count, "pages": total_pages, "current_page": page}
except Exception as e:
logger.error(f"Критическая ошибка при получении списка купонов: {e}")
logger.exception("Трассировка стека ошибки получения купонов")
- return []
+ return {"coupons": [], "total": 0, "pages": 0, "current_page": page}
-async def delete_coupon_from_db(coupon_code: str, session: Any):
+async def delete_coupon(coupon_code: str, session: Any):
"""
Удаляет купон из базы данных по его коду.
@@ -160,7 +250,7 @@ async def delete_coupon_from_db(coupon_code: str, session: Any):
Exception: В случае ошибки при выполнении запроса к базе данных
Example:
- result = await delete_coupon_from_db('SALE50', session)
+ result = await delete_coupon('SALE50', session)
"""
try:
coupon_record = await session.fetchrow(
@@ -189,67 +279,38 @@ async def delete_coupon_from_db(coupon_code: str, session: Any):
return False
-async def restore_trial(tg_id: int, session: Any):
+async def update_trial(tg_id: int, status: int, session: Any):
"""
- Восстанавливает возможность использования триального периода для пользователя.
+ Устанавливает статус триального периода для пользователя.
Args:
tg_id (int): Telegram ID пользователя
+ status (int): Статус триального периода (0 - доступен, 1 - использован)
session (Any): Сессия базы данных
Returns:
- bool: True, если триал успешно восстановлен, False в случае ошибки
+ bool: True, если статус успешно установлен, False в случае ошибки
"""
try:
await session.execute(
"""
INSERT INTO connections (tg_id, trial)
- VALUES ($1, 0)
+ VALUES ($1, $2)
ON CONFLICT (tg_id)
- DO UPDATE SET trial = 0
+ DO UPDATE SET trial = $2
""",
tg_id,
+ status,
)
- logger.info(f"Триальный период успешно восстановлен для пользователя {tg_id}")
+ status_text = "восстановлен" if status == 0 else "использован"
+ logger.info(f"Триальный период успешно {status_text} для пользователя {tg_id}")
return True
except Exception as e:
- logger.error(
- f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при установке статуса триального периода для пользователя {tg_id}: {e}")
return False
-async def use_trial(tg_id: int, session: Any):
- """
- Отмечает использование триального периода для пользователя.
-
- Args:
- tg_id (int): Telegram ID пользователя
- session (Any): Сессия базы данных
-
- Returns:
- bool: True, если триал успешно использован, False в случае ошибки
- """
- try:
- await session.execute(
- """
- INSERT INTO connections (tg_id, trial)
- VALUES ($1, 1)
- ON CONFLICT (tg_id)
- DO UPDATE SET trial = 1
- """,
- tg_id,
- )
- logger.info(f"Триальный период успешно использован для пользователя {tg_id}")
- return True
- except Exception as e:
- logger.error(f"Ошибка при использовании триала для пользователя {tg_id}: {e}")
- return False
-
-
-async def add_connection(
- tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None
-):
+async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None):
"""
Добавляет новое подключение для пользователя в базу данных.
@@ -276,9 +337,7 @@ async def add_connection(
f"Успешно добавлено новое подключение для пользователя {tg_id} с балансом {balance} и статусом триала {trial}"
)
except Exception as e:
- logger.error(
- f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}"
- )
+ logger.error(f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}")
raise
@@ -352,15 +411,13 @@ async def store_key(
key,
server_id,
)
- logger.info(
- f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}"
- )
+ logger.info(f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}")
except Exception as e:
logger.error(f"Ошибка при сохранении ключа для пользователя {tg_id}: {e}")
raise
-async def get_keys(tg_id: int):
+async def get_keys(tg_id: int, session: Any):
"""
Получает список ключей для указанного пользователя.
@@ -373,10 +430,8 @@ async def get_keys(tg_id: int):
Raises:
Exception: В случае ошибки при подключении к базе данных или выполнении запроса
"""
- conn = None
try:
- conn = await asyncpg.connect(DATABASE_URL)
- records = await conn.fetch(
+ records = await session.fetch(
"""
SELECT client_id, email, created_at, key
FROM keys
@@ -389,17 +444,14 @@ async def get_keys(tg_id: int):
except Exception as e:
logger.error(f"Ошибка при получении ключей для пользователя {tg_id}: {e}")
raise
- finally:
- if conn:
- await conn.close()
-async def get_keys_by_server(tg_id: int, server_id: str):
+async def get_keys_by_server(tg_id: int | None, server_id: str, session: Any):
"""
- Получает список ключей для указанного пользователя на определенном сервере.
+ Получает список ключей на определенном сервере. Если tg_id=None, возвращает все ключи на сервере.
Args:
- tg_id (int): Telegram ID пользователя
+ tg_id (int | None): Telegram ID пользователя или None для всех пользователей
server_id (str): Идентификатор сервера
Returns:
@@ -408,61 +460,36 @@ async def get_keys_by_server(tg_id: int, server_id: str):
Raises:
Exception: В случае ошибки при подключении к базе данных или выполнении запроса
"""
- conn = None
try:
- conn = await asyncpg.connect(DATABASE_URL)
- records = await conn.fetch(
- """
- SELECT client_id, email, created_at, key
- FROM keys
- WHERE tg_id = $1 AND server_id = $2
- """,
- tg_id,
- server_id,
- )
- logger.info(
- f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}"
- )
+ if tg_id is not None:
+ records = await session.fetch(
+ """
+ SELECT *
+ FROM keys
+ WHERE tg_id = $1 AND server_id = $2
+ """,
+ tg_id,
+ server_id,
+ )
+ logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}")
+ else:
+ records = await session.fetch(
+ """
+ SELECT *
+ FROM keys
+ WHERE server_id = $1
+ """,
+ server_id,
+ )
+ logger.info(f"Успешно получено {len(records)} ключей на сервере {server_id}")
+
return records
except Exception as e:
- logger.error(
- f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}"
- )
+ error_msg = f"Ошибка при получении ключей на сервере {server_id}"
+ if tg_id is not None:
+ error_msg += f" для пользователя {tg_id}"
+ logger.error(f"{error_msg}: {e}")
raise
- finally:
- if conn:
- await conn.close()
-
-
-async def has_active_key(tg_id: int) -> bool:
- """
- Проверяет наличие активных ключей для указанного пользователя.
-
- Args:
- tg_id (int): Telegram ID пользователя
-
- Returns:
- bool: True, если у пользователя есть активные ключи, иначе False
-
- Raises:
- Exception: В случае ошибки при подключении к базе данных или выполнении запроса
- """
- conn = None
- try:
- conn = await asyncpg.connect(DATABASE_URL)
- count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id)
- logger.info(
- f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}"
- )
- return count > 0
- except Exception as e:
- logger.error(
- f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}"
- )
- raise
- finally:
- if conn:
- await conn.close()
async def get_balance(tg_id: int) -> float:
@@ -481,11 +508,9 @@ async def get_balance(tg_id: int) -> float:
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
- balance = await conn.fetchval(
- "SELECT balance FROM connections WHERE tg_id = $1", tg_id
- )
+ balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
logger.info(f"Получен баланс для пользователя {tg_id}: {balance}")
- return balance if balance is not None else 0.0
+ return round(balance, 1) if balance is not None else 0.0
except Exception as e:
logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}")
return 0.0
@@ -494,21 +519,25 @@ async def get_balance(tg_id: int) -> float:
await conn.close()
-async def update_balance(tg_id: int, amount: float):
+async def update_balance(tg_id: int, amount: float, session: Any = None):
"""
Обновляет баланс пользователя в базе данных.
Args:
tg_id (int): Telegram ID пользователя
amount (float): Сумма для обновления баланса
+ session (Any, optional): Сессия базы данных. Если не передана, создается новая.
Raises:
Exception: В случае ошибки при подключении к базе данных или обновлении баланса
"""
conn = None
try:
- conn = await asyncpg.connect(DATABASE_URL)
- await conn.execute(
+ if session is None:
+ conn = await asyncpg.connect(DATABASE_URL)
+ session = conn
+
+ await session.execute(
"""
UPDATE connections
SET balance = balance + $1
@@ -525,7 +554,7 @@ async def update_balance(tg_id: int, amount: float):
logger.error(f"Ошибка при обновлении баланса для пользователя {tg_id}: {e}")
raise
finally:
- if conn:
+ if conn is not None:
await conn.close()
@@ -541,15 +570,11 @@ async def get_trial(tg_id: int, session: Any) -> int:
int: Статус триала (0 - не использован, 1 - использован)
"""
try:
- trial = await session.fetchval(
- "SELECT trial FROM connections WHERE tg_id = $1", tg_id
- )
+ trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id)
logger.info(f"Получен статус триала для пользователя {tg_id}: {trial}")
return trial if trial is not None else 0
except Exception as e:
- logger.error(
- f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}")
return 0
@@ -573,44 +598,17 @@ async def get_key_count(tg_id: int) -> int:
logger.info(f"Получено количество ключей для пользователя {tg_id}: {count}")
return count if count is not None else 0
except Exception as e:
- logger.error(
- f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}")
return 0
finally:
if conn:
await conn.close()
-async def get_all_users(conn):
- """
- Получает список всех пользователей из базы данных.
-
- Args:
- conn: Подключение к базе данных
-
- Returns:
- list: Список Telegram ID всех пользователей
-
- Raises:
- Exception: В случае ошибки при получении данных
- """
- try:
- users = await conn.fetch("SELECT tg_id FROM connections")
- logger.info(f"Получен список всех пользователей. Количество: {len(users)}")
- return users
- except Exception as e:
- logger.error(f"Ошибка при получении списка пользователей: {e}")
- raise
-
-
async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any):
try:
-
if referred_tg_id == referrer_tg_id:
- logger.warning(
- f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку."
- )
+ logger.warning(f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку.")
return
await session.execute(
@@ -621,9 +619,7 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any):
referred_tg_id,
referrer_tg_id,
)
- logger.info(
- f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}"
- )
+ logger.info(f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}")
except Exception as e:
logger.error(f"Ошибка при добавлении реферала: {e}")
raise
@@ -656,9 +652,7 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
for level in range(1, MAX_REFERRAL_LEVELS + 1):
if current_tg_id in visited_tg_ids:
- logger.warning(
- f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение."
- )
+ logger.warning(f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение.")
break
visited_tg_ids.add(current_tg_id)
@@ -697,15 +691,11 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
bonus = round(amount * bonus_percent, 2)
if bonus > 0:
- logger.info(
- f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}."
- )
+ logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}.")
await update_balance(referrer_tg_id, bonus)
except Exception as e:
- logger.error(
- f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}")
finally:
if conn:
await conn.close()
@@ -804,9 +794,7 @@ async def get_referral_stats(referrer_tg_id: int):
referrer_tg_id,
)
- logger.debug(
- f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}"
- )
+ logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}")
return {
"total_referrals": total_referrals,
@@ -816,9 +804,7 @@ async def get_referral_stats(referrer_tg_id: int):
}
except Exception as e:
- logger.error(
- f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}"
- )
+ logger.error(f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}")
raise
finally:
if conn:
@@ -826,7 +812,7 @@ async def get_referral_stats(referrer_tg_id: int):
logger.info("Закрытие подключения к базе данных")
-async def update_key_expiry(client_id: str, new_expiry_time: int):
+async def update_key_expiry(client_id: str, new_expiry_time: int, session: Any):
"""
Обновление времени истечения ключа для указанного клиента.
@@ -837,14 +823,8 @@ async def update_key_expiry(client_id: str, new_expiry_time: int):
Raises:
Exception: В случае ошибки при подключении к базе данных или обновлении ключа
"""
- conn = None
try:
- conn = await asyncpg.connect(DATABASE_URL)
- logger.info(
- f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}"
- )
-
- await conn.execute(
+ await session.execute(
"""
UPDATE keys
SET expiry_time = $1, notified = FALSE, notified_24h = FALSE
@@ -856,49 +836,8 @@ async def update_key_expiry(client_id: str, new_expiry_time: int):
logger.info(f"Успешно обновлено время истечения ключа для клиента {client_id}")
except Exception as e:
- logger.error(
- f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}"
- )
+ logger.error(f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}")
raise
- finally:
- if conn:
- await conn.close()
- logger.info("Закрытие подключения к базе данных")
-
-
-async def delete_key(client_id: str):
- """
- Удаление ключа из базы данных для указанного клиента.
-
- Args:
- client_id (str): Уникальный идентификатор клиента, ключ которого будет удален
-
- Raises:
- Exception: В случае ошибки при подключении к базе данных или удалении ключа
- """
- conn = None
- try:
- conn = await asyncpg.connect(DATABASE_URL)
- logger.info(
- f"Установлено подключение к базе данных для удаления ключа клиента {client_id}"
- )
-
- await conn.execute(
- """
- DELETE FROM keys
- WHERE client_id = $1
- """,
- client_id,
- )
- logger.info(f"Успешно удален ключ для клиента {client_id}")
-
- except Exception as e:
- logger.error(f"Ошибка при удалении ключа для клиента {client_id}: {e}")
- raise
- finally:
- if conn:
- await conn.close()
- logger.info("Закрытие подключения к базе данных")
async def add_balance_to_client(client_id: str, amount: float):
@@ -915,9 +854,7 @@ async def add_balance_to_client(client_id: str, amount: float):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
- logger.info(
- f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}"
- )
+ logger.info(f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}")
await conn.execute(
"""
@@ -955,9 +892,7 @@ async def get_client_id_by_email(email: str):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
- logger.info(
- f"Установлено подключение к базе данных для поиска client_id по email: {email}"
- )
+ logger.info(f"Установлено подключение к базе данных для поиска client_id по email: {email}")
client_id = await conn.fetchval(
"""
@@ -998,13 +933,9 @@ async def get_tg_id_by_client_id(client_id: str):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
- logger.info(
- f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}"
- )
+ logger.info(f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}")
- result = await conn.fetchrow(
- "SELECT tg_id FROM keys WHERE client_id = $1", client_id
- )
+ result = await conn.fetchrow("SELECT tg_id FROM keys WHERE client_id = $1", client_id)
if result:
logger.info(f"Найден Telegram ID для client_id: {client_id}")
@@ -1047,9 +978,7 @@ async def upsert_user(
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
- logger.info(
- f"Установлено подключение к базе данных для обновления пользователя {tg_id}"
- )
+ logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}")
await conn.execute(
"""
@@ -1096,9 +1025,7 @@ async def add_payment(tg_id: int, amount: float, payment_system: str):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
- logger.info(
- f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}"
- )
+ logger.info(f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}")
await conn.execute(
"""
@@ -1109,9 +1036,7 @@ async def add_payment(tg_id: int, amount: float, payment_system: str):
amount,
payment_system,
)
- logger.info(
- f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}"
- )
+ logger.info(f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}")
except Exception as e:
logger.error(f"Ошибка при добавлении платежа для пользователя {tg_id}: {e}")
raise
@@ -1144,19 +1069,13 @@ async def add_notification(tg_id: int, notification_type: str, session: Any):
tg_id,
notification_type,
)
- logger.info(
- f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}"
- )
+ logger.info(f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}")
except Exception as e:
- logger.error(
- f"Ошибка при добавлении notification для пользователя {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при добавлении notification для пользователя {tg_id}: {e}")
raise
-async def check_notification_time(
- tg_id: int, notification_type: str, hours: int = 12, session: Any = None
-) -> bool:
+async def check_notification_time(tg_id: int, notification_type: str, hours: int = 12, session: Any = None) -> bool:
"""
Проверяет, прошло ли указанное количество часов с момента последнего уведомления.
@@ -1201,9 +1120,7 @@ async def check_notification_time(
return can_notify
except Exception as e:
- logger.error(
- f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}")
return False
finally:
@@ -1211,48 +1128,48 @@ async def check_notification_time(
await conn.close()
-async def get_servers_from_db():
- conn = await asyncpg.connect(DATABASE_URL)
+async def get_servers(session: Any = None):
+ conn = None
+ try:
+ conn = session if session is not None else await asyncpg.connect(DATABASE_URL)
- result = await conn.fetch(
- """
- SELECT cluster_name, server_name, api_url, subscription_url, inbound_id
- FROM servers
- """
- )
-
- await conn.close()
-
- servers = {}
- for row in result:
- cluster_name = row["cluster_name"]
- if cluster_name not in servers:
- servers[cluster_name] = []
-
- servers[cluster_name].append(
- {
- "server_name": row["server_name"],
- "api_url": row["api_url"],
- "subscription_url": row["subscription_url"],
- "inbound_id": row["inbound_id"],
- }
+ result = await conn.fetch(
+ """
+ SELECT cluster_name, server_name, api_url, subscription_url, inbound_id
+ FROM servers
+ """
)
+ servers = {}
+ for row in result:
+ cluster_name = row["cluster_name"]
+ if cluster_name not in servers:
+ servers[cluster_name] = []
- return servers
+ servers[cluster_name].append(
+ {
+ "server_name": row["server_name"],
+ "api_url": row["api_url"],
+ "subscription_url": row["subscription_url"],
+ "inbound_id": row["inbound_id"],
+ }
+ )
+
+ return servers
+
+ finally:
+ if conn is not None and session is None:
+ await conn.close()
async def delete_user_data(session: Any, tg_id: int):
-
try:
- await session.execute(
- "DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id
- )
+ await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id)
except Exception as e:
logger.warning(f"У Вас версия без подарков для {tg_id}: {e}")
await session.execute("DELETE FROM payments WHERE tg_id = $1", tg_id)
await session.execute("DELETE FROM users WHERE tg_id = $1", tg_id)
await session.execute("DELETE FROM connections WHERE tg_id = $1", tg_id)
- await session.execute("DELETE FROM keys WHERE tg_id = $1", tg_id)
+ await delete_key(tg_id, session)
await session.execute("DELETE FROM referrals WHERE referrer_tg_id = $1", tg_id)
@@ -1305,10 +1222,349 @@ async def store_gift_link(
logger.error(f"Не удалось добавить подарок с ID {gift_id} в базу данных.")
return False
except Exception as e:
-
logger.error(f"Ошибка при сохранении подарка с ID {gift_id} в базе данных: {e}")
return False
finally:
if conn is not None and session is None:
await conn.close()
+
+
+async def get_key_details(email, session):
+ record = await session.fetchrow(
+ """
+ SELECT k.key, k.email, k.expiry_time, k.server_id,k, k.client_id, k.created_at, c.tg_id, c.balance
+ FROM keys k
+ JOIN connections c ON k.tg_id = c.tg_id
+ WHERE k.email = $1
+ """,
+ email,
+ )
+
+ if not record:
+ return None
+
+ cluster_name = record["server_id"]
+
+ moscow_tz = pytz.timezone("Europe/Moscow")
+ expiry_date = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=moscow_tz)
+ current_date = datetime.now(moscow_tz)
+ time_left = expiry_date - current_date
+
+ if time_left.total_seconds() <= 0:
+ days_left_message = "Ключ истек."
+ elif time_left.days > 0:
+ days_left_message = f"Осталось дней: {time_left.days}"
+ else:
+ hours_left = time_left.seconds // 3600
+ days_left_message = f"Осталось часов: {hours_left}"
+
+ return {
+ "key": record["key"],
+ "created_at": record["created_at"],
+ "expiry_time": record["expiry_time"],
+ "client_id": record["client_id"],
+ "expiry_date": expiry_date.strftime("%d %B %Y года %H:%M"),
+ "days_left_message": days_left_message,
+ "server_name": cluster_name,
+ "balance": record["balance"],
+ "tg_id": record["tg_id"],
+ "email": record["email"],
+ }
+
+
+async def delete_key(identifier, session):
+ """
+ Удаляет ключ из базы данных по client_id или tg_id
+
+ Args:
+ identifier (str): client_id или tg_id для удаления
+ session: Сессия базы данных
+
+ Raises:
+ Exception: В случае ошибки при удалении ключа
+ """
+ try:
+ # Проверяем, является ли идентификатор числом (tg_id) или строкой (client_id)
+ if identifier.isdigit():
+ query = "DELETE FROM keys WHERE tg_id = $1"
+ else:
+ query = "DELETE FROM keys WHERE client_id = $1"
+
+ await session.execute(query, identifier)
+ logger.info(f"Ключ с идентификатором {identifier} успешно удалён")
+ except Exception as e:
+ logger.error(f"Ошибка при удалении ключа с идентификатором {identifier} из базы данных: {e}")
+
+
+async def create_server(
+ cluster_name: str, server_name: str, api_url: str, subscription_url: str, inbound_id: int, session: Any
+):
+ """
+ Добавляет новый сервер в базу данных.
+
+ Args:
+ cluster_name (str): Название кластера
+ server_name (str): Название сервера
+ api_url (str): URL API сервера
+ subscription_url (str): URL подписки
+ inbound_id (int): ID входящего подключения
+ session (Any): Сессия базы данных
+
+ Raises:
+ Exception: В случае ошибки при добавлении сервера
+ """
+ try:
+ await session.execute(
+ """
+ INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id)
+ VALUES ($1, $2, $3, $4, $5)
+ """,
+ cluster_name,
+ server_name,
+ api_url,
+ subscription_url,
+ inbound_id,
+ )
+ logger.info(f"Сервер {server_name} успешно добавлен в кластер {cluster_name}")
+ except Exception as e:
+ logger.error(f"Ошибка при добавлении сервера {server_name} в кластер {cluster_name}: {e}")
+ raise
+
+
+async def delete_server(server_name: str, session: Any):
+ """
+ Удаляет сервер из базы данных по его названию.
+
+ Args:
+ server_name (str): Название сервера для удаления
+ session (Any): Сессия базы данных
+
+ Raises:
+ Exception: В случае ошибки при удалении сервера
+ """
+ try:
+ await session.execute(
+ """
+ DELETE FROM servers WHERE server_name = $1
+ """,
+ server_name,
+ )
+ logger.info(f"Сервер {server_name} успешно удалён из базы данных")
+ except Exception as e:
+ logger.error(f"Ошибка при удалении сервера {server_name} из базы данных: {e}")
+ raise
+
+
+async def create_coupon_usage(coupon_id: int, user_id: int, session: Any):
+ """
+ Создаёт запись об использовании купона в базе данных.
+
+ Args:
+ coupon_id (int): ID купона
+ user_id (int): ID пользователя
+ session (Any): Сессия базы данных
+
+ Raises:
+ Exception: В случае ошибки при создании записи
+ """
+ try:
+ await session.execute(
+ """
+ INSERT INTO coupon_usages (coupon_id, user_id, used_at)
+ VALUES ($1, $2, $3)
+ """,
+ coupon_id,
+ user_id,
+ datetime.utcnow(),
+ )
+ logger.info(f"Создана запись об использовании купона {coupon_id} пользователем {user_id}")
+ except Exception as e:
+ logger.error(f"Ошибка при создании записи об использовании купона {coupon_id} пользователем {user_id}: {e}")
+ raise
+
+
+async def check_coupon_usage(coupon_id: int, user_id: int, session: Any) -> bool:
+ """
+ Проверяет, использовал ли пользователь данный купон.
+
+ Args:
+ coupon_id (int): ID купона для проверки
+ user_id (int): ID пользователя для проверки
+ session (Any): Сессия базы данных
+
+ Returns:
+ bool: True если купон уже использован, False если нет
+
+ Raises:
+ Exception: В случае ошибки при выполнении запроса
+ """
+ try:
+ result = await session.fetchrow(
+ """
+ SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2
+ """,
+ coupon_id,
+ user_id,
+ )
+ return result is not None
+ except Exception as e:
+ logger.error(f"Ошибка при проверке использования купона {coupon_id} пользователем {user_id}: {e}")
+ raise
+
+
+async def update_coupon_usage_count(coupon_id: int, session: Any):
+ """
+ Обновляет счетчик использования купона и его статус.
+
+ Args:
+ coupon_id (int): ID купона для обновления
+ session (Any): Сессия базы данных
+
+ Raises:
+ Exception: В случае ошибки при обновлении данных купона
+ """
+ try:
+ await session.execute(
+ """
+ UPDATE coupons
+ SET usage_count = usage_count + 1,
+ is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END
+ WHERE id = $1
+ """,
+ coupon_id,
+ )
+ logger.info(f"Успешно обновлен счетчик использования купона {coupon_id}")
+ except Exception as e:
+ logger.error(f"Ошибка при обновлении счетчика использования купона {coupon_id}: {e}")
+ raise
+
+
+async def get_last_payments(tg_id: int, session: Any):
+ """
+ Получает последние 3 платежа пользователя.
+
+ Args:
+ tg_id (int): Telegram ID пользователя
+ session (Any): Сессия базы данных
+
+ Returns:
+ list: Список последних платежей пользователя
+
+ Raises:
+ Exception: В случае ошибки при выполнении запроса
+ """
+ try:
+ records = await session.fetch(
+ """
+ SELECT amount, payment_system, status, created_at
+ FROM payments
+ WHERE tg_id = $1
+ ORDER BY created_at DESC
+ LIMIT 3
+ """,
+ tg_id,
+ )
+ logger.info(f"Успешно получены последние платежи для пользователя {tg_id}")
+ return records
+ except Exception as e:
+ logger.error(f"Ошибка при получении последних платежей для пользователя {tg_id}: {e}")
+ raise
+
+
+async def get_coupon_details(coupon_id: str, session: Any):
+ """
+ Получает детали купона по его ID.
+
+ Args:
+ coupon_id (str): ID купона
+ session (Any): Сессия базы данных
+
+ Returns:
+ dict: Словарь с деталями купона или None если купон не найден
+
+ Raises:
+ Exception: В случае ошибки при выполнении запроса
+ """
+ try:
+ record = await session.fetchrow(
+ """
+ SELECT id, code, discount, usage_count, usage_limit, is_used
+ FROM coupons
+ WHERE id = $1
+ """,
+ coupon_id,
+ )
+
+ if record:
+ logger.info(f"Успешно получены детали купона {coupon_id}")
+ return dict(record)
+
+ logger.warning(f"Купон {coupon_id} не найден")
+ return None
+
+ except Exception as e:
+ logger.error(f"Ошибка при получении деталей купона {coupon_id}: {e}")
+ raise
+
+
+async def get_referral_by_referred_id(referred_tg_id: int, session: Any):
+ """
+ Получает информацию о реферале по ID приглашенного пользователя.
+
+ Args:
+ referred_tg_id (int): ID приглашенного пользователя
+ session (Any): Сессия базы данных
+
+ Returns:
+ dict: Словарь с информацией о реферале или None если не найден
+
+ Raises:
+ Exception: В случае ошибки при выполнении запроса
+ """
+ try:
+ record = await session.fetchrow(
+ """
+ SELECT * FROM referrals
+ WHERE referred_tg_id = $1
+ """,
+ referred_tg_id,
+ )
+
+ if record:
+ logger.info(f"Успешно получена информация о реферале для пользователя {referred_tg_id}")
+ return dict(record)
+
+ logger.info(f"Реферал для пользователя {referred_tg_id} не найден")
+ return None
+
+ except Exception as e:
+ logger.error(f"Ошибка при получении информации о реферале для пользователя {referred_tg_id}: {e}")
+ raise
+
+
+async def get_all_keys(session: Any = None):
+ """
+ Получает все записи из таблицы keys.
+
+ Args:
+ session (Any, optional): Сессия базы данных. По умолчанию None.
+
+ Returns:
+ list: Список всех записей из таблицы keys
+
+ Raises:
+ Exception: В случае ошибки при выполнении запроса
+ """
+ conn = None
+ try:
+ conn = session if session is not None else await asyncpg.connect(DATABASE_URL)
+ keys = await conn.fetch("SELECT * FROM keys")
+ logger.info(f"Успешно получены все записи из таблицы keys. Количество: {len(keys)}")
+ return keys
+ except Exception as e:
+ logger.error(f"Ошибка при получении записей из таблицы keys: {e}")
+ raise
+ finally:
+ if conn is not None and session is None:
+ await conn.close()
diff --git a/filters/admin.py b/filters/admin.py
index 95328eaa..5a392217 100644
--- a/filters/admin.py
+++ b/filters/admin.py
@@ -10,6 +10,5 @@ class IsAdminFilter(BaseFilter):
admin_ids: int | list[int] = ADMIN_ID
if isinstance(admin_ids, list):
return message.from_user.id in admin_ids
- return message.from_user.id == admin_ids
except Exception:
return False
diff --git a/filters/private.py b/filters/private.py
new file mode 100644
index 00000000..6dea490e
--- /dev/null
+++ b/filters/private.py
@@ -0,0 +1,8 @@
+from aiogram.enums import ChatType
+from aiogram.filters import BaseFilter
+from aiogram.types import Chat, TelegramObject
+
+
+class IsPrivateFilter(BaseFilter):
+ async def __call__(self, event: TelegramObject, event_chat: Chat) -> bool:
+ return event_chat.type == ChatType.PRIVATE
diff --git a/handlers/__init__.py b/handlers/__init__.py
index f984092c..2ac2e777 100644
--- a/handlers/__init__.py
+++ b/handlers/__init__.py
@@ -3,6 +3,7 @@ __all__ = ("router",)
from aiogram import Router
from .admin import router as admin_router
+from .captcha import router as captcha_router
from .coupons import router as coupons_router
from .donate import router as donate_router
from .instructions import router as instructions_router
@@ -12,11 +13,13 @@ from .pay import router as pay_router
from .payments import router as payments_router
from .profile import router as profile_router
from .start import router as start_router
+from .user import router as user_router
router = Router(name="handlers_main_router")
router.include_routers(
start_router,
+ captcha_router,
profile_router,
pay_router,
donate_router,
@@ -26,4 +29,5 @@ router.include_routers(
keys_router,
instructions_router,
admin_router,
+ user_router,
)
diff --git a/handlers/admin/admin_backups.py b/handlers/admin/admin_backups.py
index 51eee005..e838b891 100644
--- a/handlers/admin/admin_backups.py
+++ b/handlers/admin/admin_backups.py
@@ -12,14 +12,11 @@ router = Router()
AdminPanelCallback.filter(F.action == "backups"),
IsAdminFilter(),
)
-async def handle_backups(
- callback_query: CallbackQuery
-):
+async def handle_backups(callback_query: CallbackQuery):
kb = build_admin_back_kb("management")
await callback_query.message.edit_text(
- text="💾 Инициализация резервного копирования базы данных...",
- reply_markup=kb
+ text="💾 Инициализация резервного копирования базы данных...", reply_markup=kb
)
exception = await backup_database()
@@ -29,7 +26,4 @@ async def handle_backups(
else:
text = "✅ Резервная копия успешно создана и отправлена администраторам."
- await callback_query.message.edit_text(
- text=text,
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text=text, reply_markup=kb)
diff --git a/handlers/admin/admin_bans.py b/handlers/admin/admin_bans.py
index a515a14e..833f3084 100644
--- a/handlers/admin/admin_bans.py
+++ b/handlers/admin/admin_bans.py
@@ -1,7 +1,7 @@
from typing import Any
from aiogram import F, Router
-from aiogram.types import CallbackQuery, BufferedInputFile
+from aiogram.types import BufferedInputFile, CallbackQuery
from database import delete_user_data
from filters.admin import IsAdminFilter
@@ -15,13 +15,8 @@ router = Router()
AdminPanelCallback.filter(F.action == "bans"),
IsAdminFilter(),
)
-async def handle_bans(
- callback_query: CallbackQuery
-):
- text = (
- "🚫 Заблокировавшие бота"
- "\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!"
- )
+async def handle_bans(callback_query: CallbackQuery):
+ text = "🚫 Заблокировавшие бота\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!"
await callback_query.message.edit_text(
text=text,
@@ -33,10 +28,7 @@ async def handle_bans(
AdminPanelCallback.filter(F.action == "bans_export"),
IsAdminFilter(),
)
-async def handle_bans_export(
- callback_query: CallbackQuery,
- session: Any
-):
+async def handle_bans_export(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("management")
try:
@@ -53,9 +45,7 @@ async def handle_bans_export(
csv_output.seek(0)
- document = BufferedInputFile(
- file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv"
- )
+ document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv")
await callback_query.message.answer_document(
document=document,
@@ -72,10 +62,7 @@ async def handle_bans_export(
AdminPanelCallback.filter(F.action == "bans_delete_banned"),
IsAdminFilter(),
)
-async def handle_bans_delete_banned(
- callback_query: CallbackQuery,
- session: Any
-):
+async def handle_bans_delete_banned(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("bans")
try:
@@ -92,9 +79,7 @@ async def handle_bans_delete_banned(
for tg_id in blocked_ids:
await delete_user_data(session, tg_id)
- await session.execute(
- "DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids
- )
+ await session.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids)
await callback_query.message.answer(
text=f"🗑️ Удалены данные о {len(blocked_ids)} пользователях и связанных записях.",
diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py
index d1a34644..52c10a4e 100644
--- a/handlers/admin/admin_coupons.py
+++ b/handlers/admin/admin_coupons.py
@@ -4,9 +4,9 @@ from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
-from database import delete_coupon_from_db, get_all_coupons, create_coupon
+from database import create_coupon, delete_coupon, get_all_coupons
from filters.admin import IsAdminFilter
-from keyboards.admin.coupons_kb import build_coupons_kb, build_coupons_list_kb, AdminCouponDeleteCallback
+from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from logger import logger
@@ -22,22 +22,16 @@ class AdminCouponsState(StatesGroup):
IsAdminFilter(),
)
async def handle_coupons(
- callback_query: types.CallbackQuery,
+ callback_query: types.CallbackQuery,
):
- await callback_query.message.edit_text(
- text="🛠 Меню управления купонами:",
- reply_markup=build_coupons_kb()
- )
+ await callback_query.message.edit_text(text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb())
@router.callback_query(
AdminPanelCallback.filter(F.action == "coupons_create"),
IsAdminFilter(),
)
-async def handle_coupons_create(
- callback_query: types.CallbackQuery,
- state: FSMContext
-):
+async def handle_coupons_create(callback_query: types.CallbackQuery, state: FSMContext):
text = (
"🎫 Введите данные для создания купона в формате:\n\n"
"📝 код 💰 сумма 🔢 лимит\n\n"
@@ -51,15 +45,8 @@ async def handle_coupons_create(
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
-@router.message(
- AdminCouponsState.waiting_for_coupon_data,
- IsAdminFilter()
-)
-async def handle_coupon_data_input(
- message: types.Message,
- state: FSMContext,
- session: Any
-):
+@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter())
+async def handle_coupon_data_input(message: types.Message, state: FSMContext, session: Any):
text = message.text.strip()
parts = text.split()
@@ -83,10 +70,7 @@ async def handle_coupon_data_input(
coupon_amount = float(parts[1])
usage_limit = int(parts[2])
except ValueError:
- text = (
- "⚠️ Проверьте правильность введенных данных!\n"
- "💱 Сумма должна быть числом, а лимит — целым числом."
- )
+ text = "⚠️ Проверьте правильность введенных данных!\n💱 Сумма должна быть числом, а лимит — целым числом."
await message.answer(
text=text,
@@ -103,10 +87,7 @@ async def handle_coupon_data_input(
f"🔢 Лимит использования: {usage_limit} раз"
)
- await message.answer(
- text=text,
- reply_markup=kb
- )
+ await message.answer(text=text, reply_markup=kb)
await state.clear()
except Exception as e:
@@ -117,12 +98,14 @@ async def handle_coupon_data_input(
AdminPanelCallback.filter(F.action == "coupons_list"),
IsAdminFilter(),
)
-async def handle_coupons_list(
- callback_query: types.CallbackQuery,
- session: Any
-):
+async def handle_coupons_list(callback_query: types.CallbackQuery, session: Any):
try:
- coupons = await get_all_coupons(session)
+ page = int(callback_query.data.split(":")[1]) if ":" in callback_query.data else 1
+ per_page = 10
+ result = await get_all_coupons(session, page, per_page)
+ coupons = result["coupons"]
+ total_pages = result["pages"]
+ current_page = result["current_page"]
if not coupons:
await callback_query.message.edit_text(
@@ -142,13 +125,11 @@ async def handle_coupons_list(
f"✅ Использовано: {coupon['usage_count']} раз\n\n"
)
- await callback_query.message.edit_text(
- text=coupon_list,
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text=coupon_list, reply_markup=kb)
except Exception as e:
logger.error(f"Ошибка при получении списка купонов: {e}")
+ await callback_query.message.answer("Произошла ошибка при получении списка купонов.")
@router.callback_query(
@@ -156,14 +137,12 @@ async def handle_coupons_list(
IsAdminFilter(),
)
async def handle_coupon_delete(
- callback_query: types.CallbackQuery,
- callback_data: AdminCouponDeleteCallback,
- session: Any
+ callback_query: types.CallbackQuery, callback_data: AdminCouponDeleteCallback, session: Any
):
coupon_code = callback_data.coupon_code
try:
- result = await delete_coupon_from_db(coupon_code, session)
+ result = await delete_coupon(coupon_code, session)
if result:
await handle_coupons_list(callback_query, session)
diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py
index a5594caa..f3d76f87 100644
--- a/handlers/admin/admin_panel.py
+++ b/handlers/admin/admin_panel.py
@@ -5,57 +5,33 @@ from aiogram.types import CallbackQuery
from bot import version
from filters.admin import IsAdminFilter
-from keyboards.admin.panel_kb import build_panel_kb, AdminPanelCallback, build_management_kb
+from keyboards.admin.panel_kb import AdminPanelCallback, build_management_kb, build_panel_kb
router = Router()
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "admin"),
- IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "admin"), IsAdminFilter())
async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext):
- text = (
- "🤖 Панель администратора"
- f"\n📌 Версия бота: {version}"
- )
+ text = f"🤖 Панель администратора\n📌 Версия бота: {version}"
await state.clear()
- await callback_query.message.edit_text(
- text=text,
- reply_markup=build_panel_kb()
- )
+ await callback_query.message.edit_text(text=text, reply_markup=build_panel_kb())
-@router.callback_query(
- F.data == "admin",
- IsAdminFilter()
-)
+@router.callback_query(F.data == "admin", IsAdminFilter())
async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext):
await handle_admin_message(callback_query.message, state)
-@router.message(
- Command("admin"),
- IsAdminFilter()
-)
+@router.message(Command("admin"), IsAdminFilter())
async def handle_admin_message(message: types.Message, state: FSMContext):
- text = (
- "🤖 Панель администратора"
- f"\n📌 Версия бота: {version}"
- )
+ text = f"🤖 Панель администратора\n📌 Версия бота: {version}"
await state.clear()
- await message.answer(
- text=text,
- reply_markup=build_panel_kb()
- )
+ await message.answer(text=text, reply_markup=build_panel_kb())
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "management"),
- IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "management"), IsAdminFilter())
async def handle_management(callback_query: CallbackQuery):
await callback_query.message.edit_text(
text="🤖 Управление ботом",
diff --git a/handlers/admin/admin_restart.py b/handlers/admin/admin_restart.py
index 157bf13b..d2501758 100644
--- a/handlers/admin/admin_restart.py
+++ b/handlers/admin/admin_restart.py
@@ -4,7 +4,7 @@ from aiogram import F, Router
from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
-from keyboards.admin.panel_kb import build_restart_kb, AdminPanelCallback, build_admin_back_kb
+from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb, build_restart_kb
router = Router()
@@ -33,17 +33,8 @@ async def handle_restart_confirm(callback_query: CallbackQuery):
capture_output=True,
text=True,
)
- await callback_query.message.edit_text(
- text="🔄 Бот успешно перезагружен!",
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text="🔄 Бот успешно перезагружен!", reply_markup=kb)
except subprocess.CalledProcessError:
- await callback_query.message.edit_text(
- text="🔄 Бот успешно перезагружен!",
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text="🔄 Бот успешно перезагружен!", reply_markup=kb)
except Exception as e:
- await callback_query.message.edit_text(
- text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}",
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", reply_markup=kb)
diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py
index e82598bd..4e8a92cf 100644
--- a/handlers/admin/admin_sender.py
+++ b/handlers/admin/admin_sender.py
@@ -8,7 +8,7 @@ from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
-from keyboards.admin.sender_kb import build_sender_kb, AdminSenderCallback
+from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb
from logger import logger
router = Router()
@@ -22,9 +22,7 @@ class AdminSender(StatesGroup):
AdminPanelCallback.filter(F.action == "sender"),
IsAdminFilter(),
)
-async def handle_sender(
- callback_query: CallbackQuery
-):
+async def handle_sender(callback_query: CallbackQuery):
await callback_query.message.edit_text(
text="✍️ Выберите группу пользователей для рассылки:",
reply_markup=build_sender_kb(),
@@ -35,11 +33,7 @@ async def handle_sender(
AdminSenderCallback.filter(),
IsAdminFilter(),
)
-async def handle_sender_callback(
- callback_query: CallbackQuery,
- callback_data: AdminSenderCallback,
- state: FSMContext
-):
+async def handle_sender_callback(callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext):
await callback_query.message.edit_text(
text="✍️ Введите текст сообщения для рассылки:",
reply_markup=build_admin_back_kb("sender"),
@@ -52,11 +46,7 @@ async def handle_sender_callback(
AdminSender.waiting_for_message,
IsAdminFilter(),
)
-async def handle_message_input(
- message: types.Message,
- state: FSMContext,
- session: Any
-):
+async def handle_message_input(message: types.Message, state: FSMContext, session: Any):
text_message = message.text
try:
@@ -64,20 +54,26 @@ async def handle_message_input(
send_to = state_data.get("type", "all")
if send_to == "subscribed":
- tg_ids = await session.fetch("""
+ tg_ids = await session.fetch(
+ """
SELECT DISTINCT c.tg_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.expiry_time > $1
- """, int(datetime.utcnow().timestamp() * 1000))
+ """,
+ int(datetime.utcnow().timestamp() * 1000),
+ )
elif send_to == "unsubscribed":
- tg_ids = await session.fetch("""
+ tg_ids = await session.fetch(
+ """
SELECT c.tg_id
FROM connections c
LEFT JOIN keys k ON c.tg_id = k.tg_id
GROUP BY c.tg_id
HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1
- """, int(datetime.utcnow().timestamp() * 1000))
+ """,
+ int(datetime.utcnow().timestamp() * 1000),
+ )
else:
tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections")
@@ -87,13 +83,10 @@ async def handle_message_input(
for record in tg_ids:
tg_id = record["tg_id"]
try:
- await message.bot.send_message(
- chat_id=tg_id,
- text=text_message
- )
+ await message.bot.send_message(chat_id=tg_id, text=text_message)
success_count += 1
- except Exception:
- pass
+ except Exception as e:
+ logger.error(e)
text = (
f"📤 Рассылка завершена!"
@@ -102,10 +95,7 @@ async def handle_message_input(
f"\n❌ Не доставлено: {total_users - success_count}"
)
- await message.answer(
- text=text,
- reply_markup=build_admin_back_kb("stats")
- )
+ await message.answer(text=text, reply_markup=build_admin_back_kb("stats"))
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py
index d190f1ee..9632b424 100644
--- a/handlers/admin/admin_servers.py
+++ b/handlers/admin/admin_servers.py
@@ -1,3 +1,5 @@
+from typing import Any
+
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
@@ -6,12 +8,16 @@ from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
-from database import check_unique_server_name, get_servers_from_db
+from database import check_unique_server_name, delete_server, get_servers
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
-from keyboards.admin.servers_kb import build_manage_server_kb, \
- build_delete_server_kb, \
- build_manage_cluster_kb, build_clusters_editor_kb, AdminServerEditorCallback
+from keyboards.admin.servers_kb import (
+ AdminServerEditorCallback,
+ build_clusters_editor_kb,
+ build_delete_server_kb,
+ build_manage_cluster_kb,
+ build_manage_server_kb,
+)
router = Router()
@@ -28,10 +34,8 @@ class AdminServersEditor(StatesGroup):
AdminPanelCallback.filter(F.action == "servers"),
IsAdminFilter(),
)
-async def handle_servers(
- callback_query: types.CallbackQuery
-):
- servers = await get_servers_from_db()
+async def handle_servers(callback_query: types.CallbackQuery):
+ servers = await get_servers()
text = (
"🔧 Управление кластерами\n\n"
@@ -51,36 +55,23 @@ async def handle_servers(
AdminPanelCallback.filter(F.action == "clusters_add"),
IsAdminFilter(),
)
-async def handle_clusters_add(
- callback_query: types.CallbackQuery,
- state: FSMContext
-):
+async def handle_clusters_add(callback_query: types.CallbackQuery, state: FSMContext):
text = (
"🔧 Введите имя нового кластера:\n\n"
"Имя кластера должно быть уникальным!\n"
"Пример: cluster1 или us_east_1"
)
- await callback_query.message.edit_text(
- text=text,
- reply_markup=build_admin_back_kb("servers")
- )
+ await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb("servers"))
await state.set_state(AdminServersEditor.waiting_for_cluster_name)
-@router.message(
- AdminServersEditor.waiting_for_cluster_name,
- IsAdminFilter()
-)
-async def handle_cluster_name_input(
- message: types.Message,
- state: FSMContext
-):
+@router.message(AdminServersEditor.waiting_for_cluster_name, IsAdminFilter())
+async def handle_cluster_name_input(message: types.Message, state: FSMContext):
if not message.text:
await message.answer(
- text="❌ Имя кластера не может быть пустым. Попробуйте снова.",
- reply_markup=build_admin_back_kb("servers")
+ text="❌ Имя кластера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers")
)
return
@@ -101,18 +92,11 @@ async def handle_cluster_name_input(
await state.set_state(AdminServersEditor.waiting_for_server_name)
-@router.message(
- AdminServersEditor.waiting_for_server_name,
- IsAdminFilter()
-)
-async def handle_server_name_input(
- message: types.Message,
- state: FSMContext
-):
+@router.message(AdminServersEditor.waiting_for_server_name, IsAdminFilter())
+async def handle_server_name_input(message: types.Message, state: FSMContext):
if not message.text:
await message.answer(
- text="❌ Имя сервера не может быть пустым. Попробуйте снова.",
- reply_markup=build_admin_back_kb("servers")
+ text="❌ Имя сервера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers")
)
return
@@ -121,7 +105,7 @@ async def handle_server_name_input(
if not await check_unique_server_name(server_name):
await message.answer(
text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.",
- reply_markup=build_admin_back_kb("servers")
+ reply_markup=build_admin_back_kb("servers"),
)
return
@@ -144,18 +128,12 @@ async def handle_server_name_input(
await state.set_state(AdminServersEditor.waiting_for_api_url)
-@router.message(
- AdminServersEditor.waiting_for_api_url,
- IsAdminFilter()
-)
-async def handle_api_url_input(
- message: types.Message,
- state: FSMContext
-):
+@router.message(AdminServersEditor.waiting_for_api_url, IsAdminFilter())
+async def handle_api_url_input(message: types.Message, state: FSMContext):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
text="❌ API URL должен начинаться с https://. Попробуйте снова.",
- reply_markup=build_admin_back_kb("servers")
+ reply_markup=build_admin_back_kb("servers"),
)
return
@@ -182,18 +160,12 @@ async def handle_api_url_input(
await state.set_state(AdminServersEditor.waiting_for_subscription_url)
-@router.message(
- AdminServersEditor.waiting_for_subscription_url,
- IsAdminFilter()
-)
-async def handle_subscription_url_input(
- message: types.Message,
- state: FSMContext
-):
+@router.message(AdminServersEditor.waiting_for_subscription_url, IsAdminFilter())
+async def handle_subscription_url_input(message: types.Message, state: FSMContext):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
text="❌ subscription_url должен начинаться с https://. Попробуйте снова.",
- reply_markup=build_admin_back_kb("servers")
+ reply_markup=build_admin_back_kb("servers"),
)
return
@@ -216,20 +188,14 @@ async def handle_subscription_url_input(
await state.set_state(AdminServersEditor.waiting_for_inbound_id)
-@router.message(
- AdminServersEditor.waiting_for_inbound_id,
- IsAdminFilter()
-)
-async def handle_inbound_id_input(
- message: types.Message,
- state: FSMContext
-):
+@router.message(AdminServersEditor.waiting_for_inbound_id, IsAdminFilter())
+async def handle_inbound_id_input(message: types.Message, state: FSMContext):
inbound_id = message.text.strip()
if not inbound_id.isdigit():
await message.answer(
text="❌ inbound_id должен быть числовым значением. Попробуйте снова.",
- reply_markup=build_admin_back_kb("servers")
+ reply_markup=build_admin_back_kb("servers"),
)
return
@@ -261,17 +227,13 @@ async def handle_inbound_id_input(
await state.clear()
-@router.callback_query(
- AdminServerEditorCallback.filter(F.action == "clusters_manage"),
- IsAdminFilter()
-)
+@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter())
async def handle_clusters_manage(
- callback_query: types.CallbackQuery,
- callback_data: AdminServerEditorCallback,
+ callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
- servers = await get_servers_from_db()
+ servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
await callback_query.message.edit_text(
@@ -280,23 +242,17 @@ async def handle_clusters_manage(
)
-@router.callback_query(
- AdminServerEditorCallback.filter(F.action == "servers_availability"),
- IsAdminFilter()
-)
+@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter())
async def handle_servers_availability(
- callback_query: types.CallbackQuery,
- callback_data: AdminServerEditorCallback
+ callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
- servers = await get_servers_from_db()
+ servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
if not cluster_servers:
- await callback_query.message.answer(
- text=f"Кластер '{cluster_name}' не содержит серверов."
- )
+ await callback_query.message.answer(text=f"Кластер '{cluster_name}' не содержит серверов.")
return
text = (
@@ -304,51 +260,32 @@ async def handle_servers_availability(
"Это может занять до 1 минуты, пожалуйста, подождите..."
)
- in_progress_message = await callback_query.message.answer(
- text=text
- )
+ in_progress_message = await callback_query.message.answer(text=text)
- text = (
- f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
- )
+ text = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
for server in cluster_servers:
- xui = AsyncApi(
- server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD
- )
+ xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD)
try:
await xui.login()
online_users = len(await xui.client.online())
- text += (
- f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
- )
+ text += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
except Exception as e:
text += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
- await in_progress_message.edit_text(
- text=text,
- reply_markup=build_admin_back_kb("servers")
- )
+ await in_progress_message.edit_text(text=text, reply_markup=build_admin_back_kb("servers"))
-@router.callback_query(
- AdminServerEditorCallback.filter(F.action == "servers_manage"),
- IsAdminFilter()
-)
-async def handle_servers_manage(
- callback_query: types.CallbackQuery,
- callback_data: AdminServerEditorCallback
-):
+@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_manage"), IsAdminFilter())
+async def handle_servers_manage(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback):
server_name = callback_data.data
- servers = await get_servers_from_db()
+ servers = await get_servers()
cluster_name, server = next(
- ((c, s) for c, cs in servers.items()
- for s in cs if s["server_name"] == server_name),
- (None, None)
+ ((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None)
)
if server:
@@ -368,19 +305,11 @@ async def handle_servers_manage(
reply_markup=build_manage_server_kb(server_name, cluster_name),
)
else:
- await callback_query.message.edit_text(
- text="❌ Сервер не найден."
- )
+ await callback_query.message.edit_text(text="❌ Сервер не найден.")
-@router.callback_query(
- AdminServerEditorCallback.filter(F.action == "servers_delete"),
- IsAdminFilter()
-)
-async def handle_servers_delete(
- callback_query: types.CallbackQuery,
- callback_data: AdminServerEditorCallback
-):
+@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete"), IsAdminFilter())
+async def handle_servers_delete(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback):
server_name = callback_data.data
await callback_query.message.edit_text(
@@ -389,39 +318,22 @@ async def handle_servers_delete(
)
-@router.callback_query(
- AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"),
- IsAdminFilter()
-)
+@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter())
async def handle_servers_delete_confirm(
- callback_query: types.CallbackQuery,
- callback_data: AdminServerEditorCallback
+ callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
server_name = callback_data.data
- conn = await asyncpg.connect(DATABASE_URL)
- await conn.execute(
- """
- DELETE FROM servers WHERE server_name = $1
- """,
- server_name,
- )
- await conn.close()
+ await delete_server(server_name, session)
await callback_query.message.edit_text(
- text=f"🗑️ Сервер {server_name} успешно удален.",
- reply_markup=build_admin_back_kb("servers")
+ text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("servers")
)
-@router.callback_query(
- AdminServerEditorCallback.filter(F.action == "servers_add"),
- IsAdminFilter()
-)
+@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter())
async def handle_servers_add(
- callback_query: types.CallbackQuery,
- callback_data: AdminServerEditorCallback,
- state: FSMContext
+ callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext
):
cluster_name = callback_data.data
@@ -441,17 +353,13 @@ async def handle_servers_add(
await state.set_state(AdminServersEditor.waiting_for_server_name)
-@router.callback_query(
- AdminServerEditorCallback.filter(F.action == "clusters_backup"),
- IsAdminFilter()
-)
+@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter())
async def handle_clusters_backup(
- callback_query: types.CallbackQuery,
- callback_data: AdminServerEditorCallback,
+ callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
- servers = await get_servers_from_db()
+ servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
for server in cluster_servers:
diff --git a/handlers/admin/admin_stats.py b/handlers/admin/admin_stats.py
index 0a74d69f..48ad79bb 100644
--- a/handlers/admin/admin_stats.py
+++ b/handlers/admin/admin_stats.py
@@ -17,10 +17,7 @@ router = Router()
AdminPanelCallback.filter(F.action == "stats"),
IsAdminFilter(),
)
-async def handle_stats(
- callback_query: CallbackQuery,
- session: Any
-):
+async def handle_stats(callback_query: CallbackQuery, session: Any):
try:
total_users = await session.fetchval("SELECT COUNT(*) FROM users")
total_keys = await session.fetchval("SELECT COUNT(*) FROM keys")
@@ -35,13 +32,9 @@ async def handle_stats(
total_payments_month = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
- total_payments_all_time = await session.fetchval(
- "SELECT COALESCE(SUM(amount), 0) FROM payments"
- )
+ total_payments_all_time = await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments")
- registrations_today = await session.fetchval(
- "SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE"
- )
+ registrations_today = await session.fetchval("SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE")
registrations_week = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('week', CURRENT_DATE)"
)
@@ -49,9 +42,7 @@ async def handle_stats(
"SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
- users_updated_today = await session.fetchval(
- "SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE"
- )
+ users_updated_today = await session.fetchval("SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE")
active_keys = await session.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
@@ -81,10 +72,7 @@ async def handle_stats(
f" 🏦 За все время: {total_payments_all_time} ₽\n"
)
- await callback_query.message.edit_text(
- text=stats_message,
- reply_markup=build_stats_kb()
- )
+ await callback_query.message.edit_text(text=stats_message, reply_markup=build_stats_kb())
except Exception as e:
logger.error(f"Error in user_stats_menu: {e}")
@@ -93,46 +81,28 @@ async def handle_stats(
AdminPanelCallback.filter(F.action == "stats_export_users_csv"),
IsAdminFilter(),
)
-async def handle_export_users_csv(
- callback_query: CallbackQuery,
- session: Any
-):
+async def handle_export_users_csv(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("stats")
try:
export = await export_users_csv(session)
- await callback_query.message.answer_document(
- document=export,
- caption="📥 Экспорт пользователей в CSV"
- )
+ await callback_query.message.answer_document(document=export, caption="📥 Экспорт пользователей в CSV")
except Exception as e:
logger.error(f"Ошибка при экспорте пользователей в CSV: {e}")
- await callback_query.message.edit_text(
- text=f"❗ Произошла ошибка при экспорте: {e}",
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text=f"❗ Произошла ошибка при экспорте: {e}", reply_markup=kb)
@router.callback_query(
AdminPanelCallback.filter(F.action == "stats_export_payments_csv"),
IsAdminFilter(),
)
-async def handle_export_payments_csv(
- callback_query: CallbackQuery,
- session: Any
-):
+async def handle_export_payments_csv(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("stats")
try:
export = await export_payments_csv(session)
- await callback_query.message.answer_document(
- document=export,
- caption="📥 Экспорт платежей в CSV"
- )
+ await callback_query.message.answer_document(document=export, caption="📥 Экспорт платежей в CSV")
except Exception as e:
logger.error(f"Ошибка при экспорте платежей в CSV: {e}")
- await callback_query.message.edit_text(
- text=f"❗ Произошла ошибка при экспорте: {e}",
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text=f"❗ Произошла ошибка при экспорте: {e}", reply_markup=kb)
diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py
new file mode 100644
index 00000000..4d4ff573
--- /dev/null
+++ b/handlers/admin/admin_user_editor.py
@@ -0,0 +1,592 @@
+import asyncio
+from datetime import datetime
+from typing import Any
+
+import pytz
+from aiogram import Bot, F, Router, types
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from config import TOTAL_GB
+from database import (
+ delete_key,
+ delete_user_data,
+ get_client_id_by_email,
+ get_key_details,
+ get_keys,
+ get_servers,
+ update_key_expiry,
+ update_trial,
+)
+from filters.admin import IsAdminFilter
+from handlers.keys.key_utils import (
+ delete_key_from_cluster,
+ renew_key_in_cluster,
+)
+from handlers.utils import sanitize_key_name
+from logger import logger
+
+router = Router()
+
+
+class UserEditorState(StatesGroup):
+ waiting_for_tg_id = State()
+ waiting_for_username = State()
+ displaying_user_info = State()
+ waiting_for_new_balance = State()
+ waiting_for_key_name = State()
+ waiting_for_expiry_time = State()
+ waiting_for_message_text = State()
+
+
+@router.callback_query(F.data == "search_by_tg_id", IsAdminFilter())
+async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup())
+ await state.set_state(UserEditorState.waiting_for_tg_id)
+
+
+@router.callback_query(F.data == "search_by_username", IsAdminFilter())
+async def prompt_username(callback_query: CallbackQuery, state: FSMContext):
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await callback_query.message.answer("🔍 Введите Username клиента:", reply_markup=builder.as_markup())
+ await state.set_state(UserEditorState.waiting_for_username)
+
+
+@router.message(UserEditorState.waiting_for_username, IsAdminFilter())
+async def handle_username_input(message: types.Message, state: FSMContext, session: Any):
+ username = message.text.strip().lstrip("@").replace("https://t.me/", "")
+ user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username)
+
+ if not user_record:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer(
+ "🔍 Пользователь с указанным username не найден. 🚫",
+ reply_markup=builder.as_markup(),
+ )
+ await state.clear()
+ return
+
+ tg_id = user_record["tg_id"]
+ username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
+ balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
+ key_records = await get_keys(tg_id, session)
+ referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
+
+ if balance is None:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer(
+ "🚫 Пользователь с указанным tg_id не найден. 🔍",
+ reply_markup=builder.as_markup(),
+ )
+ await state.clear()
+ return
+
+ builder = InlineKeyboardBuilder()
+
+ for (email,) in key_records:
+ builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
+
+ builder.row(
+ InlineKeyboardButton(
+ text="📝 Изменить баланс",
+ callback_data=f"change_balance_{tg_id}",
+ )
+ )
+
+ builder.row(
+ InlineKeyboardButton(
+ text="🔄 Восстановить пробник",
+ callback_data=f"restore_trial_{tg_id}",
+ )
+ )
+ builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
+ builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+
+ user_info = (
+ f"📊 Информация о пользователе:\n\n"
+ f"🆔 ID пользователя: {tg_id}\n"
+ f"👤 Логин пользователя: @{username}\n"
+ f"💰 Баланс: {balance}\n"
+ f"👥 Количество рефералов: {referral_count}\n"
+ f"🔑 Ключи (для редактирования нажмите на ключ):"
+ )
+ await message.answer(user_info, reply_markup=builder.as_markup())
+ await state.set_state(UserEditorState.displaying_user_info)
+
+
+@router.callback_query(F.data.startswith("send_message_"))
+async def handle_send_message(callback_query: types.CallbackQuery, state: FSMContext):
+ tg_id = callback_query.data.split("_")[2]
+ await state.update_data(target_tg_id=tg_id)
+ await callback_query.message.answer("✉️ Введите текст сообщения, которое вы хотите отправить пользователю.")
+ await state.set_state(UserEditorState.waiting_for_message_text)
+
+
+@router.message(UserEditorState.waiting_for_message_text, IsAdminFilter())
+async def process_send_message(message: types.Message, state: FSMContext, bot: Bot):
+ data = await state.get_data()
+ target_tg_id = data.get("target_tg_id")
+
+ if not target_tg_id:
+ await message.answer("🚫 Ошибка: ID пользователя не найден.")
+ await state.clear()
+ return
+
+ try:
+ await bot.send_message(chat_id=target_tg_id, text=message.text)
+ await message.answer("✅ Сообщение успешно отправлено.")
+ except Exception as e:
+ await message.answer(f"❌ Не удалось отправить сообщение: {e}")
+
+ await state.clear()
+
+
+@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter())
+async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any):
+ tg_id = int(message.text)
+ username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
+ balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
+ key_records = await get_keys(tg_id, session)
+ referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
+
+ if balance is None:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer(
+ "❌ Пользователь с указанным tg_id не найден. 🔍",
+ reply_markup=builder.as_markup(),
+ )
+ await state.clear()
+ return
+
+ builder = InlineKeyboardBuilder()
+
+ for (email,) in key_records:
+ builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
+
+ builder.row(
+ InlineKeyboardButton(
+ text="📝 Изменить баланс",
+ callback_data=f"change_balance_{tg_id}",
+ )
+ )
+ builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
+ builder.row(
+ InlineKeyboardButton(
+ text="🔄 Восстановить пробник",
+ callback_data=f"restore_trial_{tg_id}",
+ )
+ )
+ builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"))
+
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+
+ user_info = (
+ f"📊 Информация о пользователе:\n\n"
+ f"🆔 ID пользователя: {tg_id}\n"
+ f"👤 Логин пользователя: @{username}\n"
+ f"💰 Баланс: {balance}\n"
+ f"👥 Количество рефералов: {referral_count}\n"
+ f"🔑 Ключи (для редактирования нажмите на ключ):"
+ )
+ await message.answer(user_info, reply_markup=builder.as_markup())
+ await state.set_state(UserEditorState.displaying_user_info)
+
+
+@router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter())
+async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any):
+ tg_id = int(callback_query.data.split("_")[2])
+
+ await update_trial(tg_id, 0, session)
+
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад в меню администратора", callback_data="admin"))
+
+ await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup())
+
+
+@router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter())
+async def process_balance_change(callback_query: CallbackQuery, state: FSMContext):
+ tg_id = int(callback_query.data.split("_")[2])
+ await state.update_data(tg_id=tg_id)
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await callback_query.message.answer("💸 Введите новую сумму баланса:", reply_markup=builder.as_markup())
+ await state.set_state(UserEditorState.waiting_for_new_balance)
+
+
+@router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter())
+async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any):
+ if not message.text.isdigit() or int(message.text) < 0:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer(
+ "❌ Пожалуйста, введите корректную сумму для изменения баланса.",
+ reply_markup=builder.as_markup(),
+ )
+ return
+
+ new_balance = int(message.text)
+ user_data = await state.get_data()
+ tg_id = user_data.get("tg_id")
+
+ await session.execute(
+ "UPDATE connections SET balance = $1 WHERE tg_id = $2",
+ new_balance,
+ tg_id,
+ )
+
+ response_message = f"✅ Баланс успешно изменен на {new_balance}."
+
+ builder = InlineKeyboardBuilder()
+ builder.row(
+ InlineKeyboardButton(
+ text="⬅️ Назад в меню администратора",
+ callback_data="admin",
+ )
+ )
+ await message.answer(response_message, reply_markup=builder.as_markup())
+ await state.clear()
+
+
+@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter())
+async def process_key_edit(callback_query: CallbackQuery, session: Any):
+ email = callback_query.data.split("_", 2)[2]
+ key_details = await get_key_details(email, session)
+
+ if not key_details:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await callback_query.message.answer(
+ "🔍 Информация о ключе не найдена. 🚫",
+ reply_markup=builder.as_markup(),
+ )
+ return
+
+ response_message = (
+ f"🔑 Ключ: {key_details['key']}\n"
+ f"⏰ Дата истечения: {key_details['expiry_date']}\n"
+ f"💰 Баланс пользователя: {key_details['balance']}\n"
+ f"🌐 Кластер: {key_details['server_name']}"
+ )
+
+ builder = InlineKeyboardBuilder()
+ builder.row(
+ InlineKeyboardButton(
+ text="ℹ️ Получить информацию о юзере",
+ callback_data=f"user_info|{key_details['tg_id']}",
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text="⏳ Изменить время истечения",
+ callback_data=f"change_expiry|{email}",
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text="❌ Удалить ключ",
+ callback_data=f"delete_key_admin|{email}",
+ )
+ )
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+
+ await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
+
+
+@router.callback_query(F.data == "search_by_key_name", IsAdminFilter())
+async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await callback_query.message.answer("🔑 Введите имя ключа:", reply_markup=builder.as_markup())
+ await state.set_state(UserEditorState.waiting_for_key_name)
+
+
+@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
+async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any):
+ key_name = sanitize_key_name(message.text)
+ key_details = await get_key_details(key_name, session)
+
+ if not key_details:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer(
+ "🚫 Пользователь с указанным именем ключа не найден.",
+ reply_markup=builder.as_markup(),
+ )
+ await state.clear()
+ return
+
+ response_message = (
+ f"🔑 Ключ: {key_details['key']}\n"
+ f"⏰ Дата истечения: {key_details['expiry_date']}\n"
+ f"💰 Баланс пользователя: {key_details['balance']}\n"
+ f"🌐 Сервер: {key_details['server_name']}"
+ )
+
+ key_buttons = InlineKeyboardBuilder()
+ key_buttons.row(
+ InlineKeyboardButton(
+ text="ℹ️ Получить информацию о юзере",
+ callback_data=f"user_info|{key_details['tg_id']}",
+ )
+ )
+ key_buttons.row(
+ InlineKeyboardButton(
+ text="⏳ Изменить время истечения",
+ callback_data=f"change_expiry|{key_name}",
+ )
+ )
+ key_buttons.row(
+ InlineKeyboardButton(
+ text="❌ Удалить ключ",
+ callback_data=f"delete_key_admin|{key_name}",
+ )
+ )
+ key_buttons.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+
+ await message.answer(response_message, reply_markup=key_buttons.as_markup())
+ await state.clear()
+
+
+@router.callback_query(F.data.startswith("change_expiry|"), IsAdminFilter())
+async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext):
+ email = callback_query.data.split("|")[1]
+ await callback_query.message.answer(
+ f"⏳ Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:"
+ )
+ await state.update_data(email=email)
+ await state.set_state(UserEditorState.waiting_for_expiry_time)
+
+
+@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
+async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any):
+ user_data = await state.get_data()
+ email = user_data.get("email")
+
+ if not email:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup())
+ await state.clear()
+ return
+
+ try:
+ expiry_time_str = message.text
+ moscow_tz = pytz.timezone("Europe/Moscow")
+ expiry_time = int(
+ datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=moscow_tz).timestamp() * 1000
+ )
+
+ client_id = await get_client_id_by_email(email)
+ if client_id is None:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer(
+ f"🚫 Клиент с email {email} не найден. 🔍",
+ reply_markup=builder.as_markup(),
+ )
+ await state.clear()
+ return
+
+ record = await get_key_details(email, session)
+ if not record:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer(
+ "🚫 Клиент не найден в базе данных. 🔍",
+ reply_markup=builder.as_markup(),
+ )
+ await state.clear()
+ return
+
+ clusters = await get_servers(session)
+
+ async def update_key_on_all_servers():
+ tasks = []
+ for cluster_name, cluster_servers in clusters.items():
+ for _server in cluster_servers:
+ tasks.append(
+ asyncio.create_task(
+ renew_key_in_cluster(
+ cluster_name,
+ email,
+ client_id,
+ expiry_time,
+ total_gb=TOTAL_GB,
+ )
+ )
+ )
+ await asyncio.gather(*tasks)
+
+ await update_key_on_all_servers()
+
+ await update_key_expiry(client_id, expiry_time, session)
+
+ response_message = (
+ f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
+ )
+
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin"))
+ await message.answer(response_message, reply_markup=builder.as_markup())
+ except ValueError:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await message.answer(
+ "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.",
+ reply_markup=builder.as_markup(),
+ )
+ except Exception as e:
+ logger.error(e)
+ await state.clear()
+
+
+@router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter())
+async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any):
+ email = callback_query.data.split("|")[1]
+ key_details = await get_key_details(email, session)
+
+ if key_details is None:
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+ await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup())
+ return
+
+ builder = InlineKeyboardBuilder()
+ builder.row(
+ types.InlineKeyboardButton(
+ text="✅ Да, удалить",
+ callback_data=f"confirm_delete_admin|{key_details['client_id']}",
+ )
+ )
+ builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor"))
+ await callback_query.message.answer(
+ "❓ Вы уверены, что хотите удалить ключ?",
+ reply_markup=builder.as_markup(),
+ )
+
+
+@router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter())
+async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any):
+ client_id = callback_query.data.split("|")[1]
+ record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id)
+
+ if record:
+ email = record["email"]
+ response_message = "✅ Ключ успешно удален."
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
+
+ clusters = await get_servers(session)
+
+ async def delete_key_from_servers(email, client_id):
+ tasks = []
+ for cluster_name, cluster_servers in clusters.items():
+ for _server in cluster_servers:
+ tasks.append(delete_key_from_cluster(cluster_name, email, client_id))
+ await asyncio.gather(*tasks)
+
+ await delete_key_from_servers(email, client_id)
+ await delete_key(client_id, session)
+
+ await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
+ else:
+ response_message = "🚫 Ключ не найден или уже удален."
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
+ await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
+
+
+@router.callback_query(F.data.startswith("user_info|"), IsAdminFilter())
+async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
+ tg_id = int(callback_query.data.split("|")[1])
+ username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
+ balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
+ key_records = await get_keys(tg_id, session)
+ referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
+
+ builder = InlineKeyboardBuilder()
+
+ for (email,) in key_records:
+ builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
+
+ builder.row(InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
+ builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
+
+ user_info = (
+ f"📊 Информация о пользователе:\n\n"
+ f"🆔 ID пользователя: {tg_id}\n"
+ f"👤 Логин пользователя: @{username}\n"
+ f"💰 Баланс: {balance}\n"
+ f"👥 Количество рефералов: {referral_count}\n"
+ f"🔑 Ключи (для редактирования нажмите на ключ):"
+ )
+
+ await callback_query.message.answer(user_info, reply_markup=builder.as_markup())
+ await state.set_state(UserEditorState.displaying_user_info)
+
+
+@router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter())
+async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
+ tg_id = int(callback_query.data.split("_")[3])
+
+ confirmation_markup = InlineKeyboardMarkup(
+ row_width=2,
+ inline_keyboard=[
+ [InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}")],
+ [InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")],
+ ],
+ )
+
+ await callback_query.message.answer(
+ f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
+ reply_markup=confirmation_markup,
+ )
+
+
+@router.callback_query(F.data.startswith("delete_user_"), IsAdminFilter())
+async def delete_user(callback_query: types.CallbackQuery, session: Any):
+ tg_id = int(callback_query.data.split("_")[2])
+
+ key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id)
+
+ async def delete_keys_from_servers():
+ try:
+ tasks = []
+ for email, client_id in key_records:
+ servers = await get_servers(session)
+ for cluster_id, _cluster in servers.items():
+ tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
+ await asyncio.gather(*tasks)
+ except Exception as e:
+ logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
+
+ await delete_keys_from_servers()
+
+ try:
+ await delete_user_data(session, tg_id)
+
+ back_button = InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
+
+ await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard)
+ except Exception as e:
+ logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
+ await callback_query.message.answer(
+ f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
+ )
diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py
index 2520299a..8ad42bea 100644
--- a/handlers/admin/admin_users.py
+++ b/handlers/admin/admin_users.py
@@ -10,19 +10,29 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery
from config import TOTAL_GB
-from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry
+from database import delete_key, delete_user_data, get_client_id_by_email, get_servers, update_key_expiry, update_trial
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
delete_key_from_cluster,
- delete_key_from_db,
- renew_key_in_cluster, update_subscription,
+ renew_key_in_cluster,
+ update_subscription,
)
from handlers.utils import sanitize_key_name
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
-from keyboards.admin.users_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \
- build_user_delete_kb, AdminUserEditorCallback, build_editor_kb, build_users_balance_kb, \
- build_users_balance_change_kb, build_user_key_kb, build_users_key_expiry_kb, AdminUserKeyEditorCallback, \
- build_users_key_show_kb
+from keyboards.admin.users_kb import (
+ AdminUserEditorCallback,
+ AdminUserKeyEditorCallback,
+ build_editor_kb,
+ build_key_delete_kb,
+ build_key_edit_kb,
+ build_user_delete_kb,
+ build_user_edit_kb,
+ build_user_key_kb,
+ build_users_balance_change_kb,
+ build_users_balance_kb,
+ build_users_key_expiry_kb,
+ build_users_key_show_kb,
+)
from logger import logger
router = Router()
@@ -42,10 +52,7 @@ class UserEditorState(StatesGroup):
AdminPanelCallback.filter(F.action == "search_user"),
IsAdminFilter(),
)
-async def handle_search_user(
- callback_query: CallbackQuery,
- state: FSMContext
-):
+async def handle_search_user(callback_query: CallbackQuery, state: FSMContext):
text = (
"🔍 Поиск пользователя"
"\n\n📌 Введите ID, Username или перешлите сообщение пользователя."
@@ -55,36 +62,20 @@ async def handle_search_user(
)
await state.set_state(UserEditorState.waiting_for_user_data)
- await callback_query.message.edit_text(
- text=text,
- reply_markup=build_admin_back_kb()
- )
+ await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb())
@router.callback_query(
AdminPanelCallback.filter(F.action == "search_key"),
IsAdminFilter(),
)
-async def handle_search_key(
- callback_query: CallbackQuery,
- state: FSMContext
-):
+async def handle_search_key(callback_query: CallbackQuery, state: FSMContext):
await state.set_state(UserEditorState.waiting_for_key_name)
- await callback_query.message.edit_text(
- text="🔑 Введите имя ключа для поиска:",
- reply_markup=build_admin_back_kb()
- )
+ await callback_query.message.edit_text(text="🔑 Введите имя ключа для поиска:", reply_markup=build_admin_back_kb())
-@router.message(
- UserEditorState.waiting_for_user_data,
- IsAdminFilter()
-)
-async def handle_user_data_input(
- message: types.Message,
- state: FSMContext,
- session: Any
-):
+@router.message(UserEditorState.waiting_for_user_data, IsAdminFilter())
+async def handle_user_data_input(message: types.Message, state: FSMContext, session: Any):
kb = build_admin_back_kb()
if message.forward_from:
@@ -93,23 +84,18 @@ async def handle_user_data_input(
return
if not message.text:
- await message.answer(
- text="🚫 Пожалуйста, отправьте текстовое сообщение.",
- reply_markup=kb
- )
+ await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb)
return
if message.text.isdigit():
tg_id = int(message.text)
else:
# Удаление '@' символа в начале сообщения
- username = message.text.strip().lstrip('@')
+ username = message.text.strip().lstrip("@")
# Удаление начала ссылки на профиль
- username = username.replace('https://t.me/', '')
+ username = username.replace("https://t.me/", "")
- user = await session.fetchrow(
- "SELECT tg_id FROM users WHERE username = $1", username
- )
+ user = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username)
if not user:
await message.answer(
@@ -123,32 +109,19 @@ async def handle_user_data_input(
await process_user_search(message, state, session, tg_id)
-@router.message(
- UserEditorState.waiting_for_key_name,
- IsAdminFilter()
-)
-async def handle_key_name_input(
- message: types.Message,
- state: FSMContext,
- session: Any
-):
+@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
+async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any):
kb = build_admin_back_kb()
if not message.text:
- await message.answer(
- text="🚫 Пожалуйста, отправьте текстовое сообщение.",
- reply_markup=kb
- )
+ await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb)
return
key_name = sanitize_key_name(message.text)
key_details = await get_key_details(key_name, session)
if not key_details:
- await message.answer(
- text="🚫 Пользователь с указанным именем ключа не найден.",
- reply_markup=kb
- )
+ await message.answer(text="🚫 Пользователь с указанным именем ключа не найден.", reply_markup=kb)
return
await process_user_search(message, state, session, key_details["tg_id"])
@@ -159,46 +132,28 @@ async def handle_key_name_input(
IsAdminFilter(),
)
async def handle_send_message(
- callback_query: types.CallbackQuery,
- callback_data: AdminUserEditorCallback,
- state: FSMContext
+ callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext
):
tg_id = callback_data.tg_id
await callback_query.message.edit_text(
- text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:",
- reply_markup=build_editor_kb(tg_id)
+ text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", reply_markup=build_editor_kb(tg_id)
)
await state.update_data(tg_id=tg_id)
await state.set_state(UserEditorState.waiting_for_message_text)
-@router.message(
- UserEditorState.waiting_for_message_text,
- IsAdminFilter()
-)
-async def handle_message_text_input(
- message: types.Message,
- state: FSMContext
-):
+@router.message(UserEditorState.waiting_for_message_text, IsAdminFilter())
+async def handle_message_text_input(message: types.Message, state: FSMContext):
data = await state.get_data()
tg_id = data.get("tg_id")
try:
- await message.bot.send_message(
- chat_id=tg_id,
- text=message.text
- )
- await message.answer(
- text="✅ Сообщение успешно отправлено.",
- reply_markup=build_editor_kb(tg_id)
- )
+ await message.bot.send_message(chat_id=tg_id, text=message.text)
+ await message.answer(text="✅ Сообщение успешно отправлено.", reply_markup=build_editor_kb(tg_id))
except Exception as e:
- await message.answer(
- text=f"❌ Не удалось отправить сообщение: {e}",
- reply_markup=build_editor_kb(tg_id)
- )
+ await message.answer(text=f"❌ Не удалось отправить сообщение: {e}", reply_markup=build_editor_kb(tg_id))
await state.clear()
@@ -208,37 +163,28 @@ async def handle_message_text_input(
IsAdminFilter(),
)
async def handle_trial_restore(
- callback_query: types.CallbackQuery,
- callback_data: AdminUserEditorCallback,
- session: Any
+ callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
tg_id = callback_data.tg_id
- await restore_trial(tg_id, session)
- await callback_query.message.edit_text(
- text="✅ Триал успешно восстановлен!",
- reply_markup=build_editor_kb(tg_id)
- )
+ await update_trial(tg_id, 0, session)
+ await callback_query.message.edit_text(text="✅ Триал успешно восстановлен!", reply_markup=build_editor_kb(tg_id))
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_balance_edit"),
- IsAdminFilter()
-)
-async def handle_balance_change(
- callback_query: CallbackQuery,
- callback_data: AdminUserEditorCallback,
- session: Any
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_edit"), IsAdminFilter())
+async def handle_balance_change(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
tg_id = callback_data.tg_id
- records = await session.fetch("""
+ records = await session.fetch(
+ """
SELECT amount, payment_system, status, created_at
FROM payments
WHERE tg_id = $1
ORDER BY created_at DESC
LIMIT 5
- """, tg_id)
+ """,
+ tg_id,
+ )
balance = await get_user_balance(tg_id, session)
@@ -263,21 +209,12 @@ async def handle_balance_change(
else:
text += "\n 🚫 Отсутствуют"
- await callback_query.message.edit_text(
- text=text,
- reply_markup=build_users_balance_kb(tg_id)
- )
+ await callback_query.message.edit_text(text=text, reply_markup=build_users_balance_kb(tg_id))
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_balance_add"),
- IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_add"), IsAdminFilter())
async def handle_balance_add(
- callback_query: CallbackQuery,
- callback_data: AdminUserEditorCallback,
- state: FSMContext,
- session: Any
+ callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any
):
tg_id = callback_data.tg_id
amount = callback_data.data
@@ -292,19 +229,12 @@ async def handle_balance_add(
await callback_query.message.edit_text(
text="✍️ Введите сумму, которую хотите добавить на баланс пользователя:",
- reply_markup=build_users_balance_change_kb(tg_id)
+ reply_markup=build_users_balance_change_kb(tg_id),
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_balance_take"),
- IsAdminFilter()
-)
-async def handle_balance_take(
- callback_query: CallbackQuery,
- callback_data: AdminUserEditorCallback,
- state: FSMContext
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_take"), IsAdminFilter())
+async def handle_balance_take(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext):
tg_id = callback_data.tg_id
await state.update_data(tg_id=tg_id, op_type="take")
@@ -312,19 +242,12 @@ async def handle_balance_take(
await callback_query.message.edit_text(
text="✍️ Введите сумму, которую хотите вычесть из баланса пользователя:",
- reply_markup=build_users_balance_change_kb(tg_id)
+ reply_markup=build_users_balance_change_kb(tg_id),
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_balance_set"),
- IsAdminFilter()
-)
-async def handle_balance_set(
- callback_query: CallbackQuery,
- callback_data: AdminUserEditorCallback,
- state: FSMContext
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_set"), IsAdminFilter())
+async def handle_balance_set(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext):
tg_id = callback_data.tg_id
await state.update_data(tg_id=tg_id, op_type="set")
@@ -332,27 +255,19 @@ async def handle_balance_set(
await callback_query.message.edit_text(
text="✍️ Введите баланс, который хотите установить пользователю:",
- reply_markup=build_users_balance_change_kb(tg_id)
+ reply_markup=build_users_balance_change_kb(tg_id),
)
-@router.message(
- UserEditorState.waiting_for_balance,
- IsAdminFilter()
-)
-async def handle_balance_input(
- message: types.Message,
- state: FSMContext,
- session: Any
-):
+@router.message(UserEditorState.waiting_for_balance, IsAdminFilter())
+async def handle_balance_input(message: types.Message, state: FSMContext, session: Any):
data = await state.get_data()
tg_id = data.get("tg_id")
op_type = data.get("op_type")
if not message.text.isdigit() or int(message.text) < 0:
await message.answer(
- text="🚫 Пожалуйста, введите корректную сумму!",
- reply_markup=build_users_balance_change_kb(tg_id)
+ text="🚫 Пожалуйста, введите корректную сумму!", reply_markup=build_users_balance_change_kb(tg_id)
)
return
@@ -368,21 +283,12 @@ async def handle_balance_input(
text = f"✅ Баланс пользователя изменен на {amount}Р"
await set_user_balance(tg_id, amount, session)
- await message.answer(
- text=text,
- reply_markup=build_users_balance_change_kb(tg_id)
- )
+ await message.answer(text=text, reply_markup=build_users_balance_change_kb(tg_id))
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_key_edit"),
- IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter())
async def handle_key_edit(
- callback_query: CallbackQuery,
- callback_data: CallbackData,
- session: Any,
- update: bool = False
+ callback_query: CallbackQuery, callback_data: CallbackData, session: Any, update: bool = False
):
email = callback_data.data
key_details = await get_key_details(email, session)
@@ -403,42 +309,24 @@ async def handle_key_edit(
)
if not update or not callback_data.edit:
- await callback_query.message.edit_text(
- text=text,
- reply_markup=build_key_edit_kb(key_details, email)
- )
+ await callback_query.message.edit_text(text=text, reply_markup=build_key_edit_kb(key_details, email))
else:
await callback_query.message.edit_text(
- text=text,
- reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email)
+ text=text, reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email)
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_expiry_edit"),
- IsAdminFilter()
-)
-async def handle_change_expiry(
- callback_query: CallbackQuery,
- callback_data: AdminUserEditorCallback
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_expiry_edit"), IsAdminFilter())
+async def handle_change_expiry(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback):
tg_id = callback_data.tg_id
email = callback_data.data
- await callback_query.message.edit_reply_markup(
- reply_markup=build_users_key_expiry_kb(tg_id, email)
- )
+ await callback_query.message.edit_reply_markup(reply_markup=build_users_key_expiry_kb(tg_id, email))
-@router.callback_query(
- AdminUserKeyEditorCallback.filter(F.action == "add"),
- IsAdminFilter()
-)
+@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter())
async def handle_expiry_add(
- callback_query: CallbackQuery,
- callback_data: AdminUserKeyEditorCallback,
- state: FSMContext,
- session: Any
+ callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -463,18 +351,13 @@ async def handle_expiry_add(
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите добавить к времени действия ключа:",
- reply_markup=build_users_key_show_kb(tg_id, email)
+ reply_markup=build_users_key_show_kb(tg_id, email),
)
-@router.callback_query(
- AdminUserKeyEditorCallback.filter(F.action == "take"),
- IsAdminFilter()
-)
+@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "take"), IsAdminFilter())
async def handle_expiry_take(
- callback_query: CallbackQuery,
- callback_data: AdminUserKeyEditorCallback,
- state: FSMContext
+ callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -484,18 +367,13 @@ async def handle_expiry_take(
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите вычесть из времени действия ключа:",
- reply_markup=build_users_key_show_kb(tg_id, email)
+ reply_markup=build_users_key_show_kb(tg_id, email),
)
-@router.callback_query(
- AdminUserKeyEditorCallback.filter(F.action == "set"),
- IsAdminFilter()
-)
+@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "set"), IsAdminFilter())
async def handle_expiry_set(
- callback_query: CallbackQuery,
- callback_data: AdminUserKeyEditorCallback,
- state: FSMContext
+ callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -509,21 +387,11 @@ async def handle_expiry_set(
"\n Пример: 2025-02-09 09:01"
)
- await callback_query.message.edit_text(
- text=text,
- reply_markup=build_users_key_show_kb(tg_id, email)
- )
+ await callback_query.message.edit_text(text=text, reply_markup=build_users_key_show_kb(tg_id, email))
-@router.message(
- UserEditorState.waiting_for_expiry_time,
- IsAdminFilter()
-)
-async def handle_expiry_time_input(
- message: types.Message,
- state: FSMContext,
- session: Any
-):
+@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
+async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any):
data = await state.get_data()
tg_id = data.get("tg_id")
email = data.get("email")
@@ -532,7 +400,7 @@ async def handle_expiry_time_input(
if op_type != "set" and (not message.text.isdigit() or int(message.text) < 0):
await message.answer(
text="🚫 Пожалуйста, введите корректное количество дней!",
- reply_markup=build_users_key_show_kb(tg_id, email)
+ reply_markup=build_users_key_show_kb(tg_id, email),
)
return
@@ -555,31 +423,19 @@ async def handle_expiry_time_input(
await change_expiry_time(key_details["expiry_time"] - days * 24 * 3600 * 1000, email, session)
else:
try:
- expiry_time = int(
- datetime.strptime(message.text, "%Y-%m-%d %H:%M").timestamp() * 1000
- )
+ expiry_time = int(datetime.strptime(message.text, "%Y-%m-%d %H:%M").timestamp() * 1000)
text = f"✅ Время действия ключа изменено на {message.text}"
await change_expiry_time(expiry_time, email, session)
except ValueError:
- text = f"🚫 Пожалуйста, используйте корректный формат даты!"
+ text = "🚫 Пожалуйста, используйте корректный формат даты!"
except Exception as e:
text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}"
- await message.answer(
- text=text,
- reply_markup=build_users_key_show_kb(tg_id, email)
- )
+ await message.answer(text=text, reply_markup=build_users_key_show_kb(tg_id, email))
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_update_key"),
- IsAdminFilter()
-)
-async def handle_update_key(
- callback_query: CallbackQuery,
- callback_data: AdminUserEditorCallback,
- session: Any
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_update_key"), IsAdminFilter())
+async def handle_update_key(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -591,104 +447,65 @@ async def handle_update_key(
except Exception as e:
logger.error(f"Ошибка при обновлении ключа {email} администратором: {e}")
await callback_query.message.answer(
- text=f"❗ Произошла ошибка при обновлении ключа: {e}",
- reply_markup=build_user_key_kb(tg_id, email)
+ text=f"❗ Произошла ошибка при обновлении ключа: {e}", reply_markup=build_user_key_kb(tg_id, email)
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_delete_key"),
- IsAdminFilter()
-)
-async def handle_delete_key(
- callback_query: types.CallbackQuery,
- callback_data: AdminUserEditorCallback,
- session: Any
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key"), IsAdminFilter())
+async def handle_delete_key(callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
email = callback_data.data
- client_id = await session.fetchval(
- "SELECT client_id FROM keys WHERE email = $1", email
- )
+ client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email)
if client_id is None:
await callback_query.message.edit_text(
- text="🚫 Ключ не найден!",
- reply_markup=build_editor_kb(callback_data.tg_id)
+ text="🚫 Ключ не найден!", reply_markup=build_editor_kb(callback_data.tg_id)
)
return
await callback_query.message.edit_text(
- text="❓ Вы уверены, что хотите удалить ключ?",
- reply_markup=build_key_delete_kb(callback_data.tg_id, email)
+ text="❓ Вы уверены, что хотите удалить ключ?", reply_markup=build_key_delete_kb(callback_data.tg_id, email)
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"),
- IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), IsAdminFilter())
async def handle_delete_key_confirm(
- callback_query: types.CallbackQuery,
- callback_data: AdminUserEditorCallback,
- session: Any
+ callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
email = callback_data.data
- record = await session.fetchrow(
- "SELECT client_id FROM keys WHERE email = $1", email
- )
+ record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email)
kb = build_editor_kb(callback_data.tg_id)
if record:
client_id = record["client_id"]
- clusters = await get_servers_from_db()
+ clusters = await get_servers()
async def delete_key_from_servers():
tasks = []
for cluster_name, cluster_servers in clusters.items():
for _ in cluster_servers:
- tasks.append(
- delete_key_from_cluster(cluster_name, email, client_id)
- )
+ tasks.append(delete_key_from_cluster(cluster_name, email, client_id))
await asyncio.gather(*tasks)
await delete_key_from_servers()
- await delete_key_from_db(client_id, session)
+ await delete_key(client_id, session)
- await callback_query.message.edit_text(
- text="✅ Ключ успешно удален.",
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text="✅ Ключ успешно удален.", reply_markup=kb)
else:
- await callback_query.message.edit_text(
- text="🚫 Ключ не найден или уже удален.",
- reply_markup=kb
- )
+ await callback_query.message.edit_text(text="🚫 Ключ не найден или уже удален.", reply_markup=kb)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_delete_user"),
- IsAdminFilter()
-)
-async def handle_delete_user(
- callback_query: types.CallbackQuery,
- callback_data: AdminUserEditorCallback
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user"), IsAdminFilter())
+async def handle_delete_user(callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback):
tg_id = callback_data.tg_id
await callback_query.message.edit_text(
- text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
- reply_markup=build_user_delete_kb(tg_id)
+ text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?", reply_markup=build_user_delete_kb(tg_id)
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"),
- IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), IsAdminFilter())
async def handle_delete_user_confirm(
- callback_query: types.CallbackQuery,
- callback_data: AdminUserEditorCallback,
- session: Any
+ callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
tg_id = callback_data.tg_id
key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id)
@@ -697,8 +514,8 @@ async def handle_delete_user_confirm(
try:
tasks = []
for email, client_id in key_records:
- servers = await get_servers_from_db()
- for cluster_id, cluster in servers.items():
+ servers = await get_servers()
+ for cluster_id, _cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
except Exception as e:
@@ -709,8 +526,7 @@ async def handle_delete_user_confirm(
try:
await delete_user_data(session, tg_id)
await callback_query.message.edit_text(
- text=f"🗑️ Пользователь с ID {tg_id} был удален.",
- reply_markup=build_editor_kb(callback_data.tg_id)
+ text=f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=build_editor_kb(callback_data.tg_id)
)
except Exception as e:
logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
@@ -719,37 +535,19 @@ async def handle_delete_user_confirm(
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_editor"),
- IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter())
async def handle_editor(
- callback_query: types.CallbackQuery,
- callback_data: AdminUserEditorCallback,
- state: FSMContext,
- session: Any
+ callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any
):
- await process_user_search(
- callback_query.message,
- state,
- session,
- callback_data.tg_id,
- callback_data.edit
- )
+ await process_user_search(callback_query.message, state, session, callback_data.tg_id, callback_data.edit)
async def process_user_search(
- message: types.Message,
- state: FSMContext,
- session: Any,
- tg_id: int,
- edit: bool = False
+ message: types.Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False
) -> None:
await state.clear()
- balance = await session.fetchval(
- "SELECT balance FROM connections WHERE tg_id = $1", tg_id
- )
+ balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
if balance is None:
await message.answer(
@@ -758,15 +556,9 @@ async def process_user_search(
)
return
- username = await session.fetchval(
- "SELECT username FROM users WHERE tg_id = $1", tg_id
- )
- key_records = await session.fetch(
- "SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id
- )
- referral_count = await session.fetchval(
- "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
- )
+ username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
+ key_records = await session.fetch("SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id)
+ referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
text = (
f"📊 Информация о пользователе"
@@ -780,17 +572,11 @@ async def process_user_search(
if edit:
try:
- await message.edit_text(
- text=text,
- reply_markup=kb
- )
+ await message.edit_text(text=text, reply_markup=kb)
except TelegramBadRequest:
pass
else:
- await message.answer(
- text=text,
- reply_markup=kb
- )
+ await message.answer(text=text, reply_markup=kb)
async def get_key_details(email, session):
@@ -827,14 +613,12 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce
if client_id is None:
return ValueError(f"User with email {email} was not found")
- server_id = await session.fetchrow(
- "SELECT server_id FROM keys WHERE client_id = $1", client_id
- )
+ server_id = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id)
if not server_id:
return ValueError(f"User with client_id {server_id} was not found")
- clusters = await get_servers_from_db()
+ clusters = await get_servers()
async def update_key_on_all_servers():
tasks = [
@@ -859,7 +643,8 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce
async def get_user_balance(tg_id: int, session: Any) -> float:
try:
return await session.fetchval(
- "SELECT balance FROM connections WHERE tg_id = $1", tg_id,
+ "SELECT balance FROM connections WHERE tg_id = $1",
+ tg_id,
)
except Exception as e:
logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}")
@@ -870,7 +655,8 @@ async def add_user_balance(tg_id: int, balance: int, session: Any) -> None:
try:
await session.execute(
"UPDATE connections SET balance = balance + $1 WHERE tg_id = $2",
- balance, tg_id,
+ balance,
+ tg_id,
)
except Exception as e:
logger.error(f"Ошибка при добавлении баланса для пользователя {tg_id}: {e}")
@@ -880,7 +666,8 @@ async def set_user_balance(tg_id: int, balance: int, session: Any) -> None:
try:
await session.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2",
- balance, tg_id,
+ balance,
+ tg_id,
)
except Exception as e:
logger.error(f"Ошибка при установке баланса для пользователя {tg_id}: {e}")
diff --git a/handlers/buttons/gifts.py b/handlers/buttons/gifts.py
index f80d956a..3f9df975 100644
--- a/handlers/buttons/gifts.py
+++ b/handlers/buttons/gifts.py
@@ -1,6 +1,6 @@
GIFT = "🎁 Подарить подписку"
MY_GIFTS = "🎁 Мои подарки"
PROFILE = "👤 Личный кабинет"
-BACK = "🔙 Назад"
+BACK = "⬅️ Назад"
GIFTS_ABOUT = "Дарите подарки и следите, чтобы они дошли до адресата! 🎄"
SHARE_GIFT = "🎁 Поделиться подарком"
diff --git a/handlers/buttons/yookassa.py b/handlers/buttons/yookassa.py
index 52401db4..5bb26cbb 100644
--- a/handlers/buttons/yookassa.py
+++ b/handlers/buttons/yookassa.py
@@ -3,6 +3,10 @@ PAY_2 = "Оплатить"
BACK = "⬅️ Назад"
CUSTOM_SUM = "💰 Ввести свою сумму"
PROFILE = "👤 Личный кабинет"
-KEY_CREATION_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
-KEY_RENEWAL_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
+KEY_CREATION_PAYMENT_MESSAGE = (
+ "Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
+)
+KEY_RENEWAL_PAYMENT_MESSAGE = (
+ "Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
+)
DEFAULT_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей. Перейдите по ссылке для оплаты:"
diff --git a/handlers/captcha.py b/handlers/captcha.py
new file mode 100644
index 00000000..ef3bbc30
--- /dev/null
+++ b/handlers/captcha.py
@@ -0,0 +1,70 @@
+import random
+import secrets
+from typing import Any
+
+from aiogram import F, Router
+from aiogram.fsm.context import FSMContext
+from aiogram.types import CallbackQuery, Message
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from config import CAPTCHA_EMOJIS
+from logger import logger
+
+router = Router()
+
+
+async def generate_captcha(message: Message, state: FSMContext):
+ """Генерирует новую капчу и сохраняет правильный ответ в состоянии
+
+ Пример словаря CAPTCHA_EMOJIS:
+ {
+ "🐶": "собаку", # Собака
+ "🐱": "кошку", # Кошка
+ "🐭": "мышь", # Мышь
+ "🐹": "хомяка", # Хомяк
+ "🐰": "кролика", # Кролик
+ ...
+ }
+ """
+ # Выбираем случайный эмодзи и его описание из конфига
+ correct_emoji, correct_text = secrets.choice(list(CAPTCHA_EMOJIS.items()))
+
+ # Получаем 3 случайных неправильных эмодзи
+ wrong_emojis = random.sample([e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3)
+
+ # Создаем список всех эмодзи и перемешиваем их
+ all_emojis = [correct_emoji] + wrong_emojis
+ random.shuffle(all_emojis)
+
+ # Сохраняем правильный ответ в состоянии
+ await state.update_data(correct_emoji=correct_emoji)
+ await state.update_data(message=message)
+
+ # Создаем клавиатуру
+ builder = InlineKeyboardBuilder()
+ for emoji in all_emojis:
+ builder.button(text=emoji, callback_data=f"captcha_{emoji}")
+ builder.adjust(2, 2)
+
+ return {
+ "text": f"🔒 Для подтверждения что вы не робот, выберите кнопку с {correct_text}",
+ "markup": builder.as_markup(),
+ }
+
+
+@router.callback_query(F.data.startswith("captcha_"))
+async def check_captcha(callback: CallbackQuery, state: FSMContext, session: Any, admin: bool):
+ from handlers.start import start_command
+
+ selected_emoji = callback.data.split("captcha_")[1]
+ state_data = await state.get_data()
+ correct_emoji = state_data.get("correct_emoji")
+ message = state_data.get("message", callback.message)
+
+ if selected_emoji == correct_emoji:
+ logger.info(f"Пользователь {callback.message.chat.id} успешно прошел капчу")
+ await start_command(message, state, session, admin, False)
+ else:
+ logger.warning(f"Пользователь {callback.message.chat.id} неверно ответил на капчу")
+ captcha = await generate_captcha(message, state)
+ await callback.message.answer(text=captcha["text"], reply_markup=captcha["markup"])
diff --git a/handlers/coupons.py b/handlers/coupons.py
index 15b3aa55..0a125da2 100644
--- a/handlers/coupons.py
+++ b/handlers/coupons.py
@@ -1,4 +1,3 @@
-from datetime import datetime
from typing import Any
from aiogram import F, Router, types
@@ -7,7 +6,13 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from database import update_balance
+from database import (
+ check_coupon_usage,
+ create_coupon_usage,
+ get_coupon_by_code,
+ update_balance,
+ update_coupon_usage_count,
+)
class CouponActivationState(StatesGroup):
@@ -18,9 +23,7 @@ router = Router()
@router.callback_query(F.data == "activate_coupon")
-async def handle_activate_coupon(
- callback_query: types.CallbackQuery, state: FSMContext
-):
+async def handle_activate_coupon(callback_query: types.CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
@@ -40,58 +43,25 @@ async def process_coupon_code(message: types.Message, state: FSMContext, session
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
- await message.answer(
- activation_result, reply_markup=builder.as_markup(), parse_mode="HTML"
- )
+ await message.answer(activation_result, reply_markup=builder.as_markup())
await state.clear()
async def activate_coupon(user_id: int, coupon_code: str, session: Any):
- coupon_record = await session.fetchrow(
- """
- SELECT id, usage_limit, usage_count, is_used, amount
- FROM coupons
- WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
- """,
- coupon_code,
- )
+ coupon_record = await get_coupon_by_code(coupon_code, session)
if not coupon_record:
return "❌ Купон не найден 🚫 или его использование ограничено. 🔒 Пожалуйста, проверьте код и попробуйте снова. 🔍"
- usage_exists = await session.fetchrow(
- """
- SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2
- """,
- coupon_record["id"],
- user_id,
- )
+ usage_exists = await check_coupon_usage(coupon_record["id"], user_id, session)
if usage_exists:
return "❌ Вы уже активировали этот купон. 🚫 Купоны могут быть активированы только один раз. 🔒"
coupon_amount = coupon_record["amount"]
- async with session.transaction():
- await session.execute(
- """
- UPDATE coupons
- SET usage_count = usage_count + 1,
- is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END
- WHERE id = $1
- """,
- coupon_record["id"],
- )
+ await update_coupon_usage_count(coupon_record["id"], session)
+ await create_coupon_usage(coupon_record["id"], user_id, session)
- await session.execute(
- """
- INSERT INTO coupon_usages (coupon_id, user_id, used_at)
- VALUES ($1, $2, $3)
- """,
- coupon_record["id"],
- user_id,
- datetime.utcnow(),
- )
-
- await update_balance(user_id, coupon_amount)
+ await update_balance(user_id, coupon_amount, session)
return f"✅ Купон успешно активирован! 🎉\n\nНа ваш баланс добавлено {coupon_amount} рублей 💰."
diff --git a/handlers/donate.py b/handlers/donate.py
index ad460085..3de7d323 100644
--- a/handlers/donate.py
+++ b/handlers/donate.py
@@ -22,11 +22,7 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext)
await state.clear()
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(
- text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"
- )
- )
+ builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"))
builder.row(
InlineKeyboardButton(
text="💰 Ввести сумму доната",
@@ -44,14 +40,10 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext)
@router.callback_query(F.data == "enter_custom_donate_amount")
-async def process_enter_donate_amount(
- callback_query: types.CallbackQuery, state: FSMContext
-):
+async def process_enter_donate_amount(callback_query: types.CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate"))
- await callback_query.message.answer(
- "💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup()
- )
+ await callback_query.message.answer("💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup())
await state.set_state(DonateState.entering_donate_amount)
@@ -60,9 +52,7 @@ async def process_donate_amount_input(message: types.Message, state: FSMContext)
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_XTR <= 0:
- await message.answer(
- f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:"
- )
+ await message.answer(f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:")
return
await state.update_data(amount=amount)
@@ -99,9 +89,7 @@ async def on_successful_donate(message: types.Message, state: FSMContext):
try:
amount = float(message.successful_payment.invoice_payload.split("_")[0])
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
- )
+ builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖",
reply_markup=builder.as_markup(),
diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py
index 215b1452..89053024 100644
--- a/handlers/instructions/instructions.py
+++ b/handlers/instructions/instructions.py
@@ -1,12 +1,13 @@
import os
from typing import Any
-import asyncpg
+import aiofiles
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
+from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL
+from database import get_key_details
from handlers.texts import (
CONNECT_TV_TEXT,
INSTRUCTION_PC,
@@ -29,9 +30,7 @@ async def send_instructions(
if not os.path.isfile(image_path):
if isinstance(callback_query_or_message, types.CallbackQuery):
- await callback_query_or_message.message.answer(
- "Файл изображения не найден."
- )
+ await callback_query_or_message.message.answer("Файл изображения не найден.")
else:
await callback_query_or_message.answer("Файл изображения не найден.")
return
@@ -47,9 +46,10 @@ async def send_instructions(
else:
send_photo = callback_query_or_message.answer_photo
- with open(image_path, "rb") as image_from_buffer:
+ async with aiofiles.open(image_path, "rb") as image_from_buffer:
+ image_data = await image_from_buffer.read()
await send_photo(
- BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"),
+ BufferedInputFile(image_data, filename="instructions.jpg"),
caption=instructions_message,
reply_markup=builder.as_markup(),
)
@@ -57,23 +57,12 @@ async def send_instructions(
@router.callback_query(F.data.startswith("connect_pc|"))
async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
- tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
- record = await session.fetchrow(
- """
- SELECT k.key
- FROM keys k
- WHERE k.tg_id = $1 AND k.email = $2
- """,
- tg_id,
- key_name,
- )
+ record = await get_key_details(key_name, session)
if not record:
- await callback_query.message.answer(
- "❌ Ключ не найден. Проверьте имя ключа. 🔍"
- )
+ await callback_query.message.answer("❌ Ключ не найден. Проверьте имя ключа. 🔍")
return
key = record["key"]
@@ -81,21 +70,14 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
instruction_message = f"{key_message}{INSTRUCTION_PC}"
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(
- text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"
- )
- )
- builder.row(
- InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_MACOS}{key}")
- )
+ builder.row(InlineKeyboardButton(text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"))
+ builder.row(InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_MACOS}{key}"))
builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
instruction_message,
reply_markup=builder.as_markup(),
- parse_mode="HTML",
disable_web_page_preview=True,
)
@@ -105,56 +87,33 @@ async def process_connect_tv(callback_query: types.CallbackQuery):
key_name = callback_query.data.split("|")[1]
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(
- text="▶ Продолжить", callback_data=f"continue_tv|{key_name}"
- )
- )
+ builder.row(InlineKeyboardButton(text="▶ Продолжить", callback_data=f"continue_tv|{key_name}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
text=CONNECT_TV_TEXT,
reply_markup=builder.as_markup(),
- parse_mode="HTML",
disable_web_page_preview=True,
)
@router.callback_query(F.data.startswith("continue_tv|"))
-async def process_continue_tv(callback_query: types.CallbackQuery):
+async def process_continue_tv(callback_query: types.CallbackQuery, session: Any):
key_name = callback_query.data.split("|")[1]
tg_id = callback_query.from_user.id
logger.info(f"tg_id: {tg_id}, key_name: {key_name}")
- conn = await asyncpg.connect(DATABASE_URL)
- try:
- record = await conn.fetchrow(
- """
- SELECT k.key
- FROM keys k
- WHERE k.tg_id = $1 AND k.email = $2
- """,
- tg_id,
- key_name,
- )
+ record = await get_key_details(key_name, session)
- logger.info(f"Query result: {record}")
- finally:
- await conn.close()
+ logger.info(f"Query result: {record}")
subscription_link = record["key"]
message_text = SUBSCRIPTION_DETAILS_TEXT.format(subscription_link=subscription_link)
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(
- text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html"
- )
- )
+ builder.row(InlineKeyboardButton(text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
- await callback_query.message.answer(
- text=message_text, reply_markup=builder.as_markup(), parse_mode="HTML"
- )
+ await callback_query.message.answer(text=message_text, reply_markup=builder.as_markup())
diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py
index 038e5440..3330f2da 100644
--- a/handlers/keys/key_management.py
+++ b/handlers/keys/key_management.py
@@ -24,9 +24,10 @@ from config import (
USE_NEW_PAYMENT_FLOW,
)
from database import (
+ create_temporary_data,
get_balance,
+ get_key_details,
get_trial,
- save_temporary_data,
store_key,
update_balance,
)
@@ -56,16 +57,12 @@ class Form(StatesGroup):
@router.callback_query(F.data == "create_key")
-async def confirm_create_new_key(
- callback_query: CallbackQuery, state: FSMContext, session: Any
-):
+async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} confirmed creation of a new key.")
- logger.info(
- f"Balance for user {tg_id} is sufficient. Proceeding with key creation."
- )
+ logger.info(f"Balance for user {tg_id} is sufficient. Proceeding with key creation.")
await handle_key_creation(tg_id, state, session, callback_query)
@@ -84,9 +81,7 @@ async def handle_key_creation(
expiry_time = current_time + timedelta(days=TRIAL_TIME)
logger.info(f"Assigned 1-day trial to user {tg_id}.")
- await session.execute(
- "UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id
- )
+ await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id)
await create_key(tg_id, expiry_time, state, session, message_or_query)
else:
builder = InlineKeyboardBuilder()
@@ -108,9 +103,7 @@ async def handle_key_creation(
)
)
- builder.row(
- InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
- )
+ builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message_or_query.message.answer(
"💳 Выберите тарифный план для создания нового ключа:",
@@ -136,7 +129,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
if balance < plan_price:
required_amount = plan_price - balance
- await save_temporary_data(
+ await create_temporary_data(
session,
tg_id,
"waiting_for_payment",
@@ -154,12 +147,8 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
await handle_custom_amount_input(callback_query, session)
else:
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay")
- )
- builder.row(
- InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
- )
+ builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
+ builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
f"💳 Недостаточно средств. Для продолжения необходимо пополнить баланс на {required_amount}₽.",
@@ -169,7 +158,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
expiry_time = datetime.utcnow() + timedelta(days=duration_days)
await create_key(tg_id, expiry_time, None, session, callback_query)
- await update_balance(tg_id, -plan_price)
+ await update_balance(tg_id, -plan_price, session)
async def create_key(
@@ -201,15 +190,11 @@ async def create_key(
builder = InlineKeyboardBuilder()
for country in countries:
callback_data = f"select_country|{country}|{expiry_time.isoformat()}"
- builder.row(
- InlineKeyboardButton(
- text=country, callback_data=callback_data
- )
- )
+ builder.row(InlineKeyboardButton(text=country, callback_data=callback_data))
logger.info(f"[Country Selection] Добавлена кнопка для страны: {country} с callback_data: {callback_data}")
- builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="profile"))
- logger.info("[Country Selection] Добавлена кнопка '🔙 Назад'.")
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="profile"))
+ logger.info("[Country Selection] Добавлена кнопка '⬅️ Назад'.")
if isinstance(message_or_query, Message):
logger.info("[Country Selection] Сообщение пользователя - тип Message.")
@@ -243,11 +228,7 @@ async def create_key(
key_name = generate_random_email()
logger.info(f"[Key Generation] Сгенерировано имя ключа: {key_name} для пользователя {tg_id}")
- existing_key = await session.fetchrow(
- "SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
- key_name,
- tg_id,
- )
+ existing_key = await get_key_details(key_name, session)
if not existing_key:
break
logger.warning(f"[Key Generation] Имя ключа {key_name} уже существует. Генерация нового.")
@@ -368,16 +349,10 @@ async def finalize_key_creation(
key_name = generate_random_email()
logger.info(f"Generated random key name for user {tg_id}: {key_name}")
- existing_key = await session.fetchrow(
- "SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
- key_name,
- tg_id,
- )
+ existing_key = await get_key_details(key_name, session)
if not existing_key:
break
- logger.warning(
- f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one."
- )
+ logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.")
client_id = str(uuid.uuid4())
email = key_name.lower()
diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py
index c99f84b5..8de0861b 100644
--- a/handlers/keys/key_utils.py
+++ b/handlers/keys/key_utils.py
@@ -3,9 +3,9 @@ from typing import Any
from py3xui import AsyncApi
-from client import add_client, delete_client, extend_client_key
-from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, SUPERNODE, TOTAL_GB, PUBLIC_LINK
-from database import get_servers_from_db, store_key
+from client import ClientConfig, add_client, delete_client, extend_client_key
+from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB
+from database import get_servers, store_key
from handlers.utils import get_least_loaded_cluster
from logger import logger
@@ -15,7 +15,7 @@ async def create_key_on_cluster(cluster_id: str, tg_id: int, client_id: str, ema
Создает ключ на всех серверах указанного кластера.
"""
try:
- servers = await get_servers_from_db()
+ servers = await get_servers()
cluster = servers.get(cluster_id)
if not cluster:
@@ -75,9 +75,7 @@ async def create_client_on_server(
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
- logger.warning(
- f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
- )
+ logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.")
return
if SUPERNODE:
@@ -89,24 +87,27 @@ async def create_client_on_server(
await add_client(
xui,
- client_id,
- unique_email,
- tg_id,
- limit_ip=LIMIT_IP,
- total_gb=TOTAL_GB,
- expiry_time=expiry_timestamp,
- enable=True,
- flow="xtls-rprx-vision",
- inbound_id=int(inbound_id),
- sub_id=sub_id,
+ ClientConfig(
+ client_id=client_id,
+ email=unique_email,
+ tg_id=tg_id,
+ limit_ip=LIMIT_IP,
+ total_gb=TOTAL_GB,
+ expiry_time=expiry_timestamp,
+ enable=True,
+ flow="xtls-rprx-vision",
+ inbound_id=int(inbound_id),
+ sub_id=sub_id,
+ ),
)
if SUPERNODE:
await asyncio.sleep(0.7)
+
async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb):
try:
- servers = await get_servers_from_db()
+ servers = await get_servers()
cluster = servers.get(cluster_id)
if not cluster:
@@ -124,9 +125,7 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
- logger.warning(
- f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
- )
+ logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.")
continue
if SUPERNODE:
@@ -137,37 +136,20 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
sub_id = unique_email
tasks.append(
- extend_client_key(
- xui,
- int(inbound_id),
- unique_email,
- new_expiry_time,
- client_id,
- total_gb,
- sub_id
- )
+ extend_client_key(xui, int(inbound_id), unique_email, new_expiry_time, client_id, total_gb, sub_id)
)
await asyncio.gather(*tasks)
except Exception as e:
- logger.error(
- f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}"
- )
+ logger.error(f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}")
raise e
-async def delete_key_from_db(client_id, session):
- try:
- await session.execute("DELETE FROM keys WHERE client_id = $1", client_id)
- except Exception as e:
- logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
-
-
async def delete_key_from_cluster(cluster_id, email, client_id):
"""Удаление ключа с серверов в кластере"""
try:
- servers = await get_servers_from_db()
+ servers = await get_servers()
cluster = servers.get(cluster_id)
if not cluster:
@@ -200,15 +182,13 @@ async def delete_key_from_cluster(cluster_id, email, client_id):
await asyncio.gather(*tasks)
except Exception as e:
- logger.error(
- f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}"
- )
+ logger.error(f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}")
raise e
async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id):
try:
- servers = await get_servers_from_db()
+ servers = await get_servers()
cluster = servers.get(cluster_id)
if not cluster:
@@ -232,29 +212,27 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id
tasks.append(
add_client(
xui,
- client_id,
- email,
- tg_id,
- limit_ip=LIMIT_IP,
- total_gb=TOTAL_GB,
- expiry_time=expiry_time,
- enable=True,
- flow="xtls-rprx-vision",
- inbound_id=int(inbound_id),
- sub_id=email
+ ClientConfig(
+ client_id=client_id,
+ email=email,
+ tg_id=tg_id,
+ limit_ip=LIMIT_IP,
+ total_gb=TOTAL_GB,
+ expiry_time=expiry_time,
+ enable=True,
+ flow="xtls-rprx-vision",
+ inbound_id=int(inbound_id),
+ sub_id=email,
+ ),
)
)
await asyncio.gather(*tasks)
- logger.info(
- f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}"
- )
+ logger.info(f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}")
except Exception as e:
- logger.error(
- f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}"
- )
+ logger.error(f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}")
raise e
diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py
index b62466c7..018d3f1e 100644
--- a/handlers/keys/keys.py
+++ b/handlers/keys/keys.py
@@ -4,12 +4,12 @@ import os
from datetime import datetime, timedelta
from typing import Any
+import aiofiles
import asyncpg
import pytz
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from handlers.payments.yookassa_pay import process_custom_amount_input
from bot import bot
from config import (
@@ -27,10 +27,14 @@ from config import (
USE_NEW_PAYMENT_FLOW,
)
from database import (
+ check_server_name_by_cluster,
+ create_temporary_data,
delete_key,
get_balance,
- get_servers_from_db,
- save_temporary_data,
+ get_key_details,
+ get_keys,
+ get_keys_by_server,
+ get_servers,
update_balance,
update_key_expiry,
)
@@ -44,11 +48,11 @@ from handlers.buttons.add_subscribe import (
)
from handlers.keys.key_utils import (
delete_key_from_cluster,
- delete_key_from_db,
renew_key_in_cluster,
update_subscription,
)
from handlers.payments.robokassa_pay import handle_custom_amount_input
+from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import (
DISCOUNTS,
KEY_NOT_FOUND_MSG,
@@ -67,7 +71,7 @@ router = Router()
@router.callback_query(F.data == "view_keys")
@router.message(F.text == "/subs")
async def process_callback_or_message_view_keys(
- callback_query_or_message: types.Message | types.CallbackQuery, session: Any
+ callback_query_or_message: types.Message | types.CallbackQuery, session: Any
):
if isinstance(callback_query_or_message, types.CallbackQuery):
chat_id = callback_query_or_message.message.chat.id
@@ -79,21 +83,12 @@ async def process_callback_or_message_view_keys(
send_photo = callback_query_or_message.answer_photo
try:
- records = await session.fetch(
- """
- SELECT email, client_id, expiry_time
- FROM keys
- WHERE tg_id = $1
- """,
- chat_id,
- )
+ records = await get_keys(chat_id, session)
inline_keyboard, response_message = build_keys_response(records)
image_path = os.path.join("img", "pic_keys.jpg")
- await send_with_optional_image(
- send_message, send_photo, image_path, response_message, inline_keyboard
- )
+ await send_with_optional_image(send_message, send_photo, image_path, response_message, inline_keyboard)
except Exception as e:
error_message = f"Ошибка при получении ключей: {e}"
await send_message(text=error_message)
@@ -115,9 +110,7 @@ def build_keys_response(records):
if expiry_time:
expiry_date_full = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz)
- formatted_date_full = expiry_date_full.strftime(
- "до %d %B %Y года, %H:%M"
- ).lower()
+ formatted_date_full = expiry_date_full.strftime("до %d %B %Y года, %H:%M").lower()
formatted_date_short = expiry_date_full.strftime("до %d %B").lower()
else:
@@ -125,41 +118,31 @@ def build_keys_response(records):
formatted_date_short = "без срока действия"
button_text = f"{key_name} ({formatted_date_short})"
- builder.row(
- InlineKeyboardButton(
- text=button_text, callback_data=f"view_key|{key_name}"
- )
- )
+ builder.row(InlineKeyboardButton(text=button_text, callback_data=f"view_key|{key_name}"))
response_message += f"• {key_name} ({formatted_date_full})\n"
else:
response_message = (
- "🔑 У вас пока нет подписок.\n\n"
- "Вы можете создать новую подписку для подключения устройств."
+ "🔑 У вас пока нет подписок.\n\nВы можете создать новую подписку для подключения устройств."
)
- builder.row(
- InlineKeyboardButton(text="➕ Добавить подписку", callback_data="create_key")
- )
+ builder.row(InlineKeyboardButton(text="➕ Добавить подписку", callback_data="create_key"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
inline_keyboard = builder.as_markup()
return inline_keyboard, response_message
-async def send_with_optional_image(
- send_message, send_photo, image_path, text, keyboard
-):
+async def send_with_optional_image(send_message, send_photo, image_path, text, keyboard):
"""
Отправляет сообщение с изображением, если файл существует. В противном случае отправляет только текст.
"""
if os.path.isfile(image_path):
- with open(image_path, "rb") as image_file:
+ async with aiofiles.open(image_path, "rb") as image_file:
+ image_data = await image_file.read()
await send_photo(
- photo=BufferedInputFile(
- image_file.read(), filename=os.path.basename(image_path)
- ),
+ photo=BufferedInputFile(image_data, filename=os.path.basename(image_path)),
caption=text,
reply_markup=keyboard,
)
@@ -175,15 +158,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
- record = await session.fetchrow(
- """
- SELECT k.expiry_time, k.server_id, k.key
- FROM keys k
- WHERE k.tg_id = $1 AND k.email = $2
- """,
- tg_id,
- key_name,
- )
+ record = await get_key_details(key_name, session)
if record:
key = record["key"]
@@ -207,9 +182,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
)
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
- response_message = key_message(
- key, formatted_expiry_date, days_left_message, server_name
- )
+ response_message = key_message(key, formatted_expiry_date, days_left_message, server_name)
builder = InlineKeyboardBuilder()
@@ -223,46 +196,28 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
builder.row(
InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS),
- InlineKeyboardButton(
- text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID
- ),
+ InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{key}"),
- InlineKeyboardButton(
- text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key}"
- ),
+ InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key}"),
)
builder.row(
- InlineKeyboardButton(
- text=PC_BUTTON, callback_data=f"connect_pc|{key_name}"
- ),
- InlineKeyboardButton(
- text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"
- ),
+ InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{key_name}"),
+ InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"),
)
if ENABLE_DELETE_KEY_BUTTON:
builder.row(
- InlineKeyboardButton(
- text="⏳ Продлить", callback_data=f"renew_key|{key_name}"
- ),
- InlineKeyboardButton(
- text="❌ Удалить", callback_data=f"delete_key|{key_name}"
- ),
+ InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"),
+ InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}"),
)
else:
- builder.row(
- InlineKeyboardButton(
- text="⏳ Продлить", callback_data=f"renew_key|{key_name}"
- )
- )
-
- builder.row(
- InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
- )
+ builder.row(InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"))
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
+ builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
keyboard = builder.as_markup()
@@ -272,9 +227,10 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
await callback_query.message.answer("Файл изображения не найден.")
return
- with open(image_path, "rb") as image_file:
+ async with aiofiles.open(image_path, "rb") as image_file:
+ image_data = await image_file.read()
await callback_query.message.answer_photo(
- photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"),
+ photo=BufferedInputFile(image_data, filename="pic_view.jpg"),
caption=response_message,
reply_markup=keyboard,
)
@@ -291,9 +247,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
@router.callback_query(F.data.startswith("update_subscription|"))
-async def process_callback_update_subscription(
- callback_query: types.CallbackQuery, session: Any
-):
+async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
email = callback_query.data.split("|")[1]
@@ -302,9 +256,7 @@ async def process_callback_update_subscription(
await process_callback_view_key(callback_query, session)
except Exception as e:
logger.error(f"Ошибка при обновлении ключа {email} пользователем: {e}")
- await handle_error(
- tg_id, callback_query, f"Ошибка при обновлении подписки: {e}"
- )
+ await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}")
@router.callback_query(F.data.startswith("delete_key|"))
@@ -319,11 +271,7 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery):
callback_data=f"confirm_delete|{client_id}",
)
],
- [
- types.InlineKeyboardButton(
- text="❌ Нет, отменить", callback_data="view_keys"
- )
- ],
+ [types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys")],
]
)
@@ -341,14 +289,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
- record = await session.fetchrow(
- """
- SELECT client_id, expiry_time
- FROM keys
- WHERE email = $1
- """,
- key_name,
- )
+ record = await get_key_details(key_name, session)
if record:
client_id = record["client_id"]
@@ -360,9 +301,8 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
months = plan_details["months"]
price = plan_details["price"]
discount = DISCOUNTS.get(plan_id, 0)
- button_text = (
- f'📅 {months} месяц{"а" if months > 1 else ""} ({price} руб.)'
- + (f' {discount}% скидка' if discount > 0 else "")
+ button_text = f"📅 {months} месяц{'а' if months > 1 else ''} ({price} руб.)" + (
+ f" {discount}% скидка" if discount > 0 else ""
)
builder.row(
InlineKeyboardButton(
@@ -371,19 +311,13 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
)
)
- builder.row(
- InlineKeyboardButton(
- text="🔙 Назад", callback_data="view_keys"
- )
- )
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
balance = await get_balance(tg_id)
response_message = PLAN_SELECTION_MSG.format(
balance=balance,
- expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime(
- "%Y-%m-%d %H:%M:%S"
- ),
+ expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
)
await callback_query.message.answer(
@@ -397,38 +331,30 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
@router.callback_query(F.data.startswith("confirm_delete|"))
-async def process_callback_confirm_delete(
- callback_query: types.CallbackQuery, session: Any
-):
+async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any):
email = callback_query.data.split("|")[1]
try:
- record = await session.fetchrow(
- "SELECT client_id FROM keys WHERE email = $1", email
- )
+ record = await get_key_details(email, session)
if record:
client_id = record["client_id"]
response_message = "Ключ успешно удален."
- back_button = types.InlineKeyboardButton(
- text="Назад", callback_data="view_keys"
- )
+ back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
- await delete_key(client_id)
+ await delete_key(client_id, session)
await callback_query.message.answer(
response_message,
reply_markup=keyboard,
)
- servers = await get_servers_from_db()
+ servers = await get_servers(session)
async def delete_key_from_servers():
try:
tasks = []
- for cluster_id, cluster in servers.items():
- tasks.append(
- delete_key_from_cluster(cluster_id, email, client_id)
- )
+ for cluster_id, _cluster in servers.items():
+ tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
@@ -437,13 +363,11 @@ async def process_callback_confirm_delete(
asyncio.create_task(delete_key_from_servers())
- await delete_key_from_db(client_id, session)
+ await delete_key(client_id, session)
else:
response_message = "Ключ не найден или уже удален."
- back_button = types.InlineKeyboardButton(
- text="Назад", callback_data="view_keys"
- )
+ back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await callback_query.message.answer(
@@ -464,10 +388,7 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi
total_gb = TOTAL_GB * gb_multiplier.get(plan, 1) if TOTAL_GB > 0 else 0
try:
- record = await session.fetchrow(
- "SELECT email, expiry_time FROM keys WHERE client_id = $1",
- client_id,
- )
+ record = await get_keys_by_server(tg_id, client_id, session)
if record:
email = record["email"]
@@ -486,9 +407,10 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi
required_amount = cost - balance
logger.info(
- f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}")
+ f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}"
+ )
- await save_temporary_data(
+ await create_temporary_data(
session,
tg_id,
"waiting_for_renewal_payment",
@@ -543,15 +465,7 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup())
conn = await asyncpg.connect(DATABASE_URL)
- key_info = await conn.fetchrow(
- """
- SELECT server_id
- FROM keys
- WHERE tg_id = $1 AND client_id = $2
- """,
- tg_id,
- client_id,
- )
+ key_info = await get_key_details(email, conn)
if not key_info:
logger.error(f"[RENEW] Ключ с client_id {client_id} для пользователя {tg_id} не найден.")
@@ -561,14 +475,7 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
server_id = key_info["server_id"]
if USE_COUNTRY_SELECTION:
- cluster_info = await conn.fetchrow(
- """
- SELECT cluster_name
- FROM servers
- WHERE server_name = $1
- """,
- server_id,
- )
+ cluster_info = await check_server_name_by_cluster(server_id, conn)
if not cluster_info:
logger.error(f"[RENEW] Сервер {server_id} не найден в таблице servers.")
@@ -592,8 +499,8 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
total_gb,
)
- await update_key_expiry(client_id, new_expiry_time)
- await update_balance(tg_id, -cost)
+ await update_key_expiry(client_id, new_expiry_time, conn)
+ await update_balance(tg_id, -cost, conn)
logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.")
await renew_key_on_cluster()
diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py
index 35b19f29..ca2bea1a 100644
--- a/handlers/keys/subscriptions.py
+++ b/handlers/keys/subscriptions.py
@@ -7,12 +7,13 @@ import asyncpg
from aiohttp import web
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE, TRANSITION_DATE_STR
-from database import get_servers_from_db
+from database import get_key_details, get_servers
from logger import logger
# Глобальная переменная для пула соединений
db_pool = None
+
async def init_db_pool():
"""
Инициализация пула соединений, если он ещё не создан.
@@ -21,6 +22,7 @@ async def init_db_pool():
if not db_pool:
db_pool = await asyncpg.create_pool(dsn=DATABASE_URL, min_size=5, max_size=20)
+
async def fetch_url_content(url, tg_id):
try:
logger.info(f"Получение URL: {url} для tg_id: {tg_id}")
@@ -32,17 +34,16 @@ async def fetch_url_content(url, tg_id):
logger.info(f"Успешно получен контент с {url} для tg_id: {tg_id}")
return base64.b64decode(content).decode("utf-8").split("\n")
else:
- logger.error(
- f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}"
- )
+ logger.error(f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}")
return []
- except asyncio.TimeoutError:
+ except TimeoutError:
logger.error(f"Таймаут при получении {url} для tg_id: {tg_id}")
return []
except Exception as e:
logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}")
return []
+
async def combine_unique_lines(urls, tg_id, query_string):
if SUPERNODE:
logger.info(f"Режим SUPERNODE активен. Возвращаем первую ссылку для tg_id: {tg_id}")
@@ -51,9 +52,7 @@ async def combine_unique_lines(urls, tg_id, query_string):
url_with_query = f"{urls[0]}?{query_string}" if query_string else urls[0]
return await fetch_url_content(url_with_query, tg_id)
- logger.info(
- f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}"
- )
+ logger.info(f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}")
urls_with_query = [f"{url}?{query_string}" if query_string else url for url in urls]
logger.info(f"Составлены URL-адреса: {urls_with_query}")
@@ -65,19 +64,17 @@ async def combine_unique_lines(urls, tg_id, query_string):
for lines in results:
all_lines.update(filter(None, lines))
- logger.info(
- f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}"
- )
+ logger.info(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}")
return list(all_lines)
+
transition_date = datetime.strptime(TRANSITION_DATE_STR, "%Y-%m-%d %H:%M:%S")
transition_timestamp_ms = int(transition_date.timestamp() * 1000)
transition_timestamp_ms_adjusted = transition_timestamp_ms - (3 * 60 * 60 * 1000)
-logger.info(
- f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}"
-)
+logger.info(f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}")
+
async def handle_old_subscription(request):
email = request.match_info.get("email")
@@ -95,9 +92,7 @@ async def handle_old_subscription(request):
await init_db_pool()
async with db_pool.acquire() as conn:
- key_info = await conn.fetchrow(
- "SELECT created_at, server_id FROM keys WHERE email = $1", email
- )
+ key_info = await get_key_details(email, conn)
if not key_info:
logger.warning(f"Клиент с email {email} не найден в базе.")
@@ -115,14 +110,10 @@ async def handle_old_subscription(request):
status=400,
)
- logger.info(
- f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}"
- )
+ logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}")
created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000)
- logger.info(
- f"Время создания клиента в формате datetime (UTC): {created_at_datetime}"
- )
+ logger.info(f"Время создания клиента в формате datetime (UTC): {created_at_datetime}")
if created_at_ms >= transition_timestamp_ms_adjusted:
logger.info(f"Клиент с email {email} является новым.")
@@ -131,27 +122,22 @@ async def handle_old_subscription(request):
status=400,
)
- servers = await get_servers_from_db()
+ servers = await get_servers()
cluster_servers = servers.get(cluster_name, [])
logger.info(f"Сервера в кластере: {cluster_servers}")
- urls = [
- f"{server['subscription_url']}/{email}" for server in cluster_servers
- ]
+ urls = [f"{server['subscription_url']}/{email}" for server in cluster_servers]
combined_subscriptions = await combine_unique_lines(urls, email, "")
- base64_encoded = base64.b64encode(
- "\n".join(combined_subscriptions).encode("utf-8")
- ).decode("utf-8")
+ base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8")
encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}"
headers = {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
- "profile-title": "base64:"
- + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
+ "profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
@@ -175,9 +161,7 @@ async def handle_new_subscription(request):
await init_db_pool()
async with db_pool.acquire() as conn:
- client_data = await conn.fetchrow(
- "SELECT tg_id, server_id FROM keys WHERE email = $1", email
- )
+ client_data = await get_key_details(email, conn)
if not client_data:
logger.warning(f"Клиент с email {email} не найден в базе.")
@@ -196,21 +180,17 @@ async def handle_new_subscription(request):
status=403,
)
- servers = await get_servers_from_db()
+ servers = await get_servers()
cluster_servers = servers.get(cluster_name, [])
- urls = [
- f"{server['subscription_url']}/{email}" for server in cluster_servers
- ]
+ urls = [f"{server['subscription_url']}/{email}" for server in cluster_servers]
query_string = request.query_string
logger.info(f"Извлечен query string: {query_string}")
combined_subscriptions = await combine_unique_lines(urls, tg_id, query_string)
- base64_encoded = base64.b64encode(
- "\n".join(combined_subscriptions).encode("utf-8")
- ).decode("utf-8")
+ base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8")
encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}"
@@ -218,8 +198,7 @@ async def handle_new_subscription(request):
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
- "profile-title": "base64:"
- + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
+ "profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py
index 97821fb0..44745102 100644
--- a/handlers/keys/trial_key.py
+++ b/handlers/keys/trial_key.py
@@ -6,9 +6,9 @@ from typing import Any
import pytz
from py3xui import AsyncApi
-from client import add_client
+from client import ClientConfig, add_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME
-from database import get_servers_from_db, store_key, use_trial
+from database import get_servers, get_trial, store_key, update_trial
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
from logger import logger
@@ -16,9 +16,7 @@ from logger import logger
async def create_trial_key(tg_id: int, session: Any):
try:
- trial_status = await session.fetchval(
- "SELECT trial FROM connections WHERE tg_id = $1", tg_id
- )
+ trial_status = await get_trial(tg_id, session)
if trial_status == 1:
return {"error": "Вы уже использовали пробную версию."}
except Exception as e:
@@ -35,7 +33,7 @@ async def create_trial_key(tg_id: int, session: Any):
expiry_time = current_time + timedelta(days=TRIAL_TIME)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
- clusters = await get_servers_from_db()
+ clusters = await get_servers(session)
least_loaded_cluster = await get_least_loaded_cluster()
if least_loaded_cluster not in clusters:
raise ValueError(f"Кластер {least_loaded_cluster} не найден в базе данных.")
@@ -58,16 +56,18 @@ async def create_trial_key(tg_id: int, session: Any):
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
),
- client_id,
- email,
- tg_id,
- limit_ip=LIMIT_IP,
- total_gb=TOTAL_GB,
- expiry_time=expiry_timestamp,
- enable=True,
- flow="xtls-rprx-vision",
- inbound_id=int(server_info["inbound_id"]),
- sub_id=base_email
+ ClientConfig(
+ client_id=client_id,
+ email=email,
+ tg_id=tg_id,
+ limit_ip=LIMIT_IP,
+ total_gb=TOTAL_GB,
+ expiry_time=expiry_timestamp,
+ enable=True,
+ flow="xtls-rprx-vision",
+ inbound_id=int(server_info["inbound_id"]),
+ sub_id=base_email,
+ ),
)
)
@@ -83,5 +83,5 @@ async def create_trial_key(tg_id: int, session: Any):
session=session,
)
- await use_trial(tg_id, session)
+ await update_trial(tg_id, 1, session)
return result
diff --git a/handlers/notifications.py b/handlers/notifications.py
index 367fd0c6..b051e32a 100644
--- a/handlers/notifications.py
+++ b/handlers/notifications.py
@@ -2,6 +2,7 @@ import asyncio
import os
from datetime import datetime, timedelta
+import aiofiles
import asyncpg
import pytz
from aiogram import Bot, Router, types
@@ -9,7 +10,6 @@ from aiogram.exceptions import TelegramForbiddenError
from aiogram.types import BufferedInputFile
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
-from .utils import format_time_until_deletion
from config import (
ADMIN_PASSWORD,
@@ -17,20 +17,20 @@ from config import (
AUTO_DELETE_EXPIRED_KEYS,
AUTO_RENEW_KEYS,
DATABASE_URL,
+ DELETE_KEYS_DELAY,
DEV_MODE,
EXPIRED_KEYS_CHECK_INTERVAL,
RENEWAL_PLANS,
TOTAL_GB,
TRIAL_TIME,
- DELETE_KEYS_DELAY,
)
from database import (
- add_blocked_user,
add_notification,
check_notification_time,
+ create_blocked_user,
delete_key,
get_balance,
- get_servers_from_db,
+ get_servers,
update_balance,
update_key_expiry,
)
@@ -38,29 +38,10 @@ from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluste
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED
from logger import logger
+from .utils import format_time_until_deletion
+
router = Router()
-async def check_users_and_update_blocked(bot: Bot):
- conn = None
- try:
- conn = await asyncpg.connect(DATABASE_URL)
- users = await conn.fetch("SELECT tg_id FROM users")
-
- for user in users:
- try:
- await bot.send_chat_action(user['tg_id'], "typing")
- except (TelegramForbiddenError,Exception):
- await conn.execute(
- "INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING",
- user['tg_id']
- )
- logger.info(f"User {user['tg_id']} added to blocked_users")
- except Exception as e:
- logger.error(f"Error in check_users_and_update_blocked: {e}")
- finally:
- if conn:
- await conn.close()
-
async def periodic_expired_keys_check(bot: Bot):
"""Периодическая проверка истекших ключей с кастомным интервалом."""
@@ -80,7 +61,6 @@ async def periodic_expired_keys_check(bot: Bot):
await asyncio.sleep(EXPIRED_KEYS_CHECK_INTERVAL)
-
async def notify_expiring_keys(bot: Bot):
conn = None
try:
@@ -95,8 +75,6 @@ async def notify_expiring_keys(bot: Bot):
await notify_inactive_trial_users(bot, conn)
await asyncio.sleep(0.5)
- await check_online_users()
- await asyncio.sleep(0.5)
await notify_10h_keys(bot, conn, current_time, threshold_time_10h)
await asyncio.sleep(0.5)
await notify_24h_keys(bot, conn, current_time, threshold_time_24h)
@@ -110,24 +88,6 @@ async def notify_expiring_keys(bot: Bot):
logger.info("Соединение с базой данных закрыто.")
-
-async def is_bot_blocked(bot: Bot, chat_id: int) -> bool:
- if DEV_MODE:
- return False
- try:
- member = await bot.get_chat_member(chat_id, bot.id)
- blocked = member.status == "left"
- logger.info(
- f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}"
- )
- return blocked
- except Exception as e:
- logger.warning(
- f"Не удалось проверить статус бота для пользователя {chat_id}: {e}"
- )
- return False
-
-
async def notify_10h_keys(
bot: Bot,
conn: asyncpg.Connection,
@@ -163,7 +123,11 @@ async def process_10h_record(record, bot, conn):
time_left = expiry_date - current_date
days_left_message = (
- "Ключ истек" if time_left.total_seconds() <= 0 else f"{time_left.days}" if time_left.days > 0 else f"{time_left.seconds // 3600}"
+ "Ключ истек"
+ if time_left.total_seconds() <= 0
+ else f"{time_left.days}"
+ if time_left.days > 0
+ else f"{time_left.seconds // 3600}"
)
message = KEY_EXPIRY_10H.format(
@@ -177,11 +141,11 @@ async def process_10h_record(record, bot, conn):
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
try:
- await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
+ await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn)
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
- await update_key_expiry(record["client_id"], new_expiry_time)
+ await update_key_expiry(record["client_id"], new_expiry_time, conn)
- servers = await get_servers_from_db()
+ servers = await get_servers(conn)
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
@@ -194,10 +158,11 @@ async def process_10h_record(record, bot, conn):
)
if os.path.isfile(image_path):
- with open(image_path, "rb") as image_file:
+ async with aiofiles.open(image_path, "rb") as image_file:
+ image_data = await image_file.read()
await bot.send_photo(
tg_id,
- photo=BufferedInputFile(image_file.read(), filename="notify_10h.jpg"),
+ photo=BufferedInputFile(image_data, filename="notify_10h.jpg"),
caption=KEY_RENEWED.format(email=email),
reply_markup=keyboard,
)
@@ -212,7 +177,6 @@ async def process_10h_record(record, bot, conn):
await send_renewal_notification(bot, tg_id, email, message, conn, record["client_id"], "notified")
-
async def notify_24h_keys(
bot: Bot,
conn: asyncpg.Connection,
@@ -250,7 +214,11 @@ async def process_24h_record(record, bot, conn):
time_left = expiry_date - current_date
days_left_message = (
- "Ключ истек" if time_left.total_seconds() <= 0 else f"{time_left.days}" if time_left.days > 0 else f"{time_left.seconds // 3600}"
+ "Ключ истек"
+ if time_left.total_seconds() <= 0
+ else f"{time_left.days}"
+ if time_left.days > 0
+ else f"{time_left.seconds // 3600}"
)
message_24h = KEY_EXPIRY_24H.format(
@@ -263,11 +231,11 @@ async def process_24h_record(record, bot, conn):
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
try:
- await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
+ await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn)
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
- await update_key_expiry(record["client_id"], new_expiry_time)
+ await update_key_expiry(record["client_id"], new_expiry_time, conn)
- servers = await get_servers_from_db()
+ servers = await get_servers(conn)
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
@@ -280,10 +248,11 @@ async def process_24h_record(record, bot, conn):
)
if os.path.isfile(image_path):
- with open(image_path, "rb") as image_file:
+ async with aiofiles.open(image_path, "rb") as image_file:
+ image_data = await image_file.read()
await bot.send_photo(
tg_id,
- photo=BufferedInputFile(image_file.read(), filename="notify_24h.jpg"),
+ photo=BufferedInputFile(image_data, filename="notify_24h.jpg"),
caption=KEY_RENEWED.format(email=email),
reply_markup=keyboard,
)
@@ -308,10 +277,11 @@ async def send_renewal_notification(bot, tg_id, email, message, conn, client_id,
image_path = os.path.join("img", "notify_24h.jpg")
if os.path.isfile(image_path):
- with open(image_path, "rb") as image_file:
+ async with aiofiles.open(image_path, "rb") as image_file:
+ image_data = await image_file.read()
await bot.send_photo(
tg_id,
- photo=BufferedInputFile(image_file.read(), filename="notify_24h.jpg"),
+ photo=BufferedInputFile(image_data, filename="notify_24h.jpg"),
caption=message,
reply_markup=keyboard.as_markup(),
)
@@ -320,7 +290,7 @@ async def send_renewal_notification(bot, tg_id, email, message, conn, client_id,
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
- await conn.execute(f"UPDATE keys SET {flag} = TRUE WHERE client_id = $1", client_id)
+ await conn.execute("UPDATE keys SET notified_24h = $1 WHERE client_id = $2", flag, client_id)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
@@ -348,17 +318,10 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
username = user["username"]
first_name = user["first_name"]
last_name = user["last_name"]
- display_name = (
- username
- or first_name
- or last_name
- or "Пользователь"
- )
+ display_name = username or first_name or last_name or "Пользователь"
try:
- can_notify = await check_notification_time(
- tg_id, "inactive_trial", hours=24, session=conn
- )
+ can_notify = await check_notification_time(tg_id, "inactive_trial", hours=24, session=conn)
if can_notify:
builder = InlineKeyboardBuilder()
@@ -368,11 +331,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
callback_data="create_key",
)
)
- builder.row(
- types.InlineKeyboardButton(
- text="👤 Личный кабинет", callback_data="profile"
- )
- )
+ builder.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
keyboard = builder.as_markup()
message = (
@@ -384,26 +343,21 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
try:
await bot.send_message(tg_id, message, reply_markup=keyboard)
- logger.info(
- f"Отправлено уведомление неактивному пользователю {tg_id}."
- )
+ logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.")
await add_notification(tg_id, "inactive_trial", session=conn)
except TelegramForbiddenError:
- logger.warning(
- f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users."
- )
- await add_blocked_user(tg_id, conn)
+ logger.warning(f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users.")
+ await create_blocked_user(tg_id, conn)
except Exception as e:
- logger.error(
- f"Ошибка при отправке уведомления пользователю {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
except Exception as e:
logger.error(f"Ошибка при обработке пользователя {tg_id}: {e}")
await asyncio.sleep(1)
+
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float):
logger.info("Проверка подписок, срок действия которых скоро истекает...")
@@ -440,19 +394,17 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
for record in expired_keys:
try:
await delete_key_from_cluster(
- cluster_id=record["server_id"],
- email=record["email"],
- client_id=record["client_id"]
+ cluster_id=record["server_id"], email=record["email"], client_id=record["client_id"]
)
- await conn.execute(
- "DELETE FROM keys WHERE client_id = $1",
- record["client_id"]
+ await delete_key(record["client_id"], conn)
+ logger.info(
+ f"Ключ {record['client_id']} удалён"
+ + (f" после задержки {DELETE_KEYS_DELAY} сек." if DELETE_KEYS_DELAY > 0 else "")
)
- logger.info(f"Ключ {record['client_id']} удалён" +
- (f" после задержки {DELETE_KEYS_DELAY} сек." if DELETE_KEYS_DELAY > 0 else ""))
except Exception as e:
logger.error(f"Ошибка при удалении ключа {record['client_id']}: {e}")
+
async def process_key(record, bot, conn):
tg_id = record["tg_id"]
client_id = record["client_id"]
@@ -474,26 +426,20 @@ async def process_key(record, bot, conn):
keyboard = InlineKeyboardBuilder()
if DELETE_KEYS_DELAY > 0:
- keyboard.row(types.InlineKeyboardButton(
- text="🔄 Продлить",
- callback_data=f"renew_key|{email}"
- ))
-
- keyboard.row(types.InlineKeyboardButton(
- text="👤 Личный кабинет",
- callback_data="profile"
- ))
+ keyboard.row(types.InlineKeyboardButton(text="🔄 Продлить", callback_data=f"renew_key|{email}"))
+
+ keyboard.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
image_path = os.path.join("img", "notify_expired.jpg")
try:
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
- await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
+ await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn)
new_expiry_time = int((datetime.now(moscow_tz) + timedelta(days=30)).timestamp() * 1000)
- await update_key_expiry(client_id, new_expiry_time)
+ await update_key_expiry(client_id, new_expiry_time, conn)
- servers = await get_servers_from_db()
+ servers = await get_servers(conn)
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB)
@@ -511,10 +457,11 @@ async def process_key(record, bot, conn):
try:
if os.path.isfile(image_path):
- with open(image_path, "rb") as image_file:
+ async with aiofiles.open(image_path, "rb") as image_file:
+ image_data = await image_file.read()
await bot.send_photo(
tg_id,
- photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"),
+ photo=BufferedInputFile(image_data, filename="notify_expired.jpg"),
caption=KEY_RENEWED.format(email=email),
reply_markup=keyboard.as_markup(),
)
@@ -531,7 +478,7 @@ async def process_key(record, bot, conn):
time_since_expiry = current_time_utc - expiry_time
if time_since_expiry <= EXPIRED_KEYS_CHECK_INTERVAL * 1000:
message_expired = f"Ваша подписка {email} истекла. Пополните баланс для продления."
-
+
if DELETE_KEYS_DELAY > 0:
time_until_deletion = format_time_until_deletion(DELETE_KEYS_DELAY)
if time_until_deletion != "0 минут":
@@ -539,19 +486,16 @@ async def process_key(record, bot, conn):
try:
if os.path.isfile(image_path):
- with open(image_path, "rb") as image_file:
+ async with aiofiles.open(image_path, "rb") as image_file:
+ image_data = await image_file.read()
await bot.send_photo(
tg_id,
- photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"),
+ photo=BufferedInputFile(image_data, filename="notify_expired.jpg"),
caption=message_expired,
reply_markup=keyboard.as_markup(),
)
else:
- await bot.send_message(
- tg_id,
- text=message_expired,
- reply_markup=keyboard.as_markup()
- )
+ await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard.as_markup())
logger.info(f"Уведомление об истечении подписки отправлено пользователю {tg_id}.")
except Exception as e:
logger.error(f"Не удалось отправить уведомление об истечении клиенту {tg_id}: {e}")
@@ -560,9 +504,9 @@ async def process_key(record, bot, conn):
if AUTO_DELETE_EXPIRED_KEYS:
current_time = int(datetime.utcnow().timestamp() * 1000)
-
+
if DELETE_KEYS_DELAY == 0 or current_time >= expiry_time + (DELETE_KEYS_DELAY * 1000):
- servers = await get_servers_from_db()
+ servers = await get_servers(conn)
for cluster_id in servers:
try:
@@ -592,22 +536,3 @@ async def process_key(record, bot, conn):
except Exception as e:
logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
-
-async def check_online_users():
- servers = await get_servers_from_db()
-
- for cluster_id, cluster in servers.items():
- for server_id, server in enumerate(cluster):
- xui = AsyncApi(
- server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD
- )
- await xui.login()
- try:
- online_users = len(await xui.client.online())
- logger.info(
- f"Сервер '{server['server_name']}' доступен, текущее количество активных пользователей: {online_users}."
- )
- except Exception as e:
- logger.error(
- f"Не удалось проверить пользователей на сервере {server_id}: {e}"
- )
diff --git a/handlers/pay.py b/handlers/pay.py
index 2b7c753c..24dd97a1 100644
--- a/handlers/pay.py
+++ b/handlers/pay.py
@@ -54,15 +54,9 @@ async def handle_pay(callback_query: CallbackQuery):
)
)
- builder.row(
- InlineKeyboardButton(
- text="🎟️ Активировать купон", callback_data="activate_coupon"
- )
- )
+ builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon"))
if DONATIONS_ENABLE:
- builder.row(
- InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
- )
+ builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py
index f9cb7a6e..5daeee61 100644
--- a/handlers/payments/robokassa_pay.py
+++ b/handlers/payments/robokassa_pay.py
@@ -52,9 +52,7 @@ if ROBOKASSA_ENABLE:
def generate_payment_link(amount, inv_id, description, tg_id):
"""Генерация ссылки на оплату."""
- logger.debug(
- f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}"
- )
+ logger.debug(f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}")
payment_link = robokassa._payment.link.generate_by_script(
out_sum=amount,
inv_id=inv_id,
@@ -66,9 +64,7 @@ def generate_payment_link(amount, inv_id, description, tg_id):
@router.callback_query(F.data == "pay_robokassa")
-async def process_callback_pay_robokassa(
- callback_query: types.CallbackQuery, state: FSMContext, session: Any
-):
+async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated Robokassa payment.")
@@ -78,18 +74,18 @@ async def process_callback_pay_robokassa(
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
- callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
+ callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
- callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
+ callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i + 1]['callback_data']}",
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
- callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
+ callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
@@ -111,9 +107,7 @@ async def process_callback_pay_robokassa(
@router.callback_query(F.data.startswith("robokassa_amount|"))
-async def process_amount_selection(
- callback_query: types.CallbackQuery, state: FSMContext
-):
+async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
logger.info(f"Получены данные callback_data: {callback_query.data}")
data = callback_query.data.split("|")
@@ -167,9 +161,7 @@ async def robokassa_webhook(request):
shp_id = params.get("shp_id")
signature_value = params.get("SignatureValue")
- logger.info(
- f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}"
- )
+ logger.info(f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}")
if not check_payment_signature(params):
logger.error("Неверная подпись или данные запроса.")
@@ -208,9 +200,7 @@ def check_payment_signature(params):
logger.info(f"Signature string before hashing: {signature_string}")
- expected_signature = (
- hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
- )
+ expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
logger.info(f"Expected signature: {expected_signature}")
logger.info(f"Received signature: {signature_value}")
@@ -219,27 +209,25 @@ def check_payment_signature(params):
@router.callback_query(F.data == "enter_custom_amount_robokassa")
-async def process_custom_amount_selection(
- callback_query: types.CallbackQuery, state: FSMContext
-):
+async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} chose to enter a custom amount.")
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_robokassa"))
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa"))
await callback_query.message.answer(
"Пожалуйста, введите сумму пополнения.",
reply_markup=builder.as_markup(),
)
- await state.set_state(
- ReplenishBalanceState.waiting_for_payment_confirmation_robokassa
- )
+ await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
-async def handle_custom_amount_input(message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None):
+async def handle_custom_amount_input(
+ message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None
+):
if isinstance(message, types.CallbackQuery):
tg_id = message.message.chat.id
else:
@@ -249,7 +237,6 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer
inv_id = 0
try:
-
conn = await asyncpg.connect(DATABASE_URL)
user_data = await get_temporary_data(conn, tg_id)
await conn.close()
@@ -277,9 +264,13 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer
)
if state_type == "waiting_for_payment":
- message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
+ message_text = (
+ f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
+ )
elif state_type == "waiting_for_renewal_payment":
- message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
+ message_text = (
+ f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
+ )
else:
await message.answer("Некорректное состояние данных. Попробуйте снова.")
return
diff --git a/handlers/profile.py b/handlers/profile.py
index dfd5fa23..64bbe141 100644
--- a/handlers/profile.py
+++ b/handlers/profile.py
@@ -1,6 +1,7 @@
import os
from typing import Any
+import aiofiles
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
@@ -8,7 +9,7 @@ from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS
-from database import get_balance, get_key_count, get_referral_stats, get_trial
+from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial
from handlers.buttons.profile import (
ADD_SUB,
BALANCE,
@@ -51,9 +52,7 @@ async def process_callback_view_profile(
try:
trial_status = await get_trial(chat_id, conn)
- profile_message = profile_message_send(
- username, chat_id, int(balance), key_count
- )
+ profile_message = profile_message_send(username, chat_id, int(balance), key_count)
if key_count == 0:
profile_message += "\n
🔧 Нажмите кнопку ➕ Подписка, чтобы настроить VPN-подключение" @@ -81,22 +80,21 @@ async def process_callback_view_profile( InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"), ) if admin: - builder.row( - InlineKeyboardButton(text="🔧 Администратор", callback_data="admin") - ) + builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start")) if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() if is_callback: await callback_query_or_message.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic.jpg"), + photo=BufferedInputFile(image_data, filename="pic.jpg"), caption=profile_message, reply_markup=builder.as_markup(), ) else: await callback_query_or_message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic.jpg"), + photo=BufferedInputFile(image_data, filename="pic.jpg"), caption=profile_message, reply_markup=builder.as_markup(), ) @@ -122,10 +120,8 @@ async def balance_handler(callback_query: types.CallbackQuery): builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - await callback_query.message.answer( - "💰 Управление балансом:", - reply_markup=builder.as_markup() - ) + await callback_query.message.answer("💰 Управление балансом:", reply_markup=builder.as_markup()) + @router.callback_query(F.data == "balance_history") async def balance_history_handler(callback_query: types.CallbackQuery, session: Any): @@ -133,22 +129,15 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session: builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - query = """ - SELECT amount, payment_system, status, created_at - FROM payments - WHERE tg_id = $1 - ORDER BY created_at DESC - LIMIT 3 - """ - records = await session.fetch(query, callback_query.from_user.id) + records = await get_last_payments(callback_query.from_user.id, session) if records: history_text = "📊 Последние 3 операции с балансом:\n\n" for record in records: - amount = record['amount'] - payment_system = record['payment_system'] - status = record['status'] - date = record['created_at'].strftime('%Y-%m-%d %H:%M:%S') + amount = record["amount"] + payment_system = record["payment_system"] + status = record["status"] + date = record["created_at"].strftime("%Y-%m-%d %H:%M:%S") history_text += ( f"Сумма: {amount}₽\n" f"Способ оплаты: {payment_system}\n" @@ -158,11 +147,7 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session: else: history_text = "❌ У вас пока нет операций с балансом." - await callback_query.message.answer( - history_text, - reply_markup=builder.as_markup() - ) - + await callback_query.message.answer(history_text, reply_markup=builder.as_markup()) @router.message(F.text == "/tariffs") @@ -183,9 +168,10 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): ) if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="tariffs.jpg"), + photo=BufferedInputFile(image_data, filename="tariffs.jpg"), caption=tariffs_message, reply_markup=builder.as_markup(), ) @@ -210,9 +196,10 @@ async def invite_handler(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic_invite.jpg"), + photo=BufferedInputFile(image_data, filename="pic_invite.jpg"), caption=invite_message, reply_markup=builder.as_markup(), ) diff --git a/handlers/start.py b/handlers/start.py index ad8d2c42..2eac8a4b 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,6 +1,7 @@ import os from typing import Any +import aiofiles from aiogram import F, Router from aiogram.filters import Command from aiogram.fsm.context import FSMContext @@ -13,6 +14,7 @@ from aiogram.types import ( from aiogram.utils.keyboard import InlineKeyboardBuilder from config import ( + CAPTCHA_ENABLE, CHANNEL_EXISTS, CHANNEL_URL, CONNECT_ANDROID, @@ -26,8 +28,10 @@ from database import ( add_connection, add_referral, check_connection_exists, + get_coupon_details, + get_referral_by_referred_id, get_trial, - use_trial, + update_trial, ) from handlers.buttons.add_subscribe import ( DOWNLOAD_ANDROID_BUTTON, @@ -37,6 +41,7 @@ from handlers.buttons.add_subscribe import ( PC_BUTTON, TV_BUTTON, ) +from handlers.captcha import generate_captcha from handlers.keys.key_management import create_key from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn @@ -47,18 +52,24 @@ router = Router() @router.callback_query(F.data == "start") async def handle_start_callback_query( - callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool + callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool, captcha: bool = False ): - await start_command(callback_query.message, state, session, admin) + await start_command(callback_query.message, state, session, admin, captcha) @router.message(Command("start")) -async def start_command(message: Message, state: FSMContext, session: Any, admin: bool): +async def start_command(message: Message, state: FSMContext, session: Any, admin: bool, captcha: bool = True): """Обрабатывает команду /start, включает логику рефералов и подарков.""" logger.info(f"Вызвана функция start_command для пользователя {message.chat.id}") await state.clear() + # Проверка капчи, если включена + if CAPTCHA_ENABLE and captcha: + captcha = await generate_captcha(message, state) + await message.answer(text=captcha["text"], reply_markup=captcha["markup"]) + return + if message.text: try: connection_exists = await check_connection_exists(message.chat.id) @@ -66,9 +77,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin if not connection_exists: await add_connection(tg_id=message.chat.id, session=session) - logger.info( - f"Пользователь {message.chat.id} успешно добавлен в базу данных." - ) + logger.info(f"Пользователь {message.chat.id} успешно добавлен в базу данных.") if "gift_" in message.text: logger.info(f"Обнаружена ссылка на подарок: {message.text}") @@ -77,51 +86,29 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin recipient_tg_id = message.chat.id - gift_info = await session.fetchrow( - "SELECT * FROM gifts WHERE gift_id = $1 AND is_used = FALSE", - gift_id, - ) + gift_info = await get_coupon_details(gift_id, session) if gift_info is None: - logger.warning( - f"Подарок с ID {gift_id} уже был использован или не существует." - ) - await message.answer( - "Этот подарок уже был использован или не существует." - ) + logger.warning(f"Подарок с ID {gift_id} уже был использован или не существует.") + await message.answer("Этот подарок уже был использован или не существует.") return await show_start_menu(message, admin, session) if gift_info["sender_tg_id"] == recipient_tg_id: logger.warning( f"Пользователь {recipient_tg_id} попытался активировать подарок, который был отправлен им самим." ) - await message.answer( - "❌ Вы не можете получить подарок от самого себя." - ) + await message.answer("❌ Вы не можете получить подарок от самого себя.") return await show_start_menu(message, admin, session) - await session.execute( - """ - INSERT INTO connections (tg_id, balance, trial) - VALUES ($1, 0, 1) - ON CONFLICT (tg_id) DO UPDATE SET trial = 1 - """, - recipient_tg_id, - ) + await add_connection(tg_id=recipient_tg_id, session=session) selected_months = gift_info["selected_months"] expiry_time = gift_info["expiry_time"] expiry_time_naive = expiry_time.replace(tzinfo=None) - logger.info( - f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}." - ) + logger.info(f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}.") - await create_key( - recipient_tg_id, expiry_time_naive, state, session, message - ) - logger.info( - f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев." - ) + await create_key(recipient_tg_id, expiry_time_naive, state, session, message) + logger.info(f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев.") await session.execute( "UPDATE gifts SET is_used = TRUE, recipient_tg_id = $1 WHERE gift_id = $2", @@ -132,9 +119,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin await message.answer( f"🎉 Ваш подарок на {selected_months} {'месяц' if selected_months == 1 else 'месяца' if selected_months in [2, 3, 4] else 'месяцев'} активирован!" ) - logger.info( - f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}." - ) + logger.info(f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}.") return elif "referral_" in message.text: @@ -142,36 +127,23 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin referrer_tg_id = int(message.text.split("referral_")[1]) if connection_exists: - logger.info( - f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом." - ) - await message.answer( - "❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку." - ) + logger.info(f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом.") + await message.answer("❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку.") return await show_start_menu(message, admin, session) if referrer_tg_id == message.chat.id: - logger.warning( - f"Пользователь {message.chat.id} попытался стать рефералом самого себя." - ) - await message.answer( - "❌ Вы не можете быть рефералом самого себя." - ) + logger.warning(f"Пользователь {message.chat.id} попытался стать рефералом самого себя.") + await message.answer("❌ Вы не можете быть рефералом самого себя.") return await show_start_menu(message, admin, session) - existing_referral = await session.fetchrow( - "SELECT * FROM referrals WHERE referred_tg_id = $1", - message.chat.id, - ) + existing_referral = await get_referral_by_referred_id(message.chat.id, session) if existing_referral: logger.info(f"Реферал с ID {message.chat.id} уже существует.") return await show_start_menu(message, admin, session) await add_referral(message.chat.id, referrer_tg_id, session) - logger.info( - f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}" - ) + logger.info(f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}") return await show_start_menu(message, admin, session) except (ValueError, IndexError) as e: @@ -179,16 +151,12 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin return else: - logger.info( - f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка." - ) + logger.info(f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка.") await show_start_menu(message, admin, session) except (ValueError, IndexError) as e: - logger.error( - f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}" - ) + logger.error(f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}") await message.answer("❌ Произошла ошибка. Пожалуйста, попробуйте снова.") else: await show_start_menu(message, admin, session) @@ -203,33 +171,28 @@ async def show_start_menu(message: Message, admin: bool, session: Any): builder = InlineKeyboardBuilder() if trial_status == 0: - builder.row( - InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn") - ) + builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) if CHANNEL_EXISTS: builder.row( InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL), - InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL) + InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL), ) else: - builder.row( - InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL) - ) + builder.row(InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL)) if admin: - builder.row( - InlineKeyboardButton(text="🔧 Администратор", callback_data="admin") - ) + builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) builder.row(InlineKeyboardButton(text="🌐 О VPN", callback_data="about_vpn")) if os.path.isfile(image_path): - with open(image_path, "rb") as image_from_buffer: + async with aiofiles.open(image_path, "rb") as image_from_buffer: + image_data = await image_from_buffer.read() await message.answer_photo( - photo=BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"), + photo=BufferedInputFile(image_data, filename="pic.jpg"), caption=WELCOME_TEXT, reply_markup=builder.as_markup(), ) @@ -249,7 +212,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): if "error" in trial_key_info: await callback_query.message.answer(trial_key_info["error"]) else: - await use_trial(user_id, session) + await update_trial(user_id, 1, session) key_message = ( f"🔑 Ваш персональный ключ доступа:\n" @@ -268,24 +231,20 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): builder.row( InlineKeyboardButton( text=IMPORT_IOS, - url=f'{CONNECT_IOS}{trial_key_info["key"]}', + url=f"{CONNECT_IOS}{trial_key_info['key']}", ), InlineKeyboardButton( text=IMPORT_ANDROID, - url=f'{CONNECT_ANDROID}{trial_key_info["key"]}', + url=f"{CONNECT_ANDROID}{trial_key_info['key']}", ), ) builder.row( InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"), InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"), ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await callback_query.message.answer( - key_message, reply_markup=builder.as_markup() - ) + await callback_query.message.answer(key_message, reply_markup=builder.as_markup()) @router.callback_query(F.data == "about_vpn") @@ -293,9 +252,7 @@ async def handle_about_vpn(callback_query: CallbackQuery): builder = InlineKeyboardBuilder() if DONATIONS_ENABLE: - builder.row( - InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") - ) + builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) builder.row( InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL), @@ -306,6 +263,4 @@ async def handle_about_vpn(callback_query: CallbackQuery): ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start")) - await callback_query.message.answer( - get_about_vpn("3.2.3-minor"), reply_markup=builder.as_markup() - ) + await callback_query.message.answer(get_about_vpn("3.2.3-minor"), reply_markup=builder.as_markup()) diff --git a/handlers/user.py b/handlers/user.py new file mode 100644 index 00000000..44a190ec --- /dev/null +++ b/handlers/user.py @@ -0,0 +1,22 @@ +from typing import Any + +from aiogram import Router +from aiogram.filters.chat_member_updated import KICKED, MEMBER, ChatMemberUpdatedFilter +from aiogram.types import ChatMemberUpdated + +from database import create_blocked_user, delete_blocked_user +from logger import logger + +router = Router() + + +@router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=KICKED)) +async def user_blocked_bot(event: ChatMemberUpdated, session: Any): + logger.info(f"User {event.from_user.id} blocked the bot.") + await create_blocked_user(event.from_user.id, session) + + +@router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=MEMBER)) +async def user_unblocked_bot(event: ChatMemberUpdated, session: Any): + logger.info(f"User {event.from_user.id} unblocked the bot.") + await delete_blocked_user(event.from_user.id, session) diff --git a/handlers/utils.py b/handlers/utils.py index 34c28d8e..8b4c71a7 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -1,23 +1,23 @@ import json import random import re +import secrets +import string +from datetime import datetime, timedelta import aiohttp import asyncpg from bot import bot from config import DATABASE_URL -from database import get_servers_from_db +from database import get_all_keys, get_servers from logger import logger -from datetime import datetime, timedelta async def get_usd_rate(): try: async with aiohttp.ClientSession() as session: - async with session.get( - "https://www.cbr-xml-daily.ru/daily_json.js" - ) as response: + async with session.get("https://www.cbr-xml-daily.ru/daily_json.js") as response: if response.status == 200: data = await response.text() usd = float(json.loads(data)["Valute"]["USD"]["Value"]) @@ -52,7 +52,7 @@ def generate_random_email(length: int = 6) -> str: Returns: str: Сгенерированная случайная строка. """ - return "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=length)) + return "".join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length)) if length > 0 else "" async def get_least_loaded_cluster() -> str: @@ -62,13 +62,13 @@ async def get_least_loaded_cluster() -> str: Returns: str: Идентификатор наименее загруженного кластера. """ - servers = await get_servers_from_db() + servers = await get_servers() cluster_loads: dict[str, int] = {cluster_id: 0 for cluster_id in servers.keys()} async with asyncpg.create_pool(DATABASE_URL) as pool: async with pool.acquire() as conn: - keys = await conn.fetch("SELECT server_id FROM keys") + keys = await get_all_keys(conn) for key in keys: cluster_id = key["server_id"] if cluster_id in cluster_loads: @@ -87,9 +87,7 @@ async def get_least_loaded_cluster() -> str: return least_loaded_cluster -async def handle_error( - tg_id: int, callback_query: object | None = None, message: str = "" -) -> None: +async def handle_error(tg_id: int, callback_query: object | None = None, message: str = "") -> None: """ Обрабатывает ошибку, отправляя сообщение пользователю. @@ -101,27 +99,26 @@ async def handle_error( try: if callback_query and hasattr(callback_query, "message"): try: - await bot.delete_message( - chat_id=tg_id, message_id=callback_query.message.message_id - ) + await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) except Exception as delete_error: logger.warning(f"Не удалось удалить сообщение: {delete_error}") - await bot.send_message(tg_id, message, parse_mode="HTML") + await bot.send_message(tg_id, message) except Exception as e: logger.error(f"Ошибка при обработке ошибки: {e}") + def format_time_until_deletion(seconds: int) -> str: if seconds <= 0: return "0 минут" - + days = seconds // (3600 * 24) hours = (seconds % (3600 * 24)) // 3600 minutes = (seconds % 3600 + 59) // 60 parts = [] - + if days > 0: if days == 1: parts.append(f"{days} день") @@ -129,7 +126,7 @@ def format_time_until_deletion(seconds: int) -> str: parts.append(f"{days} дня") else: parts.append(f"{days} дней") - + if hours > 0: if hours == 1: parts.append(f"{hours} час") @@ -137,7 +134,7 @@ def format_time_until_deletion(seconds: int) -> str: parts.append(f"{hours} часа") else: parts.append(f"{hours} часов") - + if minutes > 0 and days == 0: if minutes == 1: parts.append("1 минута") diff --git a/keyboards/admin/bans_kb.py b/keyboards/admin/bans_kb.py index 958d0a14..3c5c5145 100644 --- a/keyboards/admin/bans_kb.py +++ b/keyboards/admin/bans_kb.py @@ -1,21 +1,13 @@ from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn def build_bans_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="📄 Выгрузить в CSV", - callback_data=AdminPanelCallback(action="bans_export").pack() - ) - builder.button( - text="🗑️ Удалить из БД", - callback_data=AdminPanelCallback(action="bans_delete_banned").pack() - ) - builder.row( - build_admin_back_btn("management") - ) + builder.button(text="📄 Выгрузить в CSV", callback_data=AdminPanelCallback(action="bans_export").pack()) + builder.button(text="🗑️ Удалить из БД", callback_data=AdminPanelCallback(action="bans_delete_banned").pack()) + builder.row(build_admin_back_btn("management")) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/coupons_kb.py b/keyboards/admin/coupons_kb.py index 41ebd8c3..c24d705c 100644 --- a/keyboards/admin/coupons_kb.py +++ b/keyboards/admin/coupons_kb.py @@ -11,17 +11,9 @@ class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"): def build_coupons_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="➕ Создать купон", - callback_data=AdminPanelCallback(action="coupons_create").pack() - ) - builder.button( - text="Купоны", - callback_data=AdminPanelCallback(action="coupons_list").pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="➕ Создать купон", callback_data=AdminPanelCallback(action="coupons_create").pack()) + builder.button(text="Купоны", callback_data=AdminPanelCallback(action="coupons_list").pack()) + builder.row(build_admin_back_btn()) return builder.as_markup() @@ -32,12 +24,8 @@ def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup: coupon_code = coupon["code"] builder.button( text=f"❌ Удалить {coupon_code}", - callback_data=AdminCouponDeleteCallback( - coupon_code=coupon_code - ).pack(), + callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code).pack(), ) - builder.row( - build_admin_back_btn("coupons") - ) + builder.row(build_admin_back_btn("coupons")) return builder.as_markup() diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 9b85a216..2c23381d 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -1,5 +1,5 @@ from aiogram.filters.callback_data import CallbackData -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -9,76 +9,36 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"): def build_panel_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="👤 Поиск пользователя", - callback_data=AdminPanelCallback(action="search_user").pack() - ) - builder.button( - text="🔑 Поиск по названию ключа", - callback_data=AdminPanelCallback(action="search_key").pack() - ) + builder.button(text="👤 Поиск пользователя", callback_data=AdminPanelCallback(action="search_user").pack()) + builder.button(text="🔑 Поиск по названию ключа", callback_data=AdminPanelCallback(action="search_key").pack()) builder.row( - InlineKeyboardButton( - text="🖥️ Серверы", - callback_data=AdminPanelCallback(action="servers").pack() - ), - InlineKeyboardButton( - text="🎟️ Купоны", - callback_data=AdminPanelCallback(action="coupons").pack() - ) - ) - builder.button( - text="📢 Рассылка", - callback_data=AdminPanelCallback(action="sender").pack() + InlineKeyboardButton(text="🖥️ Серверы", callback_data=AdminPanelCallback(action="servers").pack()), + InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()), ) + builder.button(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()) builder.row( - InlineKeyboardButton( - text="📊 Статистика", - callback_data=AdminPanelCallback(action="stats").pack() - ), - InlineKeyboardButton( - text="🤖 Управление", - callback_data=AdminPanelCallback(action="management").pack() - ) - ) - builder.button( - text="Личный кабинет", - callback_data="profile" + InlineKeyboardButton(text="📊 Статистика", callback_data=AdminPanelCallback(action="stats").pack()), + InlineKeyboardButton(text="🤖 Управление", callback_data=AdminPanelCallback(action="management").pack()), ) + builder.button(text="Личный кабинет", callback_data="profile") builder.adjust(1, 1, 2, 1, 2, 1) return builder.as_markup() def build_management_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="💾 Создать резервную копию", - callback_data=AdminPanelCallback(action="backups").pack() - ) - builder.button( - text="🚫 Заблокировавшие бота", - callback_data=AdminPanelCallback(action="bans").pack() - ) - builder.button( - text="🔄 Перезагрузить бота", - callback_data=AdminPanelCallback(action="restart").pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="💾 Создать резервную копию", callback_data=AdminPanelCallback(action="backups").pack()) + builder.button(text="🚫 Заблокировавшие бота", callback_data=AdminPanelCallback(action="bans").pack()) + builder.button(text="🔄 Перезагрузить бота", callback_data=AdminPanelCallback(action="restart").pack()) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() def build_restart_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="✅ Да, перезагрузить", - callback_data=AdminPanelCallback(action="restart_confirm").pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="✅ Да, перезагрузить", callback_data=AdminPanelCallback(action="restart_confirm").pack()) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() @@ -89,9 +49,7 @@ def build_admin_back_kb(action: str = "admin") -> InlineKeyboardMarkup: def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.row( - build_admin_btn(text, action) - ) + builder.row(build_admin_btn(text, action)) return builder.as_markup() diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py index bfe9ad39..d0acfc59 100644 --- a/keyboards/admin/sender_kb.py +++ b/keyboards/admin/sender_kb.py @@ -11,26 +11,9 @@ class AdminSenderCallback(CallbackData, prefix="admin_sender"): def build_sender_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="👥 Все пользователи", - callback_data=AdminSenderCallback( - type="all" - ).pack() - ) - builder.button( - text="✅ Пользователи с подпиской", - callback_data=AdminSenderCallback( - type="subscribed" - ).pack() - ) - builder.button( - text="❌ Пользователи без подписки", - callback_data=AdminSenderCallback( - type="unsubscribed" - ).pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="👥 Все пользователи", callback_data=AdminSenderCallback(type="all").pack()) + builder.button(text="✅ Пользователи с подпиской", callback_data=AdminSenderCallback(type="subscribed").pack()) + builder.button(text="❌ Пользователи без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack()) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py index d7dd239c..e857408b 100644 --- a/keyboards/admin/servers_kb.py +++ b/keyboards/admin/servers_kb.py @@ -16,19 +16,11 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: for cluster_name in servers: builder.button( text=f"⚙️ {cluster_name}", - callback_data=AdminServerEditorCallback( - action="clusters_manage", - data=cluster_name - ).pack() + callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack(), ) - builder.button( - text="➕ Добавить кластер", - callback_data=AdminPanelCallback(action="clusters_add").pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="➕ Добавить кластер", callback_data=AdminPanelCallback(action="clusters_add").pack()) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() @@ -39,36 +31,22 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark for server in cluster_servers: builder.button( text=f"🌍 {server['server_name']}", - callback_data=AdminServerEditorCallback( - action="servers_manage", - data=server["server_name"] - ).pack() + callback_data=AdminServerEditorCallback(action="servers_manage", data=server["server_name"]).pack(), ) builder.button( text="➕ Добавить сервер", - callback_data=AdminServerEditorCallback( - action="servers_add", - data=cluster_name - ).pack() + callback_data=AdminServerEditorCallback(action="servers_add", data=cluster_name).pack(), ) builder.button( text="🌐 Доступность серверов", - callback_data=AdminServerEditorCallback( - action="servers_availability", - data=cluster_name - ).pack() + callback_data=AdminServerEditorCallback(action="servers_availability", data=cluster_name).pack(), ) builder.button( text="💾 Создать бэкап кластера", - callback_data=AdminServerEditorCallback( - action="clusters_backup", - data=cluster_name - ).pack() - ) - builder.row( - build_admin_back_btn("servers") + callback_data=AdminServerEditorCallback(action="clusters_backup", data=cluster_name).pack(), ) + builder.row(build_admin_back_btn("servers")) builder.adjust(1) return builder.as_markup() @@ -76,18 +54,10 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text="🗑️ Удалить", - callback_data=AdminServerEditorCallback( - action="servers_delete", - data=server_name - ).pack() + text="🗑️ Удалить", callback_data=AdminServerEditorCallback(action="servers_delete", data=server_name).pack() ) builder.button( - text="🔙 Назад", - callback_data=AdminServerEditorCallback( - action="clusters_manage", - data=cluster_name - ).pack() + text="🔙 Назад", callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack() ) builder.adjust(1) return builder.as_markup() @@ -96,18 +66,10 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text="✅ Да", - callback_data=AdminServerEditorCallback( - action="servers_delete_confirm", - data=server_name - ).pack() + text="✅ Да", callback_data=AdminServerEditorCallback(action="servers_delete_confirm", data=server_name).pack() ) builder.button( - text="🔙 Назад", - callback_data=AdminServerEditorCallback( - action="servers_manage", - data=server_name - ).pack() + text="🔙 Назад", callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack() ) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/stats_kb.py b/keyboards/admin/stats_kb.py index 4e1db626..b8046593 100644 --- a/keyboards/admin/stats_kb.py +++ b/keyboards/admin/stats_kb.py @@ -1,25 +1,19 @@ from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn def build_stats_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="🔄 Обновить", - callback_data=AdminPanelCallback(action="stats").pack() - ) + builder.button(text="🔄 Обновить", callback_data=AdminPanelCallback(action="stats").pack()) builder.button( text="📥 Выгрузить пользователей в CSV", - callback_data=AdminPanelCallback(action="stats_export_users_csv").pack() + callback_data=AdminPanelCallback(action="stats_export_users_csv").pack(), ) builder.button( - text="📥 Выгрузить оплаты в CSV", - callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack() - ) - builder.row( - build_admin_back_btn() + text="📥 Выгрузить оплаты в CSV", callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack() ) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 6eb8537c..54d056a9 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -32,47 +32,25 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: days = (expiry - current_time).days builder.button( text=f"🔑 {email} ({'<1' if days < 1 else days} дн.)", - callback_data=AdminUserEditorCallback( - action="users_key_edit", - tg_id=tg_id, - data=str(email) - ).pack() + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=str(email)).pack(), ) builder.button( - text="✉️ Сообщение", - callback_data=AdminUserEditorCallback( - action="users_send_message", - tg_id=tg_id - ).pack() + text="✉️ Сообщение", callback_data=AdminUserEditorCallback(action="users_send_message", tg_id=tg_id).pack() ) builder.button( text="💸 Изменить баланс", - callback_data=AdminUserEditorCallback( - action="users_balance_edit", - tg_id=tg_id - ).pack() + callback_data=AdminUserEditorCallback(action="users_balance_edit", tg_id=tg_id).pack(), ) builder.button( text="♻️ Восстановить триал", - callback_data=AdminUserEditorCallback( - action="users_trial_restore", - tg_id=tg_id - ).pack() + callback_data=AdminUserEditorCallback(action="users_trial_restore", tg_id=tg_id).pack(), ) builder.button( - text="❌ Удалить клиента", - callback_data=AdminUserEditorCallback( - action="users_delete_user", - tg_id=tg_id - ).pack() - ) - builder.row( - build_editor_btn("🔄 Обновить данные", tg_id, edit=True) - ) - builder.row( - build_admin_back_btn() + text="❌ Удалить клиента", callback_data=AdminUserEditorCallback(action="users_delete_user", tg_id=tg_id).pack() ) + builder.row(build_editor_btn("🔄 Обновить данные", tg_id, edit=True)) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() @@ -81,10 +59,7 @@ def build_users_balance_change_kb(tg_id: int) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text="🔙 Назад", # todo: fix magic text was set - callback_data=AdminUserEditorCallback( - action="users_balance_edit", - tg_id=tg_id - ).pack() + callback_data=AdminUserEditorCallback(action="users_balance_edit", tg_id=tg_id).pack(), ) return builder.as_markup() @@ -94,44 +69,23 @@ def build_users_balance_kb(tg_id: int) -> InlineKeyboardMarkup: for month, amount in RENEWAL_PRICES.items(): builder.button( text=f"+ {amount}Р ({month} мес.)", - callback_data=AdminUserEditorCallback( - action="users_balance_add", - tg_id=tg_id, - data=amount - ).pack() + callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=amount).pack(), ) builder.button( text=f"- {amount}Р ({month} мес.)", - callback_data=AdminUserEditorCallback( - action="users_balance_add", - tg_id=tg_id, - data=-amount - ).pack() + callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=-amount).pack(), ) builder.button( - text="💵 Добавить", - callback_data=AdminUserEditorCallback( - action="users_balance_add", - tg_id=tg_id - ).pack() + text="💵 Добавить", callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id).pack() ) builder.button( - text="💵 Вычесть", - callback_data=AdminUserEditorCallback( - action="users_balance_take", - tg_id=tg_id - ).pack() + text="💵 Вычесть", callback_data=AdminUserEditorCallback(action="users_balance_take", tg_id=tg_id).pack() ) builder.button( text="💵 Установить баланс", - callback_data=AdminUserEditorCallback( - action="users_balance_set", - tg_id=tg_id - ).pack() - ) - builder.row( - build_editor_back_btn(tg_id, True) + callback_data=AdminUserEditorCallback(action="users_balance_set", tg_id=tg_id).pack(), ) + builder.row(build_editor_back_btn(tg_id, True)) builder.adjust(2, 2, 2, 2, 2, 1) return builder.as_markup() @@ -140,12 +94,7 @@ def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text="🔙 Назад", # todo: fix magic text was set - callback_data=AdminUserEditorCallback( - action="users_key_edit", - tg_id=tg_id, - data=email, - edit=True - ).pack() + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email, edit=True).pack(), ) return builder.as_markup() @@ -156,53 +105,25 @@ def build_users_key_expiry_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: month = int(month) builder.button( text=f"+ {month} мес.", - callback_data=AdminUserKeyEditorCallback( - action="add", - tg_id=tg_id, - data=email, - month=month - ).pack() + callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email, month=month).pack(), ) builder.button( text=f"- {month} мес.", - callback_data=AdminUserKeyEditorCallback( - action="add", - tg_id=tg_id, - data=email, - month=-month - ).pack() + callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email, month=-month).pack(), ) builder.button( - text="⏳ Добавить дни", - callback_data=AdminUserKeyEditorCallback( - action="add", - tg_id=tg_id, - data=email - ).pack() + text="⏳ Добавить дни", callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email).pack() ) builder.button( - text="⏳ Вычесть дни", - callback_data=AdminUserKeyEditorCallback( - action="take", - tg_id=tg_id, - data=email - ).pack() + text="⏳ Вычесть дни", callback_data=AdminUserKeyEditorCallback(action="take", tg_id=tg_id, data=email).pack() ) builder.button( text="⏳ Установить дату истечения", - callback_data=AdminUserKeyEditorCallback( - action="set", - tg_id=tg_id, - data=email - ).pack() + callback_data=AdminUserKeyEditorCallback(action="set", tg_id=tg_id, data=email).pack(), ) builder.button( text="🔙 Назад", # todo: fix magic text was set - callback_data=AdminUserEditorCallback( - action="users_key_edit", - tg_id=tg_id, - data=email - ).pack() + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(), ) builder.adjust(2, 2, 2, 2, 2, 1) return builder.as_markup() @@ -212,14 +133,9 @@ def build_user_delete_kb(tg_id: int): builder = InlineKeyboardBuilder() builder.button( text="❌ Да, удалить!", - callback_data=AdminUserEditorCallback( - action="users_delete_user_confirm", - tg_id=tg_id - ).pack() - ) - builder.row( - build_editor_back_btn(tg_id, True) + callback_data=AdminUserEditorCallback(action="users_delete_user_confirm", tg_id=tg_id).pack(), ) + builder.row(build_editor_back_btn(tg_id, True)) builder.adjust(1) return builder.as_markup() @@ -227,12 +143,7 @@ def build_user_delete_kb(tg_id: int): def build_user_key_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text=f"🔙 Назад", - callback_data=AdminUserEditorCallback( - action="users_key_edit", - tg_id=tg_id, - data=email - ).pack() + text="🔙 Назад", callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack() ) builder.adjust(1) return builder.as_markup() @@ -243,30 +154,18 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: builder.button( text="⏳ Время истечения", callback_data=AdminUserEditorCallback( - action="users_expiry_edit", - data=email, - tg_id=key_details["tg_id"] - ).pack() + action="users_expiry_edit", data=email, tg_id=key_details["tg_id"] + ).pack(), ) builder.button( text="🔄 Перевыпустить", - callback_data=AdminUserEditorCallback( - action="users_update_key", - data=email, - tg_id=key_details["tg_id"] - ).pack() + callback_data=AdminUserEditorCallback(action="users_update_key", data=email, tg_id=key_details["tg_id"]).pack(), ) builder.button( text="❌ Удалить", - callback_data=AdminUserEditorCallback( - action="users_delete_key", - data=email, - tg_id=key_details["tg_id"] - ).pack() - ) - builder.row( - build_editor_back_btn(key_details["tg_id"], True) + callback_data=AdminUserEditorCallback(action="users_delete_key", data=email, tg_id=key_details["tg_id"]).pack(), ) + builder.row(build_editor_back_btn(key_details["tg_id"], True)) builder.adjust(1) return builder.as_markup() @@ -276,16 +175,10 @@ def build_key_delete_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton( text="✅ Да, удалить", - callback_data=AdminUserEditorCallback( - action="users_delete_key_confirm", - data=email, - tg_id=tg_id - ).pack() + callback_data=AdminUserEditorCallback(action="users_delete_key_confirm", data=email, tg_id=tg_id).pack(), ) ) - builder.row( - build_editor_back_btn(tg_id) - ) + builder.row(build_editor_back_btn(tg_id)) builder.adjust(1) return builder.as_markup() @@ -296,9 +189,7 @@ def build_editor_kb(tg_id: int, edit: bool = False) -> InlineKeyboardMarkup: def build_editor_singleton_kb(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.row( - build_editor_btn(text, tg_id, edit) - ) + builder.row(build_editor_btn(text, tg_id, edit)) return builder.as_markup() @@ -308,10 +199,5 @@ def build_editor_back_btn(tg_id: int, edit: bool = False) -> InlineKeyboardButto def build_editor_btn(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardButton: return InlineKeyboardButton( - text=text, - callback_data=AdminUserEditorCallback( - action="users_editor", - tg_id=tg_id, - edit=edit - ).pack() + text=text, callback_data=AdminUserEditorCallback(action="users_editor", tg_id=tg_id, edit=edit).pack() ) diff --git a/middlewares/__init__.py b/middlewares/__init__.py new file mode 100644 index 00000000..3257c3bd --- /dev/null +++ b/middlewares/__init__.py @@ -0,0 +1,25 @@ +from typing import Any + +from aiogram import Dispatcher + +from .admin import AdminMiddleware +from .delete import DeleteMessageMiddleware +from .loggings import LoggingMiddleware +from .session import SessionMiddleware +from .throttling import ThrottlingMiddleware +from .user import UserMiddleware + + +def register_middleware(dispatcher: Dispatcher) -> None: + middlewares = [ + AdminMiddleware(), + SessionMiddleware(), + DeleteMessageMiddleware(), + LoggingMiddleware(), + ThrottlingMiddleware(), + UserMiddleware(), + ] + + for middleware in middlewares: + dispatcher.message.outer_middleware(middleware) + dispatcher.callback_query.outer_middleware(middleware) diff --git a/middlewares/delete.py b/middlewares/delete.py index 29281507..b305c7bb 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -9,6 +9,7 @@ from keyboards.admin.panel_kb import AdminPanelCallback from keyboards.admin.sender_kb import AdminSenderCallback from keyboards.admin.servers_kb import AdminServerEditorCallback from keyboards.admin.users_kb import AdminUserEditorCallback, AdminUserKeyEditorCallback +from logger import logger pass_callbacks = [ AdminPanelCallback, @@ -22,22 +23,17 @@ pass_callbacks = [ class DeleteMessageMiddleware(BaseMiddleware): async def __call__( - self, - handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], - event: TelegramObject, - data: dict[str, Any], + self, + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: dict[str, Any], ) -> Any: if isinstance(event, Message): - if ( - not event.text - or not event.text.startswith("/start") - ): + if not event.text or not event.text.startswith("/start"): try: - await event.bot.delete_message( - event.chat.id, event.message_id - 1 - ) - except Exception: - pass + await event.bot.delete_message(event.chat.id, event.message_id - 1) + except Exception as e: + logger.error(e) await event.delete() if isinstance(event, CallbackQuery): diff --git a/middlewares/logging.py b/middlewares/loggings.py similarity index 100% rename from middlewares/logging.py rename to middlewares/loggings.py diff --git a/middlewares/database.py b/middlewares/session.py similarity index 93% rename from middlewares/database.py rename to middlewares/session.py index c9bf6906..a5d34441 100644 --- a/middlewares/database.py +++ b/middlewares/session.py @@ -8,7 +8,7 @@ from aiogram.types import TelegramObject from config import DATABASE_URL -class DatabaseMiddleware(BaseMiddleware): +class SessionMiddleware(BaseMiddleware): async def __call__( self, handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], diff --git a/middlewares/throttling.py b/middlewares/throttling.py index 2761920c..d1085dba 100644 --- a/middlewares/throttling.py +++ b/middlewares/throttling.py @@ -1,13 +1,31 @@ -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, MutableMapping from typing import Any from aiogram import BaseMiddleware -from aiogram.types import TelegramObject +from aiogram.dispatcher.flags import get_flag +from aiogram.types import TelegramObject, Update, User +from cachetools import TTLCache + +from logger import logger -class ThrottleMiddleware(BaseMiddleware): - def __init__(self, limit: int): - self.limit = limit +class ThrottlingMiddleware(BaseMiddleware): + def __init__( + self, + *, + default_key: str | None = "default", + default_ttl: float = 0.5, + **ttl_map: float, + ) -> None: + if default_key: + ttl_map[default_key] = default_ttl + + self.default_key = default_key + self.caches: dict[str, MutableMapping[int, None]] = {} + + for name, ttl in ttl_map.items(): + self.caches[name] = TTLCache(maxsize=10_000, ttl=ttl) + logger.debug("ThrottlingMiddleware initialized.") async def __call__( self, @@ -15,5 +33,32 @@ class ThrottleMiddleware(BaseMiddleware): event: TelegramObject, data: dict[str, Any], ) -> Any: - #todo + if not isinstance(event, Update): + logger.debug(f"Received event of type {type(event)}, skipping throttling.") + return await handler(event, data) + + if event.pre_checkout_query: + logger.debug("Pre-checkout query event, skipping throttling.") + return await handler(event, data) + + if event.message and event.message.successful_payment: + logger.debug("Successful payment event, skipping throttling.") + return await handler(event, data) + + user: User | None = data.get("event_from_user", None) + + if user is not None: + key = get_flag(data, "throttling_key", default=self.default_key) + + if key: + if user.id in self.caches[key]: + logger.warning(f"User {user.id} is being throttled with key: {key}") + return None + logger.debug( + f"User {user.id} is allowed to proceed, adding to cache with key: {key}", + ) + self.caches[key][user.id] = None + else: + logger.debug(f"No throttling key provided for user {user.id}, proceeding without throttle.") + return await handler(event, data) diff --git a/pyproject.toml b/pyproject.toml index e8e30a7a..634f9533 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [tool.ruff] line-length = 120 -target-version = "py310" +target-version = "py312" [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP", "ANN", "ASYNC", "S", "BLE", "FBT", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE"] -ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001'] +ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001','W291','ANN401','DTZ003','DTZ005','F401','FBT002','FBT001','FBT003','A005','E501','UP017','DTZ004','W293','ANN202','DTZ007'] exclude = [ ".git", "venv", @@ -14,7 +14,7 @@ exclude = [ [tool.ruff.format] quote-style = "double" -indent-style = "tab" +indent-style = "space" [tool.darker] src = ["."] diff --git a/requirements.txt b/requirements.txt index 3275ecfa..2d8df8b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,4 +32,6 @@ sqlalchemy robokassa ping3 ruff -pytz \ No newline at end of file +pytz +cachetools +babel \ No newline at end of file diff --git a/servers.py b/servers.py index eacfc9e9..e1f35d53 100644 --- a/servers.py +++ b/servers.py @@ -9,16 +9,14 @@ from ping3 import ping from bot import bot from config import ADMIN_ID, DATABASE_URL, PING_TIME -from database import get_servers_from_db +from database import check_unique_server_name, create_server, get_servers from logger import logger try: from config import CLUSTERS except ImportError: CLUSTERS = None - logger.warning( - "Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!" - ) + logger.warning("Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!") async def sync_servers_with_db(): @@ -27,9 +25,7 @@ async def sync_servers_with_db(): Если CLUSTERS не найден, синхронизация не будет выполнена. """ if CLUSTERS is None: - logger.info( - "Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена." - ) + logger.info("Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена.") return try: @@ -37,35 +33,21 @@ async def sync_servers_with_db(): logger.info("Подключение к базе данных для синхронизации серверов успешно.") for cluster_name, servers in CLUSTERS.items(): - for server_key, server_info in servers.items(): - exists = await conn.fetchval( - """ - SELECT 1 FROM servers - WHERE cluster_name = $1 AND server_name = $2 - """, - cluster_name, - server_info["name"], - ) + for _server_key, server_info in servers.items(): + exists = await check_unique_server_name(server_info["name"], conn, cluster_name) if not exists: - await conn.execute( - """ - INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id) - VALUES ($1, $2, $3, $4, $5) - """, - cluster_name, - server_info["name"], - server_info["API_URL"], - server_info["SUBSCRIPTION"], - server_info["INBOUND_ID"], - ) - logger.info( - f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных." + await create_server( + cluster_name=cluster_name, + server_name=server_info["name"], + api_url=server_info["API_URL"], + subscription_url=server_info["SUBSCRIPTION"], + inbound_id=server_info["INBOUND_ID"], + session=conn, ) + logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных.") else: - logger.info( - f"Сервер {server_info['name']} из кластера {cluster_name} уже существует." - ) + logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} уже существует.") except Exception as e: logger.error(f"Ошибка при синхронизации серверов: {e}") @@ -104,25 +86,14 @@ async def notify_admin(server_name: str): current_time = datetime.now() last_notification_time = last_notification_times.get(server_name) - if ( - last_notification_time - and current_time - last_notification_time < timedelta(minutes=3) - ): - logger.info( - f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут." - ) + if last_notification_time and current_time - last_notification_time < timedelta(minutes=3): + logger.info(f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут.") return - logger.info( - f"Отправка уведомлений администратору о недоступности сервера {server_name}..." - ) + logger.info(f"Отправка уведомлений администратору о недоступности сервера {server_name}...") builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="Управление сервером", callback_data=f"manage_server|{server_name}" - ) - ) + builder.row(InlineKeyboardButton(text="Управление сервером", callback_data=f"manage_server|{server_name}")) for admin_id in ADMIN_ID: await bot.send_message( @@ -132,12 +103,9 @@ async def notify_admin(server_name: str): "Проверьте соединение к серверу, подключение к панели или удалите его из таблицы серверов в боте, " "чтобы не выдать подписку к неработающему серверу." ), - parse_mode="HTML", reply_markup=builder.as_markup(), ) - logger.info( - f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}." - ) + logger.info(f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}.") last_notification_times[server_name] = current_time except Exception as e: @@ -149,7 +117,7 @@ async def check_servers(): Периодическая проверка серверов с учетом извлечения хоста из `api_url`. """ while True: - servers = await get_servers_from_db() + servers = await get_servers() current_time = datetime.now() logger.info(f"Начинаю проверку серверов: {current_time}") @@ -161,9 +129,7 @@ async def check_servers(): server_name = server["server_name"] server_host = extract_host(original_api_url) - logger.debug( - f"Проверка доступности сервера '{server_name}' с хостом {server_host}" - ) + logger.debug(f"Проверка доступности сервера '{server_name}' с хостом {server_host}") is_online = await ping_server(server_host) @@ -171,18 +137,12 @@ async def check_servers(): last_ping_times[server_name] = current_time else: last_ping_time = last_ping_times.get(server_name) - if last_ping_time and current_time - last_ping_time > timedelta( - minutes=3 - ): - logger.warning( - f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление." - ) + if last_ping_time and current_time - last_ping_time > timedelta(minutes=3): + logger.warning(f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление.") await notify_admin(server_name) elif not last_ping_time: last_ping_times[server_name] = current_time - logger.info( - f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован." - ) + logger.info(f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован.") logger.info("Завершена проверка всех серверов.") await asyncio.sleep(PING_TIME) diff --git a/utils/csv_export.py b/utils/csv_export.py index 08cc1c59..4b39c023 100644 --- a/utils/csv_export.py +++ b/utils/csv_export.py @@ -35,10 +35,7 @@ async def export_users_csv(session: Any) -> BufferedInputFile: # Перемещение указателя в начало для чтения buffer.seek(0) - return BufferedInputFile( - file=buffer.getvalue().encode("utf-8-sig"), - filename="users_export.csv" - ) + return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename="users_export.csv") async def export_payments_csv(session: Any) -> BufferedInputFile: @@ -95,7 +92,4 @@ def _export_payments_csv(payments: list, filename: str) -> BufferedInputFile: # Перемещение указателя в начало для чтения buffer.seek(0) - return BufferedInputFile( - file=buffer.getvalue().encode("utf-8-sig"), - filename=filename - ) + return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename=filename)