diff --git a/.gitignore b/.gitignore index 2b66b41b..56a3eb02 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ Thumbs.db nginx.conf scripts -models.py \ No newline at end of file +models.py +Dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 5644fd57..00000000 --- a/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM python:3.10-slim - -ENV PYTHONUNBUFFERED=1 - -WORKDIR /app - -COPY requirements.txt . - -RUN apt-get update && \ - apt-get install -y postgresql-client locales && \ - pip install --upgrade pip && pip install -r requirements.txt && \ - sed -i '/ru_RU.UTF-8/s/^# //g' /etc/locale.gen && \ - locale-gen ru_RU.UTF-8 - -ENV LANG=ru_RU.UTF-8 -ENV LANGUAGE=ru_RU:ru -ENV LC_ALL=ru_RU.UTF-8 - -COPY . . - -RUN sed -i "s|DATABASE_URL = .*|DATABASE_URL = '${DATABASE_URL}'|" config.py - -CMD ["python", "main.py"] diff --git a/Makefile b/Makefile index 1fb029e1..2a739bd8 100644 --- a/Makefile +++ b/Makefile @@ -2,4 +2,4 @@ formatting: @echo "Running black..." && black . @echo "Running isort..." && isort . @echo "Running flake8..." && flake8 --config .flake8 - @echo "Running pylint..." && pylint . + # @echo "Running pylint..." && pylint . diff --git a/backup.py b/backup.py index b3ec4395..a4d79675 100644 --- a/backup.py +++ b/backup.py @@ -62,9 +62,10 @@ async def _send_backup_to_admin(bot, backup_file_path): if isinstance(admin_ids, list): for id in admin_ids: await bot.send_document(id, backup_input_file) + logger.info(f"Бэкап базы данных отправлен админу: {id}") else: await bot.send_document(admin_ids, backup_input_file) - logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}") + logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}") except Exception as e: logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}") diff --git a/bot.py b/bot.py index 44011c1a..db72dba1 100644 --- a/bot.py +++ b/bot.py @@ -1,28 +1,33 @@ +import traceback + from aiogram import Bot, Dispatcher, Router +from aiogram.client.default import DefaultBotProperties +from aiogram.enums import ParseMode from aiogram.fsm.storage.memory import MemoryStorage +from aiogram.types import ErrorEvent from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE +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 -bot = Bot(token=API_TOKEN) +bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) router = Router() -from handlers import commands, coupons, donate, notifications, pay, profile, start -from handlers.admin import admin_commands, admin_coupons, admin_panel, admin_user_editor +from handlers import coupons, donate, notifications, pay, profile, start +from handlers.admin import admin_coupons, admin_panel, admin_user_editor from handlers.instructions import instructions from handlers.keys import key_management, keys from handlers.payments import cryprobot_pay, freekassa_pay, robokassa_pay, stars_pay, yookassa_pay -dp.include_router(admin_commands.router) dp.include_router(admin_coupons.router) dp.include_router(admin_panel.router) dp.include_router(admin_user_editor.router) -dp.include_router(commands.router) dp.include_router(coupons.router) dp.include_router(start.router) dp.include_router(profile.router) @@ -54,3 +59,17 @@ dp.callback_query.middleware(UserMiddleware()) dp.message.middleware(DatabaseMiddleware()) dp.callback_query.middleware(DatabaseMiddleware()) + +dp.message.outer_middleware(DeleteMessageMiddleware()) +dp.callback_query.outer_middleware(DeleteMessageMiddleware()) + + +@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()}" + ) diff --git a/database.py b/database.py index b915e99d..1585824f 100644 --- a/database.py +++ b/database.py @@ -1,14 +1,14 @@ from datetime import datetime +from typing import Any import asyncpg -from config import BONUS_PERCENT, DATABASE_URL +from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES from logger import logger async def init_db(): conn = await asyncpg.connect(DATABASE_URL) - # Таблица для хранения основной информации о пользователях из Telegram await conn.execute( """ @@ -106,14 +106,39 @@ async def init_db(): """ ) + # Таблица для отслеживания отправленных уведомлений + await conn.execute( + """ + CREATE TABLE IF NOT EXISTS notifications ( + tg_id BIGINT NOT NULL, + last_notification_time TIMESTAMP NOT NULL DEFAULT NOW(), + notification_type TEXT NOT NULL, + PRIMARY KEY (tg_id, notification_type) + ) + """ + ) + await conn.close() -async def create_coupon(coupon_code: str, amount: float, usage_limit: int): - conn = await asyncpg.connect(DATABASE_URL) +async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any): + """ + Создает новый купон в базе данных. + Args: + coupon_code (str): Уникальный код купона. + amount (float): Сумма, которую дает купон. + usage_limit (int): Максимальное количество использований купона. + session (Any): Сессия базы данных для выполнения запроса. + + Raises: + Exception: В случае ошибки при создании купона. + + Example: + await create_coupon('SALE50', 50.0, 5, session) + """ try: - await conn.execute( + await session.execute( """ INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used) VALUES ($1, $2, $3, 0, FALSE) @@ -122,339 +147,852 @@ async def create_coupon(coupon_code: str, amount: float, usage_limit: int): amount, usage_limit, ) + logger.info(f"Успешно создан купон с кодом {coupon_code} на сумму {amount}") except Exception as e: - logger.error(f"Ошибка при создании купона: {e}") + logger.error(f"Ошибка при создании купона {coupon_code}: {e}") raise - finally: - await conn.close() -async def get_all_coupons(): - """Получить список всех купонов""" - conn = await asyncpg.connect(DATABASE_URL) +async def get_all_coupons(session: Any): + """ + Получает список всех купонов из базы данных. + + Returns: + list: Список словарей с информацией о купонах, каждый словарь содержит: + - code (str): Код купона + - amount (int): Сумма купона + - usage_limit (int): Максимальное количество использований + - usage_count (int): Текущее количество использований купона + + Raises: + Exception: В случае ошибки при получении данных из базы + """ try: - coupons = await conn.fetch( + # Выполняем запрос на получение всех купонов из базы данных + coupons = await session.fetch( """ SELECT code, amount, usage_limit, usage_count FROM coupons """ ) + + # Логируем успешное получение списка купонов + logger.info(f"Успешно получено {len(coupons)} купонов из базы данных") + return coupons except Exception as e: - logger.error(f"Ошибка при получении купонов: {e}") + # Подробное логирование ошибки при получении купонов + logger.error(f"Критическая ошибка при получении списка купонов: {e}") + logger.exception("Трассировка стека ошибки получения купонов") return [] - finally: - await conn.close() -async def delete_coupon_from_db(coupon_code: str): - """Удалить купон из базы данных по его коду""" - conn = await asyncpg.connect(DATABASE_URL) +async def delete_coupon_from_db(coupon_code: str, session: Any): + """ + Удаляет купон из базы данных по его коду. + + Args: + coupon_code (str): Код купона для удаления + session (Any): Сессия базы данных для выполнения запроса + + Returns: + bool: True, если купон успешно удален, False если купон не найден или произошла ошибка + + Raises: + Exception: В случае ошибки при выполнении запроса к базе данных + + Example: + result = await delete_coupon_from_db('SALE50', session) + """ try: - - coupon_record = await conn.fetchrow( + # Проверяем существование купона в базе данных + coupon_record = await session.fetchrow( """ SELECT id FROM coupons WHERE code = $1 """, coupon_code, ) + # Если купон не найден, возвращаем False if not coupon_record: + logger.info(f"Купон {coupon_code} не найден в базе данных") return False - await conn.execute( + # Удаляем купон из базы данных + await session.execute( """ DELETE FROM coupons WHERE code = $1 """, coupon_code, ) + # Логируем успешное удаление купона + logger.info(f"Купон {coupon_code} успешно удален из базы данных") return True except Exception as e: - logger.error(f"Ошибка при удалении купона: {e}") + # Логируем ошибку на русском с подробным описанием + logger.error(f"Произошла ошибка при удалении купона {coupon_code}: {e}") return False - finally: - await conn.close() -async def restore_trial(tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) +async def restore_trial(tg_id: int, session: Any): + """ + Восстанавливает возможность использования триального периода для пользователя. + + Args: + tg_id (int): Telegram ID пользователя + session (Any): Сессия базы данных + + Returns: + bool: True, если триал успешно восстановлен, False в случае ошибки + """ try: - await conn.execute("UPDATE connections SET trial = 0 WHERE tg_id = $1", tg_id) + await session.execute( + """ + INSERT INTO connections (tg_id, trial) + VALUES ($1, 0) + ON CONFLICT (tg_id) + DO UPDATE SET trial = 0 + """, + tg_id, + ) + logger.info(f"Триальный период успешно восстановлен для пользователя {tg_id}") + return True except Exception as e: - - logger.error(f"Ошибка при установке значения триала: {e}") - finally: - await conn.close() + logger.error(f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}") + return False -async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - INSERT INTO connections (tg_id, balance, trial) - VALUES ($1, $2, $3) - """, - tg_id, - balance, - trial, - ) - await conn.close() +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): + """ + Добавляет новое подключение для пользователя в базу данных. + + Args: + tg_id (int): Telegram ID пользователя + balance (float, optional): Начальный баланс пользователя. По умолчанию 0.0. + trial (int, optional): Статус триального периода. По умолчанию 0. + session (Any, optional): Сессия базы данных. + + Raises: + Exception: Если возникает ошибка при добавлении подключения в базу данных. + """ + try: + await session.execute( + """ + INSERT INTO connections (tg_id, balance, trial) + VALUES ($1, $2, $3) + """, + tg_id, + balance, + trial, + ) + logger.info( + f"Успешно добавлено новое подключение для пользователя {tg_id} с балансом {balance} и статусом триала {trial}" + ) + except Exception as e: + logger.error(f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}") + raise async def check_connection_exists(tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) - exists = await conn.fetchval( - """ - SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1) - """, - tg_id, - ) - await conn.close() - return exists + """ + Проверяет существование подключения для указанного пользователя в базе данных. + + Args: + tg_id (int): Telegram ID пользователя для проверки. + + Returns: + bool: True, если подключение существует, иначе False. + + Raises: + Exception: В случае ошибки при подключении к базе данных. + """ + try: + conn = await asyncpg.connect(DATABASE_URL) + exists = await conn.fetchval( + """ + SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1) + """, + tg_id, + ) + logger.info( + f"Проверка существования подключения для пользователя {tg_id}: {'найдено' if exists else 'не найдено'}" + ) + return exists + except Exception as e: + logger.error(f"Ошибка при проверке подключения для пользователя {tg_id}: {e}") + raise + finally: + if conn: + await conn.close() -async def store_key( - tg_id: int, - client_id: str, - email: str, - expiry_time: int, - key: str, - server_id: str, -): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id) - VALUES ($1, $2, $3, $4, $5, $6, $7) - """, - tg_id, - client_id, - email, - int(datetime.utcnow().timestamp() * 1000), - expiry_time, - key, - server_id, - ) - await conn.close() +async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str, session: Any): + """ + Сохраняет информацию о ключе в базу данных. + + Args: + tg_id (int): Telegram ID пользователя + client_id (str): Уникальный идентификатор клиента + email (str): Электронная почта или имя устройства + expiry_time (int): Время истечения ключа в миллисекундах + key (str): Ключ доступа + server_id (str): Идентификатор сервера + + Raises: + Exception: Если возникает ошибка при сохранении ключа в базу данных + """ + try: + await session.execute( + """ + INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id) + VALUES ($1, $2, $3, $4, $5, $6, $7) + """, + tg_id, + client_id, + email, + int(datetime.utcnow().timestamp() * 1000), + expiry_time, + key, + 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): - conn = await asyncpg.connect(DATABASE_URL) - records = await conn.fetch( - """ - SELECT client_id, email, created_at, key - FROM keys - WHERE tg_id = $1 - """, - tg_id, - ) - await conn.close() - return records + """ + Получает список ключей для указанного пользователя. + + Args: + tg_id (int): Telegram ID пользователя + + Returns: + list: Список записей ключей с информацией о клиенте, электронной почте, времени создания и ключе + + 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 + """, + tg_id, + ) + logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id}") + return records + 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): - 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, - ) - await conn.close() - return records + """ + Получает список ключей для указанного пользователя на определенном сервере. + + Args: + tg_id (int): Telegram ID пользователя + server_id (str): Идентификатор сервера + + Returns: + list: Список записей ключей с информацией о клиенте, электронной почте, времени создания и ключе + + 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}") + return records + except Exception as e: + logger.error(f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}") + raise + finally: + if conn: + await conn.close() async def has_active_key(tg_id: int) -> bool: - conn = await asyncpg.connect(DATABASE_URL) - count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - await conn.close() - return count > 0 + """ + Проверяет наличие активных ключей для указанного пользователя. + + 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: - conn = await asyncpg.connect(DATABASE_URL) - balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - await conn.close() - return balance if balance is not None else 0.0 + """ + Получает баланс пользователя из базы данных. + + Args: + tg_id (int): Telegram ID пользователя + + Returns: + float: Баланс пользователя, 0.0 если баланс не найден + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + 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 + except Exception as e: + logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}") + return 0.0 + finally: + if conn: + await conn.close() async def update_balance(tg_id: int, amount: float): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - UPDATE connections - SET balance = balance + $1 - WHERE tg_id = $2 - """, - amount, - tg_id, - ) + """ + Обновляет баланс пользователя в базе данных. - await handle_referral_on_balance_update(tg_id, amount) + Args: + tg_id (int): Telegram ID пользователя + amount (float): Сумма для обновления баланса - await conn.close() + Raises: + Exception: В случае ошибки при подключении к базе данных или обновлении баланса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute( + """ + UPDATE connections + SET balance = balance + $1 + WHERE tg_id = $2 + """, + amount, + tg_id, + ) + logger.info(f"Баланс пользователя {tg_id} обновлен на сумму {amount}") + + await handle_referral_on_balance_update(tg_id, amount) + + except Exception as e: + logger.error(f"Ошибка при обновлении баланса для пользователя {tg_id}: {e}") + raise + finally: + if conn: + await conn.close() -async def get_trial(tg_id: int) -> int: - conn = await asyncpg.connect(DATABASE_URL) - trial = await conn.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) - await conn.close() - return trial if trial is not None else 0 +async def get_trial(tg_id: int, session: Any) -> int: + """ + Получает статус триала для пользователя из базы данных. + + Args: + tg_id (int): Telegram ID пользователя + session (Any): Сессия базы данных + + Returns: + int: Статус триала (0 - не использован, 1 - использован) + """ + try: + 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}") + return 0 async def get_key_count(tg_id: int) -> int: - conn = await asyncpg.connect(DATABASE_URL) - count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - await conn.close() - return count if count is not None else 0 + """ + Получает количество ключей для указанного пользователя. + + Args: + tg_id (int): Telegram ID пользователя + + Returns: + int: Количество ключей пользователя, 0 если ключей нет + + 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 if count is not None else 0 + except Exception as e: + logger.error(f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}") + return 0 + finally: + if conn: + await conn.close() async def get_all_users(conn): - return await conn.fetch("SELECT tg_id FROM connections") + """ + Получает список всех пользователей из базы данных. + + 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): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - INSERT INTO referrals (referred_tg_id, referrer_tg_id) - VALUES ($1, $2) - """, - referred_tg_id, - referrer_tg_id, - ) - await conn.close() +async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): + try: + # Вставка записи о реферале в базу данных + await session.execute( + """ + INSERT INTO referrals (referred_tg_id, referrer_tg_id) + VALUES ($1, $2) + """, + referred_tg_id, + referrer_tg_id, + ) + logger.info(f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}") + except Exception as e: + logger.error(f"Ошибка при добавлении реферала: {e}") + raise async def handle_referral_on_balance_update(tg_id: int, amount: float): - conn = await asyncpg.connect(DATABASE_URL) + """ + Обработка многоуровневой реферальной системы при обновлении баланса пользователя. - referral = await conn.fetchrow( - """ - SELECT referrer_tg_id FROM referrals WHERE referred_tg_id = $1 - """, - tg_id, - ) + Метод анализирует цепочку рефералов для указанного пользователя и начисляет + бонусы рефереерам на разных уровнях согласно настроенным процентам. - if referral: - referrer_tg_id = referral["referrer_tg_id"] + Args: + tg_id (int): Идентификатор Telegram пользователя, пополнившего баланс + amount (float): Сумма пополнения баланса - bonus = amount * BONUS_PERCENT + Raises: + Exception: В случае ошибки при работе с базой данных + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Начало обработки реферальной системы для пользователя {tg_id}") - if bonus < 0: - bonus = 0 + # Максимальное количество уровней рефералов + MAX_REFERRAL_LEVELS = len(REFERRAL_BONUS_PERCENTAGES.keys()) - await update_balance(referrer_tg_id, bonus) + # Текущий уровень для отслеживания + current_tg_id = tg_id + referral_chain = [] - await conn.execute( - """ - UPDATE referrals SET reward_issued = TRUE - WHERE referrer_tg_id = $1 AND referred_tg_id = $2 - """, - referrer_tg_id, - tg_id, - ) + # Собираем цепочку рефералов + for level in range(1, MAX_REFERRAL_LEVELS + 1): + # Получаем информацию о рефере текущего уровня + referral = await conn.fetchrow( + """ + SELECT referrer_tg_id + FROM referrals + WHERE referred_tg_id = $1 + """, + current_tg_id, + ) - await conn.close() + # Если реферер не найден, прекращаем поиск + if not referral: + break + + referrer_tg_id = referral['referrer_tg_id'] + referral_chain.append({'tg_id': referrer_tg_id, 'level': level}) + + # Переходим к следующему уровню + current_tg_id = referrer_tg_id + + # Начисляем бонусы по цепочке рефералов + for referral in referral_chain: + referrer_tg_id = referral['tg_id'] + level = referral['level'] + + # Расчет бонуса для текущего уровня + bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0) + bonus = amount * bonus_percent + bonus = max(bonus, 0) # Гарантируем неотрицательный бонус + + if bonus > 0: + 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}") + finally: + if conn: + await conn.close() async def get_referral_stats(referrer_tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) - total_referrals = await conn.fetchval( - """ - SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 - """, - referrer_tg_id, - ) + """ + Получение подробной статистики рефералов для указанного пользователя. - active_referrals = await conn.fetchval( - """ - SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE - """, - referrer_tg_id, - ) + Args: + referrer_tg_id (int): Telegram ID пользователя, для которого запрашивается статистика рефералов. - await conn.close() + Returns: + dict: Словарь с детальной статистикой рефералов, содержащий: + - total_referrals (int): Общее количество рефералов + - active_referrals (int): Количество активных рефералов (с начисленным бонусом) + - referrals_by_level (dict): Количество рефералов по каждому уровню + - total_referral_bonus (float): Общая сумма бонусов от рефералов - return { - "total_referrals": total_referrals, - "active_referrals": active_referrals, - } + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info( + f"Установлено подключение к базе данных для получения статистики рефералов пользователя {referrer_tg_id}" + ) + + # Общее количество рефералов + total_referrals = await conn.fetchval( + """ + SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 + """, + referrer_tg_id, + ) + logger.debug(f"Получено общее количество рефералов: {total_referrals}") + + # Активные рефералы + active_referrals = await conn.fetchval( + """ + SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE + """, + referrer_tg_id, + ) + logger.debug(f"Получено количество активных рефералов: {active_referrals}") + + # Рефералы по уровням + referrals_by_level_records = await conn.fetch( + """ + WITH RECURSIVE referral_levels AS ( + SELECT referred_tg_id, referrer_tg_id, 1 AS level + FROM referrals + WHERE referrer_tg_id = $1 + + UNION + + SELECT r.referred_tg_id, r.referrer_tg_id, rl.level + 1 + FROM referrals r + JOIN referral_levels rl ON r.referrer_tg_id = rl.referred_tg_id + WHERE rl.level < 5 + ) + SELECT level, + COUNT(*) AS level_count, + COUNT(CASE WHEN reward_issued = TRUE THEN 1 END) AS active_level_count + FROM referral_levels rl + JOIN referrals r ON rl.referred_tg_id = r.referred_tg_id + GROUP BY level + ORDER BY level + """, + referrer_tg_id, + ) + + # Преобразование результатов в словарь + referrals_by_level = { + record['level']: {'total': record['level_count'], 'active': record['active_level_count']} + for record in referrals_by_level_records + } + logger.debug(f"Получена статистика рефералов по уровням: {referrals_by_level}") + + # Общая сумма бонусов от рефералов + total_referral_bonus = await conn.fetchval( + """ + SELECT COALESCE(SUM(amount), 0) + FROM payments + WHERE tg_id IN ( + SELECT referred_tg_id + FROM referrals + WHERE referrer_tg_id = $1 + ) AND status = 'success' + """, + referrer_tg_id, + ) + logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}") + + return { + "total_referrals": total_referrals, + "active_referrals": active_referrals, + "referrals_by_level": referrals_by_level, + "total_referral_bonus": total_referral_bonus, + } + + except Exception as e: + logger.error(f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}") + raise + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def update_key_expiry(client_id: str, new_expiry_time: int): """ - Обновление времени истечения ключа на новое значение. + Обновление времени истечения ключа для указанного клиента. + + Args: + client_id (str): Уникальный идентификатор клиента + new_expiry_time (int): Новое время истечения ключа + + Raises: + Exception: В случае ошибки при подключении к базе данных или обновлении ключа """ - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - UPDATE keys - SET expiry_time = $1, notified = FALSE, notified_24h = FALSE - WHERE client_id = $2 - """, - new_expiry_time, - client_id, - ) - await conn.close() + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}") + + await conn.execute( + """ + UPDATE keys + SET expiry_time = $1, notified = FALSE, notified_24h = FALSE + WHERE client_id = $2 + """, + new_expiry_time, + 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 delete_key(client_id: str): """ - Удаление ключа из базы данных. + Удаление ключа из базы данных для указанного клиента. + + Args: + client_id (str): Уникальный идентификатор клиента, ключ которого будет удален + + Raises: + Exception: В случае ошибки при подключении к базе данных или удалении ключа """ - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - DELETE FROM keys - WHERE client_id = $1 - """, - client_id, - ) - await conn.close() + 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): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - UPDATE connections - SET balance = balance + $1 - WHERE tg_id = $2 - """, - amount, - client_id, - ) - await conn.close() + """ + Добавление баланса клиенту по его идентификатору Telegram. + + Args: + client_id (str): Идентификатор клиента в Telegram + amount (float): Сумма для пополнения баланса + + Raises: + Exception: В случае ошибки при подключении к базе данных или обновлении баланса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}") + + await conn.execute( + """ + UPDATE connections + SET balance = balance + $1 + WHERE tg_id = $2 + """, + amount, + client_id, + ) + logger.info(f"Успешно пополнен баланс клиента {client_id} на сумму {amount}") + + except Exception as e: + logger.error(f"Ошибка при пополнении баланса для клиента {client_id}: {e}") + raise + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def get_client_id_by_email(email: str): """ - Получение client_id по email. + Получение идентификатора клиента по электронной почте. + + Args: + email (str): Электронная почта клиента + + Returns: + str: Идентификатор клиента или None, если клиент не найден + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса """ - conn = await asyncpg.connect(DATABASE_URL) - client_id = await conn.fetchval( - """ - SELECT client_id FROM keys WHERE email = $1 - """, - email, - ) - await conn.close() - return client_id + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для поиска client_id по email: {email}") + + client_id = await conn.fetchval( + """ + SELECT client_id FROM keys WHERE email = $1 + """, + email, + ) + + if client_id: + logger.info(f"Найден client_id для email: {email}") + else: + logger.warning(f"Не найден client_id для email: {email}") + + return client_id + + except Exception as e: + logger.error(f"Ошибка при получении client_id для email {email}: {e}") + raise + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def get_tg_id_by_client_id(client_id: str): - conn = await asyncpg.connect(DATABASE_URL) + """ + Получение Telegram ID по идентификатору клиента. + + Args: + client_id (str): Идентификатор клиента + + Returns: + int или None: Telegram ID клиента, если найден, иначе None + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}") + result = await conn.fetchrow("SELECT tg_id FROM keys WHERE client_id = $1", client_id) - return result["tg_id"] if result else None + + if result: + logger.info(f"Найден Telegram ID для client_id: {client_id}") + return result["tg_id"] + else: + logger.warning(f"Не найден Telegram ID для client_id: {client_id}") + return None + + except Exception as e: + logger.error(f"Ошибка при получении Telegram ID для client_id {client_id}: {e}") + raise finally: - await conn.close() + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def upsert_user( @@ -465,8 +1003,25 @@ async def upsert_user( language_code: str = None, is_bot: bool = False, ): - conn = await asyncpg.connect(DATABASE_URL) + """ + Обновляет или вставляет информацию о пользователе в базу данных. + + Args: + tg_id (int): Идентификатор пользователя в Telegram + username (str, optional): Имя пользователя в Telegram + first_name (str, optional): Имя пользователя + last_name (str, optional): Фамилия пользователя + language_code (str, optional): Код языка пользователя + is_bot (bool, optional): Флаг, указывающий является ли пользователь ботом + + Raises: + Exception: В случае ошибки при работе с базой данных + """ + conn = None try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}") + await conn.execute( """ INSERT INTO users (tg_id, username, first_name, last_name, language_code, is_bot, created_at, updated_at) @@ -487,13 +1042,33 @@ async def upsert_user( language_code, is_bot, ) + logger.info(f"Успешно обновлена информация о пользователе {tg_id}") + except Exception as e: + logger.error(f"Ошибка при обновлении информации о пользователе {tg_id}: {e}") + raise finally: - await conn.close() + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def add_payment(tg_id: int, amount: float, payment_system: str): - conn = await asyncpg.connect(DATABASE_URL) + """ + Добавляет информацию о платеже в базу данных. + + Args: + tg_id (int): Идентификатор пользователя в Telegram + amount (float): Сумма платежа + payment_system (str): Система оплаты + + Raises: + Exception: В случае ошибки при добавлении платежа + """ + conn = None try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}") + await conn.execute( """ INSERT INTO payments (tg_id, amount, payment_system, status) @@ -503,7 +1078,100 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): amount, payment_system, ) + logger.info(f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}") except Exception as e: - logger.error(f"Ошибка при добавлении платежа: {e}") + logger.error(f"Ошибка при добавлении платежа для пользователя {tg_id}: {e}") + raise finally: - await conn.close() + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных после добавления платежа") + + +async def add_notification(tg_id: int, notification_type: str, session: Any): + """ + Добавляет запись о notification в базу данных. + + Args: + tg_id (int): Идентификатор пользователя в Telegram + notification_type (str): Тип уведомления + session (Any): Сессия базы данных для выполнения запроса + + Raises: + Exception: В случае ошибки при добавлении notification + """ + try: + await session.execute( + """ + INSERT INTO notifications (tg_id, notification_type) + VALUES ($1, $2) + ON CONFLICT (tg_id, notification_type) + DO UPDATE SET last_notification_time = NOW() + """, + tg_id, + notification_type, + ) + logger.info(f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}") + except Exception as 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: + """ + Проверяет, прошло ли указанное количество часов с момента последнего уведомления. + + Args: + tg_id (int): Идентификатор пользователя в Telegram + notification_type (str): Тип уведомления + hours (int, optional): Количество часов для проверки. По умолчанию 12. + session (Any): Сессия базы данных для выполнения запроса + + Returns: + bool: True, если с момента последнего уведомления прошло больше указанного времени, иначе False + + Raises: + Exception: В случае ошибки при проверке времени уведомления + """ + try: + # Если сессия не передана, создаем новое подключение + if session is None: + conn = await asyncpg.connect(DATABASE_URL) + else: + conn = session + + result = await conn.fetchrow( + """ + SELECT + CASE + WHEN last_notification_time IS NULL THEN TRUE + WHEN NOW() - last_notification_time > INTERVAL '$1 hours' THEN TRUE + ELSE FALSE + END as can_notify + FROM notifications + WHERE tg_id = $2 AND notification_type = $3 + """, + hours, # Убираем преобразование в строку + tg_id, + notification_type, + ) + + # Если сессия не была передана, закрываем подключение + if session is None and conn: + await conn.close() + + # Если записи нет, значит уведомление можно отправить + if result is None: + return True + + can_notify = result['can_notify'] + + logger.info( + f"Проверка уведомления типа {notification_type} для пользователя {tg_id}: {'можно отправить' if can_notify else 'слишком рано'}" + ) + + return can_notify + + except Exception as e: + logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}") + return True # По умолчанию разрешаем отправку уведомления diff --git a/filters/admin.py b/filters/admin.py index 17eddb73..b902294b 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -10,11 +10,8 @@ class IsAdminFilter(BaseFilter): async def __call__(self, message: Message) -> bool: try: admin_ids: Union[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/handlers/admin/admin_commands.py b/handlers/admin/admin_commands.py deleted file mode 100644 index c5454fc5..00000000 --- a/handlers/admin/admin_commands.py +++ /dev/null @@ -1,156 +0,0 @@ -from aiogram import Router, types -from aiogram.filters import Command -from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import State, StatesGroup -from aiogram.types import InlineKeyboardButton -from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg - -from bot import bot -from config import DATABASE_URL -from database import add_balance_to_client, check_connection_exists -from filters.admin import IsAdminFilter -from handlers.texts import TRIAL -from logger import logger - -router = Router() - - -class Form(StatesGroup): - waiting_for_server_selection = State() - waiting_for_key_name = State() - viewing_profile = State() - waiting_for_message = State() - - -@router.message(Command("add_balance"), IsAdminFilter()) -async def cmd_add_balance(message: types.Message): - try: - _, client_id, amount = message.text.split() - amount = float(amount) - - if not await check_connection_exists(int(client_id)): - await message.reply(f"❌ Клиент с ID {client_id} не найден в базе данных.") - return - - await add_balance_to_client(int(client_id), amount) - await message.reply(f"✅ Баланс клиента {client_id} успешно пополнен на {amount}") - except ValueError: - await message.reply( - "❓ Неверный формат команды!\n" - "Пожалуйста, используйте следующий шаблон:\n" - "/add_balance <сумма пополнения>" - ) - except Exception as e: - await message.reply(f"🚨 Произошла непредвиденная ошибка: {e}") - - -@router.message(Command("backup"), IsAdminFilter()) -async def backup_command(message: types.Message): - from backup import backup_database - - await message.answer("🔄 Инициализация резервного копирования базы данных...") - await backup_database() - await message.answer("✅ Бэкап базы данных успешно завершен и отправлен администратору.") - - -@router.message(Command("send_trial"), IsAdminFilter()) -async def handle_send_trial_command(message: types.Message, state: FSMContext): - try: - conn = await asyncpg.connect(DATABASE_URL) - try: - records = await conn.fetch( - """ - SELECT tg_id FROM connections WHERE trial = 0 - """ - ) - - if records: - success_count = 0 - error_count = 0 - blocked_count = 0 - - for record in records: - tg_id = record["tg_id"] - trial_message = TRIAL - try: - await bot.send_message(chat_id=tg_id, text=trial_message) - success_count += 1 - except Exception as e: - if "Forbidden: bot was blocked by the user" in str(e): - blocked_count += 1 - logger.info(f"🚫 Бот заблокирован пользователем с tg_id: {tg_id}") - else: - error_count += 1 - logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}") - - await message.answer( - f"📊 Результаты рассылки пробных периодов:\n" - f"✅ Успешно отправлено: {success_count}\n" - f"🚫 Заблокировано: {blocked_count}\n" - f"❌ Ошибок: {error_count}" - ) - else: - await message.answer("📭 Нет пользователей с неиспользованными пробными ключами.") - - finally: - await conn.close() - - except Exception as e: - await message.answer(f"❗ Ошибка при отправке сообщений: {e}") - - -@router.message(Command("send_to_all"), IsAdminFilter()) -async def send_message_to_all_clients(message: types.Message, state: FSMContext, from_panel=False): - try: - await message.delete() - except Exception: - pass - - if from_panel: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await message.answer( - "✍️ Введите текст сообщения, который вы хотите отправить всем клиентам:", - reply_markup=builder.as_markup(), - ) - await state.set_state(Form.waiting_for_message) - - -@router.message(Form.waiting_for_message, IsAdminFilter()) -async def process_message_to_all( - message: types.Message, - state: FSMContext, -): - text_message = message.text - - try: - conn = await asyncpg.connect(DATABASE_URL) - tg_ids = await conn.fetch("SELECT tg_id FROM connections") - - total_users = len(tg_ids) - success_count = 0 - error_count = 0 - - for record in tg_ids: - tg_id = record["tg_id"] - try: - await bot.send_message(chat_id=tg_id, text=text_message) - success_count += 1 - except Exception as e: - error_count += 1 - logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}") - - await message.answer( - f"📤 Рассылка завершена:\n" - f"👥 Всего пользователей: {total_users}\n" - f"✅ Успешно отправлено: {success_count}\n" - f"❌ Не доставлено: {error_count}" - ) - except Exception as e: - logger.error(f"❗ Ошибка при подключении к базе данных: {e}") - await message.answer("❌ Произошла ошибка при отправке сообщения.") - finally: - await conn.close() - - await state.clear() diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index a706a78d..5ea3ca98 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -1,3 +1,5 @@ +from typing import Any + from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup @@ -18,44 +20,27 @@ router = Router() @router.callback_query(F.data == "coupons_editor", IsAdminFilter()) async def show_coupon_management_menu(callback_query: types.CallbackQuery, state: FSMContext): - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - finally: - await state.clear() - + await state.clear() builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon")) builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - - markup = builder.as_markup() - await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=markup) - await callback_query.answer() + await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=builder.as_markup()) @router.callback_query(F.data == "coupons", IsAdminFilter()) -async def show_coupon_list(callback_query: types.CallbackQuery): +async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): try: - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - - coupons = await get_all_coupons() + coupons = await get_all_coupons(session) if not coupons: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - markup = builder.as_markup() await callback_query.message.answer( - "❌ На данный момент нет доступных купонов.\n" "Вы можете вернуться в меню управления.", - parse_mode="HTML", - reply_markup=markup, + "❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙", + reply_markup=builder.as_markup(), ) - await callback_query.answer() return coupon_list = "📜 Список всех купонов:\n\n" @@ -63,10 +48,10 @@ async def show_coupon_list(callback_query: types.CallbackQuery): for coupon in coupons: coupon_list += ( - f"Код: {coupon['code']}\n" - f"Сумма: {coupon['amount']} рублей\n" - f"Лимит использования: {coupon['usage_limit']} раз\n" - f"Использовано: {coupon['usage_count']} раз\n\n" + f"🏷️ Код: {coupon['code']}\n" + f"💰 Сумма: {coupon['amount']} рублей\n" + f"🔢 Лимит использования: {coupon['usage_limit']} раз\n" + f"✅ Использовано: {coupon['usage_count']} раз\n\n" ) builder.row( @@ -77,85 +62,60 @@ async def show_coupon_list(callback_query: types.CallbackQuery): ) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - - markup = builder.as_markup() - await callback_query.message.answer(coupon_list, parse_mode="HTML", reply_markup=markup) + await callback_query.message.answer(coupon_list, reply_markup=builder.as_markup()) except Exception as e: logger.error(f"Ошибка при получении списка купонов: {e}") - await callback_query.message.answer( - f"❌ Произошла ошибка при получении списка купонов: {e}", - parse_mode="HTML", - ) - await callback_query.answer() @router.callback_query(F.data.startswith("delete_coupon_"), IsAdminFilter()) -async def handle_delete_coupon(callback_query: types.CallbackQuery): +async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any): coupon_code = callback_query.data[len("delete_coupon_") :] try: - result = await delete_coupon_from_db(coupon_code) + result = await delete_coupon_from_db(coupon_code, session) if result: - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - - await show_coupon_list(callback_query) + await show_coupon_list(callback_query, session) else: await callback_query.message.answer( f"❌ Купон с кодом {coupon_code} не найден.", - parse_mode="HTML", ) - await show_coupon_list(callback_query) + await show_coupon_list(callback_query, session) except Exception as e: logger.error(f"Ошибка при удалении купона: {e}") - await callback_query.message.answer(f"❌ Произошла ошибка при удалении купона: {e}", parse_mode="HTML") - await callback_query.answer() @router.callback_query(F.data == "create_coupon", IsAdminFilter()) async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext): - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - markup = builder.as_markup() await callback_query.message.answer( - "Введите данные для создания купона в формате:\n\n" - "код сумма лимит\n\n" - "Пример: 'COUPON1 50 5'\n\n", - parse_mode="HTML", - reply_markup=markup, + "🎫 Введите данные для создания купона в формате:\n\n" + "📝 код 💰 сумма 🔢 лимит\n\n" + "Пример: 'COUPON1 50 5' 👈\n\n", + reply_markup=builder.as_markup(), ) await state.set_state(AdminCouponsState.waiting_for_coupon_data) - await callback_query.answer() @router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter()) -async def process_coupon_data(message: types.Message, state: FSMContext): +async def process_coupon_data(message: types.Message, state: FSMContext, session: Any): text = message.text.strip() parts = text.split() builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - markup = builder.as_markup() if len(parts) != 3: await message.answer( - "❌ Некорректный формат! Пожалуйста, введите данные в формате:\n" - "код сумма лимит\n" - "Пример: 'COUPON1 50 5'", - parse_mode="HTML", - reply_markup=markup, + "❌ Некорректный формат! 📝 Пожалуйста, введите данные в формате:\n" + "🏷️ код 💰 сумма 🔢 лимит\n" + "Пример: 'COUPON1 50 5' 👈", + reply_markup=builder.as_markup(), ) return @@ -165,41 +125,26 @@ async def process_coupon_data(message: types.Message, state: FSMContext): usage_limit = int(parts[2]) except ValueError: await message.answer( - "⚠️ Проверьте правильность введенных данных.\n" "Сумма должна быть числом, а лимит — целым числом.", - parse_mode="HTML", - reply_markup=markup, + "⚠️ Проверьте правильность введенных данных!\n" + "💱 Сумма должна быть числом, 🔢 а лимит — целым числом.", + reply_markup=builder.as_markup(), ) return try: - await create_coupon(coupon_code, coupon_amount, usage_limit) + await create_coupon(coupon_code, coupon_amount, usage_limit, session) result_message = ( - f"✅ Купон с кодом {coupon_code} успешно создан!\n" - f"Сумма: {coupon_amount} рублей\n" - f"Лимит использования: {usage_limit} раз." + f"✅ Купон с кодом {coupon_code} успешно создан! 🎉\n" + f"Сумма: {coupon_amount} рублей 💰\n" + f"Лимит использования: {usage_limit} раз 🔢." ) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - markup = builder.as_markup() - - try: - await message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - - await message.answer(result_message, parse_mode="HTML", reply_markup=markup) + await message.answer(result_message, reply_markup=builder.as_markup()) await state.clear() except Exception as e: logger.error(f"Ошибка при создании купона: {e}") - await message.answer(f"❌ Ошибка при создании купона: {e}", parse_mode="HTML") - - -@router.callback_query(F.data == "back_to_coupons_menu") -async def back_to_coupons_menu(callback_query: types.CallbackQuery, state: FSMContext): - """Возвращаем пользователя в меню управления купонами""" - await state.clear() - await show_coupon_management_menu(callback_query) diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 1fe8d873..ce7466f6 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -1,19 +1,18 @@ from datetime import datetime import subprocess +from typing import Any from aiogram import F, Router, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, InlineKeyboardButton, Message +from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg from backup import backup_database from bot import bot -from config import DATABASE_URL from filters.admin import IsAdminFilter -from handlers.admin.admin_commands import send_message_to_all_clients +from logger import logger router = Router() @@ -22,6 +21,7 @@ class UserEditorState(StatesGroup): waiting_for_tg_id = State() displaying_user_info = State() waiting_for_restart_confirmation = State() + waiting_for_message = State() @router.callback_query(F.data == "admin", IsAdminFilter()) @@ -33,11 +33,6 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC async def handle_admin_message(message: types.Message, state: FSMContext): await state.clear() - try: - await message.delete() - except Exception: - pass - builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="user_stats")) builder.row(InlineKeyboardButton(text="👥 Управление пользователями", callback_data="user_editor")) @@ -45,31 +40,26 @@ async def handle_admin_message(message: types.Message, state: FSMContext): builder.row(InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")) builder.row(InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")) builder.row(InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")) - builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")) - await bot.send_message( - message.chat.id, - "🤖 Панель администратора", - reply_markup=builder.as_markup(), - ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await message.answer("🤖 Панель администратора", reply_markup=builder.as_markup()) @router.callback_query(F.data == "user_stats", IsAdminFilter()) -async def user_stats_menu(callback_query: CallbackQuery): - conn = await asyncpg.connect(DATABASE_URL) +async def user_stats_menu(callback_query: CallbackQuery, session: Any): try: - total_users = await conn.fetchval("SELECT COUNT(*) FROM connections") - total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys") - total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals") + total_users = await session.fetchval("SELECT COUNT(*) FROM connections") + total_keys = await session.fetchval("SELECT COUNT(*) FROM keys") + total_referrals = await session.fetchval("SELECT COUNT(*) FROM referrals") - total_payments_today = await conn.fetchval( + total_payments_today = await session.fetchval( "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= CURRENT_DATE" ) - total_payments_week = await conn.fetchval( + total_payments_week = await session.fetchval( "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)" ) - total_payments_all_time = await conn.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments") + total_payments_all_time = await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments") - active_keys = await conn.fetchval( + active_keys = await session.fetchval( "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", int(datetime.utcnow().timestamp() * 1000), ) @@ -94,22 +84,59 @@ async def user_stats_menu(callback_query: CallbackQuery): builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats")) builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) - await callback_query.message.edit_text(stats_message, reply_markup=builder.as_markup(), parse_mode="HTML") - finally: - await conn.close() + await callback_query.message.answer(stats_message, reply_markup=builder.as_markup()) + except Exception as e: + logger.error(f"Error in user_stats_menu: {e}") @router.callback_query(F.data == "send_to_alls", IsAdminFilter()) async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): - await send_message_to_all_clients(callback_query.message, state, from_panel=True) - await callback_query.answer() + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) + await callback_query.message.answer( + "✍️ Введите текст сообщения, который вы хотите отправить всем клиентам 📢🌐:", + reply_markup=builder.as_markup(), + ) + await state.set_state(UserEditorState.waiting_for_message) + + +@router.message(UserEditorState.waiting_for_message, IsAdminFilter()) +async def process_message_to_all(message: types.Message, state: FSMContext, session: Any): + text_message = message.text + + try: + tg_ids = await session.fetch("SELECT tg_id FROM connections") + + total_users = len(tg_ids) + success_count = 0 + error_count = 0 + + for record in tg_ids: + tg_id = record["tg_id"] + try: + await bot.send_message(chat_id=tg_id, text=text_message) + success_count += 1 + except Exception as e: + error_count += 1 + logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}") + + await message.answer( + f"📤 Рассылка завершена:\n" + f"👥 Всего пользователей: {total_users}\n" + f"✅ Успешно отправлено: {success_count}\n" + f"❌ Не доставлено: {error_count}" + ) + except Exception as e: + logger.error(f"❗ Ошибка при подключении к базе данных: {e}") + + await state.clear() @router.callback_query(F.data == "backups", IsAdminFilter()) -async def handle_backup(message: Message): - await message.answer("💾 Инициализация резервного копирования базы данных...") +async def handle_backup(callback_query: CallbackQuery, state: FSMContext): + await callback_query.message.answer("💾 Инициализация резервного копирования базы данных...") await backup_database() - await message.answer("✅ Резервная копия успешно создана и отправлена администратору.") + await callback_query.message.answer("✅ Резервная копия успешно создана и отправлена администратору.") @router.callback_query(F.data == "restart_bot", IsAdminFilter()) @@ -121,7 +148,7 @@ async def handle_restart(callback_query: CallbackQuery, state: FSMContext): InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"), ) builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) - await callback_query.message.edit_text( + await callback_query.message.answer( "🤔 Вы уверены, что хотите перезапустить бота?", reply_markup=builder.as_markup(), ) @@ -143,16 +170,13 @@ async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext): text=True, ) await state.clear() - await callback_query.message.edit_text("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) + await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) except subprocess.CalledProcessError: - await callback_query.message.edit_text("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) + await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) except Exception as e: - await callback_query.message.edit_text( - f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", - reply_markup=builder.as_markup(), + await callback_query.message.answer( + f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", reply_markup=builder.as_markup() ) - finally: - await callback_query.answer() @router.callback_query(F.data == "user_editor", IsAdminFilter()) @@ -167,11 +191,4 @@ async def user_editor_menu(callback_query: CallbackQuery): builder.row(InlineKeyboardButton(text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id")) builder.row(InlineKeyboardButton(text="🌐 Поиск по Username", callback_data="search_by_username")) builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin")) - await callback_query.message.edit_text( - "👇 Выберите способ поиска пользователя:", - reply_markup=builder.as_markup(), - ) - - -async def handle_error(tg_id, callback_query, message): - await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id) + await callback_query.message.answer("👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup()) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 94311599..c485963f 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -1,18 +1,17 @@ import asyncio from datetime import datetime +from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg -from bot import bot -from config import CLUSTERS, DATABASE_URL, TOTAL_GB +from config import CLUSTERS, TOTAL_GB from database import get_client_id_by_email, restore_trial, update_key_expiry from filters.admin import IsAdminFilter -from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster +from handlers.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster from handlers.utils import sanitize_key_name from logger import logger @@ -30,137 +29,126 @@ class UserEditorState(StatesGroup): @router.callback_query(F.data == "search_by_tg_id", IsAdminFilter()) async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): - await callback_query.message.edit_text("🔍 Введите Telegram ID клиента:") + await callback_query.message.answer("🔍 Введите Telegram ID клиента:") 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): - await callback_query.message.edit_text("🔍 Введите Username клиента:") + await callback_query.message.answer("🔍 Введите Username клиента:") 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): +async def handle_username_input(message: types.Message, state: FSMContext, session: Any): username = message.text.strip() - conn = await asyncpg.connect(DATABASE_URL) - try: - user_record = await conn.fetchrow("SELECT tg_id FROM users WHERE username = $1", username) + user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username) - if not user_record: - await message.reply("🔍 Пользователь с указанным username не найден. 🚫") - await state.clear() - return + if not user_record: + await message.answer("🔍 Пользователь с указанным username не найден. 🚫") + await state.clear() + return - tg_id = user_record["tg_id"] - username = await conn.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - key_records = await conn.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + 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 session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) + referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) - if balance is None: - await message.reply("Пользователь с указанным tg_id не найден.") - await state.clear() - return + if balance is None: + await message.answer("🚫 Пользователь с указанным tg_id не найден. 🔍") + await state.clear() + return - builder = InlineKeyboardBuilder() + builder = InlineKeyboardBuilder() - for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) + 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"change_balance_{tg_id}", ) + ) - builder.row( - InlineKeyboardButton( - text="🔄 Восстановить пробник", - callback_data=f"restore_trial_{tg_id}", - ) + builder.row( + InlineKeyboardButton( + text="🔄 Восстановить пробник", + callback_data=f"restore_trial_{tg_id}", ) + ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + 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.reply(user_info, reply_markup=builder.as_markup(), parse_mode="HTML") - await state.set_state(UserEditorState.displaying_user_info) - - finally: - await conn.close() + 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.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter()) -async def handle_tg_id_input(message: types.Message, state: FSMContext): +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 session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) + referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) - conn = await asyncpg.connect(DATABASE_URL) - try: - username = await conn.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - key_records = await conn.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + if balance is None: + await message.answer("❌ Пользователь с указанным tg_id не найден. 🔍") + await state.clear() + return - if balance is None: - await message.reply("❌ Пользователь с указанным tg_id не найден. 🔍") - await state.clear() - return + builder = InlineKeyboardBuilder() - builder = InlineKeyboardBuilder() + for (email,) in key_records: + builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) - 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"change_balance_{tg_id}", ) + ) - builder.row( - InlineKeyboardButton( - text="🔄 Восстановить пробник", - callback_data=f"restore_trial_{tg_id}", - ) + builder.row( + InlineKeyboardButton( + text="🔄 Восстановить пробник", + callback_data=f"restore_trial_{tg_id}", ) + ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + 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.reply(user_info, reply_markup=builder.as_markup(), parse_mode="HTML") - await state.set_state(UserEditorState.displaying_user_info) - - finally: - await conn.close() + 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): +async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any): tg_id = int(callback_query.data.split("_")[2]) - await restore_trial(tg_id) + await restore_trial(tg_id, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin")) - await callback_query.message.edit_text("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()) + await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()) @router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter()) @@ -168,31 +156,129 @@ async def process_balance_change(callback_query: CallbackQuery, state: FSMContex tg_id = int(callback_query.data.split("_")[2]) await state.update_data(tg_id=tg_id) - await callback_query.message.edit_text("💸 Введите новую сумму баланса:") - await callback_query.answer() + await callback_query.message.answer("💸 Введите новую сумму баланса:") 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): +async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any): if not message.text.isdigit() or int(message.text) < 0: - await message.reply("❌ Пожалуйста, введите корректную сумму для изменения баланса.") + await message.answer("❌ Пожалуйста, введите корректную сумму для изменения баланса.") return new_balance = int(message.text) user_data = await state.get_data() tg_id = user_data.get("tg_id") - conn = await asyncpg.connect(DATABASE_URL) - try: - await conn.execute( - "UPDATE connections SET balance = $1 WHERE tg_id = $2", - new_balance, - 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() - response_message = f"✅ Баланс успешно изменен на {new_balance}." +async def get_key_details(email, session): + record = await session.fetchrow( + """ + SELECT k.key, k.expiry_time, k.server_id, 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 + + # Определение сервера + server_name = "Неизвестный сервер" + for cluster in CLUSTERS.values(): + if record['server_id'] in cluster: + server_name = cluster[record['server_id']].get("name", "Неизвестный сервер") + break + + # Расчет времени до истечения + expiry_date = datetime.utcfromtimestamp(record['expiry_time'] / 1000) + current_date = datetime.utcnow() + 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'], + 'expiry_date': expiry_date.strftime("%d %B %Y года"), + 'days_left_message': days_left_message, + 'server_name': server_name, + 'balance': record['balance'], + 'tg_id': record['tg_id'], + } + + +@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: + await callback_query.message.answer("🔍 Информация о ключе не найдена. 🚫") + 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"change_expiry|{email}", + ) + ) + builder.row( + InlineKeyboardButton( + text="❌ Удалить ключ", + callback_data=f"delete_key_admin|{email}", + ) + ) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) + + 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): + await callback_query.message.answer("🔑 Введите имя ключа:") + 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( @@ -200,202 +286,57 @@ async def handle_new_balance_input(message: types.Message, state: FSMContext): callback_data="admin", ) ) - await message.reply( - response_message, + + await message.answer( + "🚫 Пользователь с указанным именем ключа не найден.", reply_markup=builder.as_markup(), - parse_mode="HTML", ) + await state.clear() + return - finally: - await conn.close() + response_message = ( + f"🔑 Ключ: {key_details['key']}\n" + f"⏰ Дата истечения: {key_details['expiry_date']}\n" + f"💰 Баланс пользователя: {key_details['balance']}\n" + f"🌐 Сервер: {key_details['server_name']}" + ) - await state.clear() - - -@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter()) -async def process_key_edit(callback_query: CallbackQuery): - email = callback_query.data.split("_", 2)[2] - - try: - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.fetchrow( - """ - SELECT k.key, k.expiry_time, k.server_id - FROM keys k - WHERE k.email = $1 - """, - email, - ) - - if record: - key = record["key"] - expiry_time = record["expiry_time"] - server_id = record["server_id"] - server_name = "Неизвестный сервер" - - for cluster in CLUSTERS.values(): - if server_id in cluster: - server_name = cluster[server_id].get("name", "Неизвестный сервер") - break - - expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) - current_date = datetime.utcnow() - 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}" - - formatted_expiry_date = expiry_date.strftime("%d %B %Y года") - - response_message = ( - f"Ключ:
{key}
\n" - f"Дата истечения: {formatted_expiry_date}\n" - f"{days_left_message}\n" - f"Сервер: {server_name}" - ) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="⏳ Изменить время истечения", - callback_data=f"change_expiry|{email}", - ), - InlineKeyboardButton( - text="❌ Удалить ключ", - callback_data=f"delete_key_admin|{email}", - ), - ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await callback_query.message.edit_text( - response_message, - reply_markup=builder.as_markup(), - parse_mode="HTML", - ) - else: - await callback_query.message.edit_text("Информация о ключе не найдена.", parse_mode="HTML") - - finally: - await conn.close() - - except Exception as e: - logger.error(f"Ошибка при получении информации о ключе: {e}") - await callback_query.answer() - - -@router.callback_query(F.data == "search_by_key_name", IsAdminFilter()) -async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): - await callback_query.message.edit_text("🔑 Введите имя ключа:") - 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): - key_name = sanitize_key_name(message.text) - - conn = await asyncpg.connect(DATABASE_URL) - try: - user_records = await conn.fetch( - """ - SELECT c.tg_id, c.balance, k.email, k.key, k.expiry_time, k.server_id - FROM connections c - JOIN keys k ON c.tg_id = k.tg_id - WHERE k.email = $1 - """, - key_name, + key_buttons = InlineKeyboardBuilder() + key_buttons.row( + InlineKeyboardButton( + text="⏳ Изменить время истечения", + callback_data=f"change_expiry|{key_name}", ) - - if not user_records: - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад в меню администратора", - callback_data="admin", - ) - ) - - await message.reply( - "🚫 Пользователь с указанным именем ключа не найден.", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - response_messages = [] - key_buttons = InlineKeyboardBuilder() - - for record in user_records: - balance = record["balance"] - email = record["email"] - key = record["key"] - expiry_time = record["expiry_time"] - server_id = record["server_id"] - server_name = "Неизвестный сервер" - - for cluster in CLUSTERS.values(): - if server_id in cluster: - server_name = cluster[server_id].get("name", "Неизвестный сервер") - break - - expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime("%d %B %Y") - - response_messages.append( - f"🔑 Ключ:
{key}
\n" - f"⏰ Дата истечения: {expiry_date}\n" - f"💰 Баланс пользователя: {balance}\n" - f"🌐 Сервер: {server_name}" - ) - - key_buttons.row( - InlineKeyboardButton( - text="⏳ Изменить время истечения", - callback_data=f"change_expiry|{email}", - ) - ) - key_buttons.row( - InlineKeyboardButton( - text="❌ Удалить ключ", - callback_data=f"delete_key_admin|{email}", - ) - ) - - key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - - await message.reply( - "\n".join(response_messages), - reply_markup=key_buttons.as_markup(), - parse_mode="HTML", + ) + key_buttons.row( + InlineKeyboardButton( + text="❌ Удалить ключ", + callback_data=f"delete_key_admin|{key_name}", ) + ) + key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - finally: - await conn.close() - + 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.edit_text( - f"⏳ Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:", - parse_mode="HTML", + 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): +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: - await message.reply("Email не найден в состоянии.") + await message.answer("📧 Email не найден в состоянии. 🚫") await state.clear() return @@ -405,160 +346,97 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext): client_id = await get_client_id_by_email(email) if client_id is None: - await message.reply(f"Клиент с email {email} не найден.") + await message.answer(f"🚫 Клиент с email {email} не найден. 🔍") await state.clear() return - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id) - if not record: - await message.reply("Клиент не найден в базе данных.") - await state.clear() - return + record = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id) + if not record: + await message.answer("🚫 Клиент не найден в базе данных. 🔍") + await state.clear() + return - async def update_key_on_all_servers(): - tasks = [] - for cluster_id in CLUSTERS: - tasks.append( - asyncio.create_task( - renew_key_in_cluster( - cluster_id, - email, - client_id, - expiry_time, - total_gb=TOTAL_GB, - ) + async def update_key_on_all_servers(): + tasks = [] + for cluster_id in CLUSTERS: + tasks.append( + asyncio.create_task( + renew_key_in_cluster( + cluster_id, + email, + client_id, + expiry_time, + total_gb=TOTAL_GB, ) ) - await asyncio.gather(*tasks) + ) + await asyncio.gather(*tasks) - await update_key_on_all_servers() + await update_key_on_all_servers() - await update_key_expiry(client_id, expiry_time) + await update_key_expiry(client_id, expiry_time) - response_message = ( - f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." - ) - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await message.reply( - response_message, - reply_markup=builder.as_markup(), - parse_mode="HTML", - ) - - finally: - await conn.close() + 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: - await message.reply("❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.") + await message.answer("❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.") except Exception as e: - await message.reply(f"Произошла ошибка: {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): - tg_id = callback_query.from_user.id +async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any): email = callback_query.data.split("|")[1] + client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email) - conn = await asyncpg.connect(DATABASE_URL) - try: - client_id = await conn.fetchval("SELECT client_id FROM keys WHERE email = $1", email) - - if client_id is None: - await bot.edit_message_text( - "Ключ не найден.", - chat_id=tg_id, - message_id=callback_query.message.message_id, - ) - return - - builder = InlineKeyboardBuilder() - builder.row( - types.InlineKeyboardButton( - text="✅ Да, удалить", - callback_data=f"confirm_delete_admin|{client_id}", - ) + if client_id is None: + await callback_query.message.answer( + "🔍 Ключ не найден. 🚫", ) - builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys")) - await bot.edit_message_text( - "❓ Вы уверены, что хотите удалить ключ?", - chat_id=tg_id, - message_id=callback_query.message.message_id, - reply_markup=builder.as_markup(), - parse_mode="HTML", - ) - finally: - await conn.close() + return - await callback_query.answer() + builder = InlineKeyboardBuilder() + builder.row( + types.InlineKeyboardButton( + text="✅ Да, удалить", + callback_data=f"confirm_delete_admin|{client_id}", + ) + ) + builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys")) + 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): - tg_id = callback_query.from_user.id +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")) - try: - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id) + async def delete_key_from_servers(email, client_id): + tasks = [] + for cluster_id in CLUSTERS: + tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) + await asyncio.gather(*tasks) - if record: - email = record["email"] - response_message = "✅ Ключ успешно удален." - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) + await delete_key_from_servers(email, client_id) + await delete_key_from_db(client_id, session) - async def delete_key_from_servers(email, client_id): - tasks = [] - for cluster_id in CLUSTERS: - tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) - await asyncio.gather(*tasks) - - await delete_key_from_servers(email, client_id) - await delete_key_from_db(client_id) - - await bot.edit_message_text( - response_message, - chat_id=tg_id, - message_id=callback_query.message.message_id, - reply_markup=builder.as_markup(), - ) - else: - response_message = "🚫 Ключ не найден или уже удален." - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - await bot.edit_message_text( - response_message, - chat_id=tg_id, - message_id=callback_query.message.message_id, - reply_markup=builder.as_markup(), - ) - - finally: - await conn.close() - - except Exception as e: - await bot.edit_message_text( - f"Ошибка при удалении ключа: {e}", - chat_id=tg_id, - message_id=callback_query.message.message_id, - ) - - await callback_query.answer() - - -async def delete_key_from_db(client_id): - """Удаление ключа из базы данных""" - try: - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id) - except Exception as e: - logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}") - finally: - await conn.close() + 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()) diff --git a/handlers/commands.py b/handlers/commands.py deleted file mode 100644 index 3ecebb00..00000000 --- a/handlers/commands.py +++ /dev/null @@ -1,17 +0,0 @@ -from aiogram import Router, types -from aiogram.filters import Command -from aiogram.fsm.context import FSMContext - -from handlers.start import start_command - -router = Router() - - -@router.message(Command("start")) -async def handle_start(message: types.Message, state: FSMContext, admin: bool = False): - await start_command(message, admin) - - -@router.message(Command("menu")) -async def handle_menu(message: types.Message, state: FSMContext, admin: bool = False): - await start_command(message, admin) diff --git a/handlers/coupons.py b/handlers/coupons.py index c79b22a4..ed487671 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -1,15 +1,13 @@ from datetime import datetime +from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg -from config import DATABASE_URL from database import update_balance -from logger import logger class CouponActivationState(StatesGroup): @@ -21,98 +19,75 @@ router = Router() @router.callback_query(F.data == "activate_coupon") async def handle_activate_coupon(callback_query: types.CallbackQuery, state: FSMContext): - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( "🎫 Введите код купона:\n\n" "📝 Пожалуйста, введите действующий код купона, который вы хотите активировать. 🔑", - parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(CouponActivationState.waiting_for_coupon_code) - await callback_query.answer() @router.message(CouponActivationState.waiting_for_coupon_code) -async def process_coupon_code(message: types.Message, state: FSMContext): - try: - await message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - +async def process_coupon_code(message: types.Message, state: FSMContext, session: Any): coupon_code = message.text.strip() - activation_result = await activate_coupon(message.from_user.id, coupon_code) + activation_result = await activate_coupon(message.chat.id, coupon_code, session) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer(activation_result, reply_markup=builder.as_markup(), parse_mode="HTML") await state.clear() -async def activate_coupon(user_id: int, coupon_code: str): - """Функция для активации купона""" - conn = await asyncpg.connect(DATABASE_URL) +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, + ) - try: - coupon_record = await conn.fetchrow( + 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, + ) + + if usage_exists: + return "❌ Вы уже активировали этот купон. 🚫 Купоны могут быть активированы только один раз. 🔒" + + coupon_amount = coupon_record["amount"] + + async with session.transaction(): + await session.execute( """ - 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 + 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_code, + coupon_record["id"], ) - if not coupon_record: - return "❌ Купон не найден 🚫 или его использование ограничено. 🔒 Пожалуйста, проверьте код и попробуйте снова. 🔍" - - usage_exists = await conn.fetchrow( + await session.execute( """ - SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2 + INSERT INTO coupon_usages (coupon_id, user_id, used_at) + VALUES ($1, $2, $3) """, coupon_record["id"], user_id, + datetime.utcnow(), ) - if usage_exists: - return "❌ Вы уже активировали этот купон. 🚫 Купоны могут быть активированы только один раз. 🔒" - - coupon_amount = coupon_record["amount"] - - async with conn.transaction(): - await conn.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 conn.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) - return f"✅ Купон успешно активирован! 🎉\n\nНа ваш баланс добавлено {coupon_amount} рублей 💰." - - except Exception as e: - logger.error(f"Ошибка при активации купона: {e}") - return "⚠️ Произошла ошибка при активации купона! 🔧\nПопробуйте ещё раз позже. 🕒" - - finally: - await conn.close() + await update_balance(user_id, coupon_amount) + return f"✅ Купон успешно активирован! 🎉\n\nНа ваш баланс добавлено {coupon_amount} рублей 💰." diff --git a/handlers/donate.py b/handlers/donate.py index 6d394abc..d441463a 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -4,7 +4,6 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder -from bot import bot from config import RUB_TO_XTR from logger import logger @@ -20,12 +19,7 @@ router = Router() @router.callback_query(F.data == "donate") async def process_donate(callback_query: types.CallbackQuery, state: FSMContext): - - try: - await state.clear() - await callback_query.message.delete() - except Exception as e: - logger.error(f"Не удалось удалить сообщение: {e}") + await state.clear() builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) @@ -35,36 +29,26 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext) callback_data="enter_custom_donate_amount", ) ) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await bot.send_message( - chat_id=callback_query.from_user.id, + await callback_query.message.answer( text="🌟 Поддержите наш проект! 💪\n\n" "💖 Каждый донат помогает развивать и улучшать сервис. " "🤝 Мы ценим вашу поддержку и работаем над тем, чтобы сделать наш продукт еще лучше. 🚀💡", reply_markup=builder.as_markup(), ) - await callback_query.answer() - @router.callback_query(F.data == "enter_custom_donate_amount") 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.edit_text(f"💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup()) + await callback_query.message.answer(f"💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup()) await state.set_state(DonateState.entering_donate_amount) - await callback_query.answer() @router.message(DonateState.entering_donate_amount) async def process_donate_amount_input(message: types.Message, state: FSMContext): - - try: - await message.delete() - except Exception as e: - logger.error(f"Не удалось удалить сообщение: {e}") - if message.text.isdigit(): amount = int(message.text) if amount // RUB_TO_XTR <= 0: @@ -91,7 +75,6 @@ async def process_donate_amount_input(message: types.Message, state: FSMContext) await state.set_state(DonateState.waiting_for_donate_payment) except Exception as e: logger.error(f"Ошибка при создании доната: {e}") - await message.answer("Произошла ошибка при создании доната.") else: await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:") @@ -104,31 +87,14 @@ async def on_pre_checkout_query(pre_checkout_query: PreCheckoutQuery): @router.message(F.successful_payment, DonateState.waiting_for_donate_payment) async def on_successful_donate(message: types.Message, state: FSMContext): try: - user_id = int(message.from_user.id) amount = float(message.successful_payment.invoice_payload.split("_")[0]) - logger.debug(f"Donate succeeded for user_id: {user_id}, amount: {amount}") - - state_data = await state.get_data() - previous_message_id = state_data.get("last_message_id") - - if previous_message_id: - try: - await bot.delete_message(chat_id=user_id, message_id=previous_message_id) - except Exception as e: - logger.error(f"Не удалось удалить сообщение: {e}") - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Вернуться в профиль", callback_data="view_profile")) - - sent_message = await bot.send_message( - chat_id=user_id, + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await message.answer( text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖", reply_markup=builder.as_markup(), ) - - await state.update_data(last_message_id=sent_message.message_id) await state.clear() - except ValueError as e: logger.error(f"Ошибка конвертации user_id или amount: {e}") except Exception as e: diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 3ac9d143..dd7dc5dd 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -1,114 +1,64 @@ import os +from typing import Any from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg -from bot import bot -from config import CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL +from config import CONNECT_WINDOWS, SUPPORT_CHAT_URL from handlers.texts import INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE -from logger import logger router = Router() +@router.callback_query(F.data == "instructions") async def send_instructions(callback_query: types.CallbackQuery): - await callback_query.message.delete() - instructions_message = INSTRUCTIONS - image_path = os.path.join("img", "instructions.jpg") - if not os.path.isfile(image_path): await callback_query.message.answer("Файл изображения не найден.") - await callback_query.answer() return builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL)) builder.row( - InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile"), + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"), ) with open(image_path, "rb") as image_from_buffer: await callback_query.message.answer_photo( BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"), caption=instructions_message, - parse_mode="Markdown", reply_markup=builder.as_markup(), ) - await callback_query.answer() - @router.callback_query(F.data.startswith("connect_pc|")) -async def process_connect_pc(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id +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] - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") + record = await session.fetchrow( + """ + SELECT k.key + FROM keys k + WHERE k.tg_id = $1 AND k.email = $2 + """, + tg_id, + key_name, + ) - try: - 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, - ) + if not record: + await callback_query.message.answer("❌ Ключ не найден. Проверьте имя ключа. 🔍") + return - if not record: - await bot.send_message( - chat_id=tg_id, - text="Ключ не найден. Проверьте имя ключа.", - parse_mode="HTML", - ) - return + key = record["key"] + key_message = KEY_MESSAGE.format(key) + instruction_message = f"{key_message}{INSTRUCTION_PC}" - key = record["key"] - key_message = KEY_MESSAGE.format(key) - instruction_message = f"{key_message}{INSTRUCTION_PC}" + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}")) + builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - connect_windows_button = types.InlineKeyboardButton( - text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}" - ) - - support_button = types.InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}") - - back_button = types.InlineKeyboardButton(text="🔙 Назад в профиль", callback_data="view_profile") - - inline_keyboard = [ - [connect_windows_button], - [support_button], - [back_button], - ] - keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard) - - await bot.send_message( - tg_id, - instruction_message, - reply_markup=keyboard, - parse_mode="HTML", - ) - - finally: - await conn.close() - - except Exception as e: - logger.error(f"Ошибка при получении ключа: {e}") - await bot.send_message( - chat_id=tg_id, - text="Произошла ошибка. Пожалуйста, повторите попытку позже.", - parse_mode="HTML", - ) - - await callback_query.answer() + await callback_query.message.answer(instruction_message, reply_markup=builder.as_markup()) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 2233c8d3..780a7e71 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -1,27 +1,25 @@ import asyncio from datetime import datetime, timedelta +from typing import Any import uuid from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message -import asyncpg +from aiogram.types import CallbackQuery, InlineKeyboardButton, Message +from aiogram.utils.keyboard import InlineKeyboardBuilder -from bot import bot, dp -from config import ( - CONNECT_ANDROID, - CONNECT_IOS, - DATABASE_URL, - DOWNLOAD_ANDROID, - DOWNLOAD_IOS, - PUBLIC_LINK, - SUPPORT_CHAT_URL, +from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, SUPPORT_CHAT_URL +from database import ( + add_connection, + check_connection_exists, + get_balance, + get_trial, + store_key, + update_balance, + use_trial, ) -from database import add_connection, get_balance, store_key, update_balance -from handlers.instructions.instructions import send_instructions from handlers.keys.key_utils import create_key_on_cluster -from handlers.profile import process_callback_view_profile from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, RENEWAL_PLANS, key_message_success from handlers.utils import get_least_loaded_cluster, sanitize_key_name from logger import logger @@ -36,96 +34,57 @@ class Form(StatesGroup): waiting_for_message = State() -@dp.callback_query(F.data == "create_key") -async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id - - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - except Exception: - pass - +@router.callback_query(F.data == "create_key") +async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext, session: Any): server_id = "все сервера" await state.update_data(selected_server_id=server_id) - await select_server(callback_query, state) - await callback_query.answer() + await select_server(callback_query, state, session) -async def select_server(callback_query: CallbackQuery, state: FSMContext): - - conn = await asyncpg.connect(DATABASE_URL) - try: - existing_connection = await conn.fetchrow( - "SELECT trial FROM connections WHERE tg_id = $1", - callback_query.from_user.id, - ) - finally: - await conn.close() - - trial_status = existing_connection["trial"] if existing_connection else 0 - +async def select_server(callback_query: CallbackQuery, state: FSMContext, session: Any): + trial_status = await get_trial(callback_query.message.chat.id, session) if trial_status == 1: - await bot.send_message( - chat_id=callback_query.from_user.id, + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="✅ Да, подключить новое устройство", callback_data="confirm_create_new_key") + ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + + await callback_query.message.answer( text=KEY, - parse_mode="HTML", - reply_markup=InlineKeyboardMarkup( - inline_keyboard=[ - [ - InlineKeyboardButton( - text="✅ Да, подключить новое устройство", - callback_data="confirm_create_new_key", - ) - ], - [InlineKeyboardButton(text="↩️ Назад", callback_data="cancel_create_key")], - ] - ), + reply_markup=builder.as_markup(), ) await state.update_data(creating_new_key=True) else: - await bot.send_message( - chat_id=callback_query.from_user.id, - text=KEY_TRIAL, - parse_mode="HTML", - ) + await callback_query.message.answer(KEY_TRIAL) await state.set_state(Form.waiting_for_key_name) - await callback_query.answer() - -@dp.callback_query(F.data == "confirm_create_new_key") +@router.callback_query(F.data == "confirm_create_new_key") async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id + tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} confirmed creation of a new key.") balance = await get_balance(tg_id) if balance < RENEWAL_PLANS["1"]["price"]: - replenish_button = InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile") - keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]]) - await callback_query.message.edit_text(NULL_BALANCE, reply_markup=keyboard) + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await callback_query.message.answer(NULL_BALANCE, reply_markup=builder.as_markup()) await state.clear() return logger.info(f"Balance for user {tg_id} is sufficient. Asking for device name.") - await callback_query.message.edit_text("🔑 Пожалуйста, введите имя подключаемого устройства:") + await callback_query.message.answer("🔑 Пожалуйста, введите имя подключаемого устройства:") await state.set_state(Form.waiting_for_key_name) logger.info(f"State set to waiting_for_key_name for user {tg_id}") await state.update_data(creating_new_key=True) - await callback_query.answer() - - -@dp.callback_query(F.data == "cancel_create_key") -async def cancel_create_key(callback_query: CallbackQuery, state: FSMContext, admin: bool): - await process_callback_view_profile(callback_query, state, admin) - await callback_query.answer() - @router.message(Form.waiting_for_key_name) -async def handle_key_name_input(message: Message, state: FSMContext): - tg_id = message.from_user.id +async def handle_key_name_input(message: Message, state: FSMContext, session: Any): + tg_id = message.chat.id key_name = sanitize_key_name(message.text) logger.info(f"User {tg_id} is attempting to create a key with the name: {key_name}") @@ -135,38 +94,27 @@ async def handle_key_name_input(message: Message, state: FSMContext): logger.warning(f"User {tg_id} entered an invalid key name: {key_name}") return - conn = await asyncpg.connect(DATABASE_URL) - try: - logger.info(f"Checking if key name '{key_name}' already exists for user {tg_id} in the database.") - existing_key = await conn.fetchrow( - "SELECT * FROM keys WHERE email = $1 AND tg_id = $2", - key_name.lower(), - tg_id, + logger.info(f"Checking if key name '{key_name}' already exists for user {tg_id} in the database.") + existing_key = await session.fetchrow( + "SELECT * FROM keys WHERE email = $1 AND tg_id = $2", + key_name.lower(), + tg_id, + ) + if existing_key: + await message.answer( + "❌ Упс! Это имя уже используется. Выберите другое уникальное название для ключа.", ) - if existing_key: - await message.bot.send_message( - tg_id, - "❌ Упс! Это имя уже используется. Выберите другое уникальное название для ключа.", - ) - logger.warning(f"Key name '{key_name}' already exists for user {tg_id}.") - await state.set_state(Form.waiting_for_key_name) - return - finally: - await conn.close() + logger.warning(f"Key name '{key_name}' already exists for user {tg_id}.") + await state.set_state(Form.waiting_for_key_name) + return client_id = str(uuid.uuid4()) email = key_name.lower() current_time = datetime.utcnow() expiry_time = None - conn = await asyncpg.connect(DATABASE_URL) - try: - logger.info(f"Checking trial status for user {tg_id}.") - existing_connection = await conn.fetchrow("SELECT trial FROM connections WHERE tg_id = $1", tg_id) - finally: - await conn.close() - - trial_status = existing_connection["trial"] if existing_connection else 0 + logger.info(f"Checking trial status for user {tg_id}.") + trial_status = await get_trial(message.chat.id, session) if trial_status == 0: expiry_time = current_time + timedelta(days=1, hours=3) @@ -174,12 +122,11 @@ async def handle_key_name_input(message: Message, state: FSMContext): else: balance = await get_balance(tg_id) if balance < RENEWAL_PLANS["1"]["price"]: - replenish_button = InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile") - keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]]) - await message.bot.send_message( - tg_id, + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await message.answer( "💳 Недостаточно средств для создания подписки на новое устройство. Пополните баланс в личном кабинете.", - reply_markup=keyboard, + reply_markup=builder.as_markup(), ) logger.warning(f"User {tg_id} has insufficient funds for key creation.") await state.clear() @@ -194,29 +141,17 @@ async def handle_key_name_input(message: Message, state: FSMContext): logger.info(f"Generated public link for the key: {public_link}") - button_support = InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL) - - button_profile = InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile") - button_iphone = InlineKeyboardButton(text="🍏 Подключить", url=f"{CONNECT_IOS}{public_link}") - button_android = InlineKeyboardButton( - text="🤖 Подключить", - url=f"{CONNECT_ANDROID}{public_link}", + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL)) + builder.row( + InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS), + InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), ) - - button_download_ios = InlineKeyboardButton(text="🍏 Скачать", url=DOWNLOAD_IOS) - button_download_android = InlineKeyboardButton( - text="🤖 Скачать", - url=DOWNLOAD_ANDROID, - ) - - keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [button_support], - [button_download_ios, button_download_android], - [button_iphone, button_android], - [button_profile], - ] + builder.row( + InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{public_link}"), + InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{public_link}"), ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) remaining_time = expiry_time - current_time days = remaining_time.days @@ -224,7 +159,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): logger.info(f"Sending key message to user {tg_id} with the public link.") - await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard) + await message.answer(key_message, reply_markup=builder.as_markup()) try: least_loaded_cluster = await get_least_loaded_cluster() @@ -244,40 +179,16 @@ async def handle_key_name_input(message: Message, state: FSMContext): await asyncio.gather(*tasks) - conn = await asyncpg.connect(DATABASE_URL) - try: - logger.info(f"Updating trial status for user {tg_id} in the database.") - existing_connection = await conn.fetchrow("SELECT * FROM connections WHERE tg_id = $1", tg_id) - if existing_connection: - await conn.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id) - else: - await add_connection(tg_id, 0, 1) - finally: - await conn.close() + logger.info(f"Updating trial status for user {tg_id} in the database.") + connection_exists = await check_connection_exists(message.chat.id) + if connection_exists: + await use_trial(message.chat.id, session) + else: + await add_connection(tg_id=tg_id, balance=0, trial=1, session=session) logger.info(f"Storing key for user {tg_id} in the database.") - await store_key( - tg_id, - client_id, - email, - expiry_timestamp, - public_link, - least_loaded_cluster, - ) + await store_key(tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster, session) except Exception as e: logger.error(f"Error while creating the key for user {tg_id}: {e}") - await message.bot.send_message(tg_id, f"❌ Ошибка при создании ключа: {e}") - await state.clear() - - -@dp.callback_query(F.data == "instructions") -async def handle_instructions(callback_query: CallbackQuery): - await send_instructions(callback_query) - - -@dp.callback_query(F.data == "back_to_main") -async def handle_back_to_main(callback_query: CallbackQuery, state: FSMContext, admin: bool): - await process_callback_view_profile(callback_query, state, admin) - await callback_query.answer() diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index d0a68a18..4418b02d 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -79,15 +79,11 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to raise e -async def delete_key_from_db(client_id): - """Удаление ключа из базы данных""" +async def delete_key_from_db(client_id, session): try: - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id) + await session.execute("DELETE FROM keys WHERE client_id = $1", client_id) except Exception as e: logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}") - finally: - await conn.close() async def delete_key_from_cluster(cluster_id, email, client_id): diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index bd15afd4..689526bf 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -2,22 +2,13 @@ import asyncio from datetime import datetime, timedelta import locale import os +from typing import Any from aiogram import F, Router, types -from aiogram.types import BufferedInputFile -import asyncpg +from aiogram.types import BufferedInputFile, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder -from bot import bot -from config import ( - CLUSTERS, - CONNECT_ANDROID, - CONNECT_IOS, - DATABASE_URL, - DOWNLOAD_ANDROID, - DOWNLOAD_IOS, - PUBLIC_LINK, - TOTAL_GB, -) +from config import CLUSTERS, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, TOTAL_GB from database import delete_key, get_balance, store_key, update_balance, update_key_expiry from handlers.keys.key_utils import ( delete_key_from_cluster, @@ -43,207 +34,147 @@ router = Router() @router.callback_query(F.data == "view_keys") -async def process_callback_view_keys(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id - +async def process_callback_view_keys(callback_query: types.CallbackQuery, session: Any): + tg_id = callback_query.message.chat.id try: - conn = await asyncpg.connect(DATABASE_URL) - try: - records = await conn.fetch( - """ - SELECT email, client_id FROM keys WHERE tg_id = $1 - """, - tg_id, + records = await session.fetch( + """ + SELECT email, client_id FROM keys WHERE tg_id = $1 + """, + tg_id, + ) + + if records: + builder = InlineKeyboardBuilder() + for record in records: + key_name = record["email"] + builder.row(InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}")) + + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + + inline_keyboard = builder.as_markup() + response_message = ( + "🔑 Список ваших устройств\n\n" "👇 Выберите устройство для управления подпиской:" ) - if records: - buttons = [] - for record in records: - key_name = record["email"] - button = types.InlineKeyboardButton( - text=f"🔑 {key_name}", - callback_data=f"view_key|{key_name}", + image_path = os.path.join("img", "pic_keys.jpg") + if os.path.isfile(image_path): + with open(image_path, "rb") as image_file: + await callback_query.message.answer_photo( + photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), + caption=response_message, + reply_markup=inline_keyboard, ) - - buttons.append([button]) - - back_button = types.InlineKeyboardButton(text="🔙 Назад", callback_data="view_profile") - buttons.append([back_button]) - - inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) - response_message = ( - "🔑 Список ваших устройств\n\n" "👇 Выберите устройство для управления подпиской:" + else: + await callback_query.message.answer( + text=response_message, + reply_markup=inline_keyboard, ) - image_path = os.path.join("img", "pic_keys.jpg") + else: + response_message = NO_KEYS + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_key")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - try: - await bot.delete_message( - chat_id=tg_id, - message_id=callback_query.message.message_id, - ) - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") + keyboard = builder.as_markup() - if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: - await bot.send_photo( - chat_id=tg_id, - photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), - caption=response_message, - parse_mode="HTML", - reply_markup=inline_keyboard, - ) - else: - await bot.send_message( - chat_id=tg_id, - text=response_message, - reply_markup=inline_keyboard, - parse_mode="HTML", - ) + image_path = os.path.join("img", "pic_keys.jpg") - else: - response_message = NO_KEYS - create_key_button = types.InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_key") - back_button = types.InlineKeyboardButton(text="🔙 Назад", callback_data="view_profile") - - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]]) - - try: - await bot.delete_message( - chat_id=tg_id, - message_id=callback_query.message.message_id, - ) - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - - image_path = os.path.join("img", "pic_keys.jpg") - - if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: - await bot.send_photo( - chat_id=tg_id, - photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), - caption=response_message, - parse_mode="HTML", - reply_markup=keyboard, - ) - else: - await bot.send_message( - chat_id=tg_id, - text=response_message, + if os.path.isfile(image_path): + with open(image_path, "rb") as image_file: + await callback_query.message.answer_photo( + photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), + caption=response_message, reply_markup=keyboard, - parse_mode="HTML", ) - - finally: - await conn.close() - + else: + await callback_query.message.answer( + text=response_message, + reply_markup=keyboard, + ) except Exception as e: await handle_error(tg_id, callback_query, f"Ошибка при получении ключей: {e}") - await callback_query.answer() - @router.callback_query(F.data.startswith("view_key|")) -async def process_callback_view_key(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id +async def process_callback_view_key(callback_query: types.CallbackQuery, session: Any): + tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] - try: - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - except Exception: - pass + 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, + ) - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.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, + if record: + key = record["key"] + expiry_time = record["expiry_time"] + server_name = record["server_id"] + expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) + current_date = datetime.utcnow() + time_left = expiry_date - current_date + + if time_left.total_seconds() <= 0: + days_left_message = "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0" + 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}" + + formatted_expiry_date = expiry_date.strftime("%d %B %Y года") + response_message = key_message(key, formatted_expiry_date, days_left_message, server_name) + + builder = InlineKeyboardBuilder() + + builder.row( + InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS), + InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), ) - if record: - key = record["key"] - expiry_time = record["expiry_time"] - server_name = record["server_id"] - expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) - current_date = datetime.utcnow() - time_left = expiry_date - current_date + builder.row( + InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{key}"), + InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{key}"), + ) - if time_left.total_seconds() <= 0: - days_left_message = "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0" - 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}" + builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{key_name}")) - formatted_expiry_date = expiry_date.strftime("%d %B %Y года") - response_message = key_message(key, formatted_expiry_date, days_left_message, server_name) + builder.row( + InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"), + InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}"), + ) - download_android_button = types.InlineKeyboardButton(text="🤖 Скачать", url=DOWNLOAD_ANDROID) - download_iphone_button = types.InlineKeyboardButton(text="🍏 Скачать", url=DOWNLOAD_IOS) - - connect_iphone_button = types.InlineKeyboardButton(text="🍏 Подключить", url=f"{CONNECT_IOS}{key}") - connect_android_button = types.InlineKeyboardButton(text="🤖 Подключить", url=f"{CONNECT_ANDROID}{key}") - - connect_pc_button = types.InlineKeyboardButton( - text="💻 Windows/Linux", - callback_data=f"connect_pc|{key_name}", + if not key.startswith(PUBLIC_LINK): + builder.row( + InlineKeyboardButton(text="🔄 Обновить подписку", callback_data=f"update_subscription|{key_name}") ) - renew_button = types.InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}") - delete_button = types.InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}") - back_button = types.InlineKeyboardButton(text="🔙 Назад в профиль", callback_data="view_profile") + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - inline_keyboard = [ - [download_iphone_button, download_android_button], - [connect_iphone_button, connect_android_button], - [connect_pc_button], - [renew_button, delete_button], - ] + keyboard = builder.as_markup() - if not key.startswith(PUBLIC_LINK): - update_subscription_button = types.InlineKeyboardButton( - text="🔄 Обновить подписку", - callback_data=f"update_subscription|{key_name}", - ) - inline_keyboard.append([update_subscription_button]) + image_path = os.path.join("img", "pic_view.jpg") - inline_keyboard.append([back_button]) + if not os.path.isfile(image_path): + await callback_query.message.answer("Файл изображения не найден.") + return - keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard) - - image_path = os.path.join("img", "pic_view.jpg") - - if not os.path.isfile(image_path): - await bot.send_message(tg_id, "Файл изображения не найден.") - return - - with open(image_path, "rb") as image_file: - await bot.send_photo( - chat_id=tg_id, - photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"), - caption=response_message, - reply_markup=keyboard, - parse_mode="HTML", - ) - else: - await bot.send_message( - chat_id=tg_id, - text="Информация о подписке не найдена.", - parse_mode="HTML", + with open(image_path, "rb") as image_file: + await callback_query.message.answer_photo( + photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"), + caption=response_message, + reply_markup=keyboard, ) - - finally: - await conn.close() - + else: + await callback_query.message.answer( + text="Информация о подписке не найдена.", + ) except Exception as e: await handle_error( tg_id, @@ -251,127 +182,85 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): f"Ошибка при получении информации о ключе: {e}", ) - await callback_query.answer() - @router.callback_query(F.data.startswith("update_subscription|")) -async def process_callback_update_subscription( - callback_query: types.CallbackQuery, -): - tg_id = callback_query.from_user.id +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] - try: - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.fetchrow( - """ - SELECT k.key, k.expiry_time, k.email, k.server_id, k.client_id - FROM keys k - WHERE k.tg_id = $1 AND k.email = $2 - """, - tg_id, - email, - ) + record = await session.fetchrow( + """ + SELECT k.key, k.expiry_time, k.email, k.server_id, k.client_id + FROM keys k + WHERE k.tg_id = $1 AND k.email = $2 + """, + tg_id, + email, + ) - if record: - expiry_time = record["expiry_time"] - client_id = record["client_id"] - public_link = f"{PUBLIC_LINK}{email}/{tg_id}" + if record: + expiry_time = record["expiry_time"] + client_id = record["client_id"] + public_link = f"{PUBLIC_LINK}{email}/{tg_id}" - try: - await conn.execute( - """ - DELETE FROM keys - WHERE tg_id = $1 AND email = $2 - """, - tg_id, - email, - ) - except Exception as delete_error: - await bot.send_message( - tg_id, - f"Ошибка при удалении старой подписки: {delete_error}", - ) - return - - least_loaded_cluster_id = await get_least_loaded_cluster() - - tasks = [] - tasks.append( - update_key_on_cluster( - tg_id, - client_id, - email, - expiry_time, - least_loaded_cluster_id, - ) + try: + await session.execute( + """ + DELETE FROM keys + WHERE tg_id = $1 AND email = $2 + """, + tg_id, + email, ) + except Exception as delete_error: + await callback_query.message.answer( + f"Ошибка при удалении старой подписки: {delete_error}", + ) + return - await asyncio.gather(*tasks) + least_loaded_cluster_id = await get_least_loaded_cluster() - await store_key( + tasks = [] + tasks.append( + update_key_on_cluster( tg_id, client_id, email, expiry_time, - public_link, - server_id=least_loaded_cluster_id, + least_loaded_cluster_id, ) + ) - try: - await bot.delete_message( - chat_id=tg_id, - message_id=callback_query.message.message_id, - ) - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") + await asyncio.gather(*tasks) - response_message = f"Ваша подписка {email} обновлена!" - back_button = types.InlineKeyboardButton(text="🔙 Назад в профиль", callback_data="view_profile") - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - - await bot.send_message( - tg_id, - response_message, - reply_markup=keyboard, - parse_mode="HTML", - ) - else: - try: - await bot.delete_message( - chat_id=tg_id, - message_id=callback_query.message.message_id, - ) - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - - await bot.send_message( - tg_id, - "Ключ не найден в базе данных.", - parse_mode="HTML", - ) - - finally: - await conn.close() + await store_key( + tg_id, + client_id, + email, + expiry_time, + public_link, + server_id=least_loaded_cluster_id, + ) + response_message = f"Ваша подписка {email} обновлена!" + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await callback_query.message.answer( + response_message, + reply_markup=builder.as_markup(), + ) + else: + await callback_query.message.answer( + "Ключ не найден в базе данных.", + ) except Exception as e: await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}") - await callback_query.answer() - @router.callback_query(F.data.startswith("delete_key|")) async def process_callback_delete_key(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id client_id = callback_query.data.split("|")[1] - try: - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - except Exception: - pass - confirmation_keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ @@ -384,177 +273,124 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery): ] ) - await bot.send_message( - chat_id=tg_id, + await callback_query.message.answer( text="Вы уверены, что хотите удалить ключ?", reply_markup=confirmation_keyboard, - parse_mode="HTML", ) except Exception as e: - await bot.send_message( - chat_id=tg_id, - text=f"Ошибка при удалении ключа: {e}", - parse_mode="HTML", - ) - - await callback_query.answer() + logger.error(e) @router.callback_query(F.data.startswith("renew_key|")) -async def process_callback_renew_key(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id +async def process_callback_renew_key(callback_query: types.CallbackQuery, session: Any): + tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] - try: - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - except Exception: - pass - - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.fetchrow( - """ - SELECT client_id, expiry_time - FROM keys - WHERE email = $1 - """, - key_name, - ) - - if record: - client_id = record["client_id"] - expiry_time = record["expiry_time"] - - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', - callback_data=f"renew_plan|1|{client_id}", - ) - ], - [ - types.InlineKeyboardButton( - text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)', - callback_data=f"renew_plan|3|{client_id}", - ) - ], - [ - types.InlineKeyboardButton( - text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)', - callback_data=f"renew_plan|6|{client_id}", - ) - ], - [ - types.InlineKeyboardButton( - text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)', - callback_data=f"renew_plan|12|{client_id}", - ) - ], - [types.InlineKeyboardButton(text="🔙 Назад", callback_data="view_profile")], - ] - ) - - 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"), - ) - - await bot.send_message( - chat_id=tg_id, - text=response_message, - reply_markup=keyboard, - parse_mode="HTML", - ) - else: - # Если ключ не найден - response_message = "Ключ не найден." - await bot.send_message(chat_id=tg_id, text=response_message, parse_mode="HTML") - - finally: - await conn.close() - - except Exception as e: - await bot.send_message( - chat_id=tg_id, - text=f"Ошибка при выборе плана: {e}", - parse_mode="HTML", + record = await session.fetchrow( + """ + SELECT client_id, expiry_time + FROM keys + WHERE email = $1 + """, + key_name, ) - await callback_query.answer() + if record: + client_id = record["client_id"] + expiry_time = record["expiry_time"] + + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', + callback_data=f"renew_plan|1|{client_id}", + ) + ) + builder.row( + InlineKeyboardButton( + text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)', + callback_data=f"renew_plan|3|{client_id}", + ) + ) + builder.row( + InlineKeyboardButton( + text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)', + callback_data=f"renew_plan|6|{client_id}", + ) + ) + builder.row( + InlineKeyboardButton( + text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)', + callback_data=f"renew_plan|12|{client_id}", + ) + ) + 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"), + ) + + await callback_query.message.answer( + text=response_message, + reply_markup=builder.as_markup(), + ) + else: + await callback_query.message.answer("Ключ не найден.") + except Exception as e: + logger.error(e) @router.callback_query(F.data.startswith("confirm_delete|")) -async def process_callback_confirm_delete(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id +async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): email = callback_query.data.split("|")[1] - try: - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) + record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) - if record: - client_id = record["client_id"] - response_message = "Ключ успешно удален." - back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + if record: + client_id = record["client_id"] + response_message = "Ключ успешно удален." + back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - await delete_key(client_id) - await bot.edit_message_text( - response_message, - chat_id=tg_id, - message_id=callback_query.message.message_id, - reply_markup=keyboard, - ) + await delete_key(client_id) + await callback_query.message.answer( + response_message, + reply_markup=keyboard, + ) - async def delete_key_from_servers(): - try: - tasks = [] - for cluster_id, cluster in CLUSTERS.items(): - tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) + async def delete_key_from_servers(): + try: + tasks = [] + for cluster_id, cluster in CLUSTERS.items(): + tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) - await asyncio.gather(*tasks) + await asyncio.gather(*tasks) - except Exception as e: - logger.error(f"Ошибка при удалении ключа {client_id}: {e}") + except Exception as e: + logger.error(f"Ошибка при удалении ключа {client_id}: {e}") - asyncio.create_task(delete_key_from_servers()) + asyncio.create_task(delete_key_from_servers()) - await delete_key_from_db(client_id) + await delete_key_from_db(client_id, session) - else: - response_message = "Ключ не найден или уже удален." - back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - - await bot.edit_message_text( - response_message, - chat_id=tg_id, - message_id=callback_query.message.message_id, - reply_markup=keyboard, - ) - - finally: - await conn.close() + else: + response_message = "Ключ не найден или уже удален." + back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + await callback_query.message.answer( + response_message, + reply_markup=keyboard, + ) except Exception as e: - await bot.edit_message_text( - f"Ошибка при удалении ключа: {e}", - chat_id=tg_id, - message_id=callback_query.message.message_id, - ) - - await callback_query.answer() + logger.error(e) @router.callback_query(F.data.startswith("renew_plan|")) -async def process_callback_renew_plan(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id +async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any): + tg_id = callback_query.message.chat.id plan, client_id = ( callback_query.data.split("|")[1], callback_query.data.split("|")[2], @@ -565,83 +401,63 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery): total_gb = TOTAL_GB * gb_multiplier.get(plan, 1) if TOTAL_GB > 0 else 0 try: - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") + record = await session.fetchrow( + "SELECT email, expiry_time FROM keys WHERE client_id = $1", + client_id, + ) - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.fetchrow( - "SELECT email, expiry_time FROM keys WHERE client_id = $1", - client_id, - ) - - if record: - email = record["email"] - expiry_time = record["expiry_time"] - current_time = datetime.utcnow().timestamp() * 1000 - - if expiry_time <= current_time: - new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000) - else: - new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000) - - cost = RENEWAL_PLANS[plan]["price"] - - balance = await get_balance(tg_id) - if balance < cost: - replenish_button = types.InlineKeyboardButton(text="Пополнить баланс", callback_data="pay") - view_profile = types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile") - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [view_profile]]) - - await bot.send_message( - tg_id, - INSUFFICIENT_FUNDS_MSG, - reply_markup=keyboard, - parse_mode="HTML", - ) - return - - response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]["months"]) - back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_profile") - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - - await bot.send_message( - tg_id, - response_message, - reply_markup=keyboard, - parse_mode="HTML", - ) - - async def renew_key_on_servers(): - tasks = [] - for cluster_id in CLUSTERS: - task = asyncio.create_task( - renew_key_in_cluster( - cluster_id, - email, - client_id, - new_expiry_time, - total_gb, - ) - ) - tasks.append(task) - - await asyncio.gather(*tasks) - - await update_balance(tg_id, -cost) - await update_key_expiry(client_id, new_expiry_time) - - await renew_key_on_servers() + if record: + email = record["email"] + expiry_time = record["expiry_time"] + current_time = datetime.utcnow().timestamp() * 1000 + if expiry_time <= current_time: + new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000) else: - await bot.send_message(tg_id, KEY_NOT_FOUND_MSG, parse_mode="HTML") + new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000) - finally: - await conn.close() + cost = RENEWAL_PLANS[plan]["price"] + balance = await get_balance(tg_id) + if balance < cost: + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="Пополнить баланс", callback_data="pay")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + + await callback_query.message.answer( + INSUFFICIENT_FUNDS_MSG, + reply_markup=builder.as_markup(), + ) + return + + response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]["months"]) + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + + await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + + async def renew_key_on_servers(): + tasks = [] + for cluster_id in CLUSTERS: + task = asyncio.create_task( + renew_key_in_cluster( + cluster_id, + email, + client_id, + new_expiry_time, + total_gb, + ) + ) + tasks.append(task) + + await asyncio.gather(*tasks) + + await update_balance(tg_id, -cost) + await update_key_expiry(client_id, new_expiry_time) + + await renew_key_on_servers() + + else: + await callback_query.message.answer(KEY_NOT_FOUND_MSG) except Exception as e: - await bot.send_message(tg_id, f"Ошибка при продлении ключа: {e}", parse_mode="HTML") - - await callback_query.answer() + logger.error(e) diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 85f02c33..13c74362 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -1,107 +1,53 @@ -import asyncio from datetime import datetime, timedelta +from typing import Any import uuid -import asyncpg from py3xui import AsyncApi from client import add_client -from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, PUBLIC_LINK, TRIAL_TIME -from database import store_key +from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, PUBLIC_LINK, TRIAL_TIME +from database import store_key, use_trial from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email, get_least_loaded_cluster -async def create_trial_key(tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) - try: - client_id = str(uuid.uuid4()) - email = generate_random_email() +async def create_trial_key(tg_id: int, session: Any): + client_id = str(uuid.uuid4()) + email = generate_random_email() + public_link = f"{PUBLIC_LINK}{email}/{tg_id}" + instructions = INSTRUCTIONS + result = {"key": public_link, "instructions": instructions} + current_time = datetime.utcnow() + expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3) + expiry_timestamp = int(expiry_time.timestamp() * 1000) - public_link = f"{PUBLIC_LINK}{email}/{tg_id}" - instructions = INSTRUCTIONS + least_loaded_cluster = await get_least_loaded_cluster() + for server_id, server in CLUSTERS[least_loaded_cluster].items(): + xui = AsyncApi( + CLUSTERS[least_loaded_cluster][server_id]["API_URL"], + username=ADMIN_USERNAME, + password=ADMIN_PASSWORD, + ) - result = {"key": public_link, "instructions": instructions} - - asyncio.create_task(generate_and_store_keys(tg_id, client_id, email, public_link)) - - return result - - finally: - await conn.close() - - -async def generate_and_store_keys(tg_id: int, client_id: str, email: str, public_link: str): - conn = await asyncpg.connect(DATABASE_URL) - try: - current_time = datetime.utcnow() - expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3) - expiry_timestamp = int(expiry_time.timestamp() * 1000) - - least_loaded_cluster = await get_least_loaded_cluster() - - tasks = [] - for server_id, server in CLUSTERS[least_loaded_cluster].items(): - task = create_key_on_server( - least_loaded_cluster, - server_id, - client_id, - email, - tg_id, - expiry_timestamp, - ) - tasks.append(task) - - await asyncio.gather(*tasks) - - await store_key( - tg_id, + await add_client( + xui, client_id, email, - expiry_timestamp, - public_link, - server_id=least_loaded_cluster, - ) - - await conn.execute( - """ - INSERT INTO connections (tg_id, trial) - VALUES ($1, 1) - ON CONFLICT (tg_id) - DO UPDATE SET trial = 1 - """, tg_id, + limit_ip=1, + total_gb=0, + expiry_time=expiry_timestamp, + enable=True, + flow="xtls-rprx-vision", ) - finally: - await conn.close() - -async def create_key_on_server( - cluster_id: str, - server_id: str, - client_id: str, - email: str, - tg_id: int, - expiry_timestamp: int, -): - """Создает ключ на сервере в указанном кластере и возвращает результат.""" - - xui = AsyncApi( - CLUSTERS[cluster_id][server_id]["API_URL"], - username=ADMIN_USERNAME, - password=ADMIN_PASSWORD, - ) - - response = await add_client( - xui, + await store_key( + tg_id, client_id, email, - tg_id, - limit_ip=1, - total_gb=0, - expiry_time=expiry_timestamp, - enable=True, - flow="xtls-rprx-vision", + expiry_timestamp, + public_link, + server_id=least_loaded_cluster, ) - - return response + await use_trial(tg_id, session) + return result diff --git a/handlers/notifications.py b/handlers/notifications.py index 7a45072f..613f940a 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -2,12 +2,20 @@ import asyncio from datetime import datetime, timedelta from aiogram import Bot, Router, types +from aiogram.utils.keyboard import InlineKeyboardBuilder import asyncpg from py3xui import AsyncApi from client import delete_client -from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB -from database import delete_key, get_balance, update_balance, update_key_expiry +from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB, TRIAL_TIME +from database import ( + add_notification, + check_notification_time, + delete_key, + get_balance, + update_balance, + update_key_expiry, +) from handlers.keys.key_utils import renew_key_in_cluster from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS from logger import logger @@ -27,6 +35,8 @@ async def notify_expiring_keys(bot: Bot): logger.info("Начало обработки уведомлений.") + await notify_inactive_trial_users(bot, conn) + await asyncio.sleep(1) await notify_10h_keys(bot, conn, current_time, threshold_time_10h) await asyncio.sleep(1) await notify_24h_keys(bot, conn, current_time, threshold_time_24h) @@ -95,28 +105,12 @@ async def notify_10h_keys( if not await is_bot_blocked(bot, tg_id): try: - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text="🔄 Продлить VPN", - callback_data=f'renew_key|{record["client_id"]}', - ) - ], - [ - types.InlineKeyboardButton( - text="💳 Пополнить баланс", - callback_data="pay", - ) - ], - [ - types.InlineKeyboardButton( - text="👤 Личный кабинет", - callback_data="view_profile", - ) - ], - ] - ) + keyboard = InlineKeyboardBuilder() + keyboard.button(text="🔄 Продлить VPN", callback_data=f'renew_key|{record["client_id"]}') + keyboard.button(text="💳 Пополнить баланс", callback_data="pay") + keyboard.button(text="👤 Личный кабинет", callback_data="profile") + keyboard.adjust(1) + keyboard = keyboard.as_markup() await bot.send_message(tg_id, message, reply_markup=keyboard) logger.info(f"Уведомление отправлено пользователю {tg_id}.") except Exception as e: @@ -175,28 +169,26 @@ async def notify_24h_keys( if not await is_bot_blocked(bot, tg_id): try: - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text="🔄 Продлить VPN", - callback_data=f'renew_key|{record["client_id"]}', - ) - ], - [ - types.InlineKeyboardButton( - text="💳 Пополнить баланс", - callback_data="pay", - ) - ], - [ - types.InlineKeyboardButton( - text="👤 Личный кабинет", - callback_data="view_profile", - ) - ], - ] + builder = InlineKeyboardBuilder() + builder.row( + types.InlineKeyboardButton( + text="🔄 Продлить VPN", + callback_data=f'renew_key|{record["client_id"]}', + ) ) + builder.row( + types.InlineKeyboardButton( + text="💳 Пополнить баланс", + callback_data="pay", + ) + ) + builder.row( + types.InlineKeyboardButton( + text="👤 Личный кабинет", + callback_data="profile", + ) + ) + keyboard = builder.as_markup() await bot.send_message(tg_id, message_24h, reply_markup=keyboard) logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.") except Exception as e: @@ -212,6 +204,59 @@ async def notify_24h_keys( await asyncio.sleep(1) +async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): + logger.info("Проверка пользователей, не активировавших пробный период...") + + inactive_trial_users = await conn.fetch( + """ + SELECT tg_id, username FROM users + WHERE tg_id IN ( + SELECT tg_id FROM connections + WHERE trial = 0 + ) AND tg_id NOT IN ( + SELECT DISTINCT tg_id FROM keys + ) + """ + ) + logger.info(f"Найдено {len(inactive_trial_users)} неактивных пользователей.") + + for user in inactive_trial_users: + tg_id = user['tg_id'] + username = user.get('username', 'Пользователь') + + try: + # Проверяем, можно ли отправить уведомление + can_notify = await check_notification_time( + tg_id, 'inactive_trial', hours=24, session=conn # Уведомление не чаще, чем раз в 24 часа + ) + + if can_notify and not await is_bot_blocked(bot, tg_id): + builder = InlineKeyboardBuilder() + builder.row( + types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key") + ) + builder.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + keyboard = builder.as_markup() + + message = ( + f"👋 Привет, {username}!\n\n" + f"🎉 У тебя есть бесплатный пробный период на {TRIAL_TIME} дней!\n" + "🕒 Не упусти возможность попробовать наш VPN прямо сейчас.\n\n" + "💡 Нажми на кнопку ниже, чтобы активировать пробный доступ." + ) + + await bot.send_message(tg_id, message, reply_markup=keyboard) + logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.") + + # Добавляем запись о notification + await add_notification(tg_id, 'inactive_trial', session=conn) + + 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("Проверка истекших ключей...") @@ -245,7 +290,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: "💡 Не откладывайте подключение VPN!" ) keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")]] + inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]] ) try: diff --git a/handlers/pay.py b/handlers/pay.py index 2dc7d5d7..9c588072 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -3,15 +3,12 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE -from database import get_trial -from handlers.start import send_welcome_message router = Router() @router.callback_query(F.data == "pay") async def handle_pay(callback_query: CallbackQuery): - await callback_query.message.delete() builder = InlineKeyboardBuilder() if YOOKASSA_ENABLE: @@ -51,23 +48,12 @@ async def handle_pay(callback_query: CallbackQuery): ) builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon")) - builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( "💸 Выберите удобный способ пополнения баланса:\n\n" "• Быстро и безопасно\n" "• Поддержка разных платежных систем\n" "• Моментальное зачисление средств 🚀", - parse_mode="HTML", reply_markup=builder.as_markup(), ) - - await callback_query.answer() - - -@router.callback_query(F.data == "back_to_menu") -async def handle_back_to_menu(callback_query: CallbackQuery, admin: bool = False): - await callback_query.message.delete() - trial_status = await get_trial(callback_query.from_user.id) - await send_welcome_message(callback_query.from_user.id, trial_status, admin) - await callback_query.answer() diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index 9fd0b37e..3704de6b 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -1,3 +1,5 @@ +from typing import Any + from aiocryptopay import AioCryptoPay, Networks from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -6,9 +8,9 @@ from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web -from bot import bot from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -24,31 +26,9 @@ class ReplenishBalanceState(StatesGroup): entering_custom_amount_crypto = State() -async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key="last_message_id"): - if state: - try: - state_data = await state.get_data() - previous_message_id = state_data.get(message_key) - - if previous_message_id: - await bot.delete_message(chat_id=chat_id, message_id=previous_message_id) - - sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup) - await state.update_data({message_key: sent_message.message_id}) - - except Exception as e: - logger.error(f"Ошибка при удалении/отправке сообщения: {e}") - return None - - return sent_message - - @router.callback_query(F.data == "pay_cryptobot") -async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id - +async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext, session: Any): builder = InlineKeyboardBuilder() - for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): builder.row( @@ -74,28 +54,19 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st callback_data="enter_custom_amount_crypto", ) ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile")) key_count = await get_key_count(tg_id) - if key_count == 0: - exists = await check_connection_exists(tg_id) + exists = await check_connection_exists(callback_query.message.chat.id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0) - - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - except Exception as e: - logger.error(f"Не удалось удалить сообщение: {e}") - - await bot.send_message( - chat_id=tg_id, - text="Выберите сумму пополнения:", + await add_connection(tg_id=callback_query.message.chat.id, balance=0.0, trial=0, session=session) + await callback_query.message.answer( + "Выберите сумму пополнения:", reply_markup=builder.as_markup(), ) - await state.set_state(ReplenishBalanceState.choosing_amount_crypto) - await callback_query.answer() @router.callback_query(F.data.startswith("crypto_amount|")) @@ -103,11 +74,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F data = callback_query.data.split("|", 1) if len(data) != 2: - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - await callback_query.message.answer("Неверные данные для выбора суммы.") return @@ -115,11 +81,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F try: amount = int(amount_str) except ValueError: - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - await callback_query.message.answer("Некорректная сумма.") return @@ -127,24 +88,18 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_crypto) try: - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - invoice = await crypto.create_invoice( asset="USDT", amount=str(int(amount // RUB_TO_USDT)), description=f"Пополнения баланса на {amount} руб", - payload=f"{callback_query.from_user.id}:{int(amount)}", + payload=f"{callback_query.message.chat.id}:{int(amount)}", ) if hasattr(invoice, "bot_invoice_url"): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")) - await bot.send_message( - chat_id=callback_query.from_user.id, + await callback_query.message.answer( text=f"Вы выбрали пополнение на {amount} рублей.", reply_markup=builder.as_markup(), ) @@ -152,22 +107,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F await callback_query.message.answer("Ошибка при создании платежа.") except Exception as e: logger.error(f"Ошибка при создании платежа: {e}") - await callback_query.message.answer("Произошла ошибка при создании платежа.") - - await callback_query.answer() - - -async def send_payment_success_notification(user_id: int, amount: float): - try: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")) - await bot.send_message( - chat_id=user_id, - text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", - reply_markup=builder.as_markup(), - ) - except Exception as e: - logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") async def cryptobot_webhook(request): @@ -204,9 +143,8 @@ async def process_crypto_payment(payload): @router.callback_query(F.data == "enter_custom_amount_crypto") async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.edit_text(text="Введите сумму пополнения:") + await callback_query.message.answer(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount_crypto) - await callback_query.answer() @router.message(ReplenishBalanceState.entering_custom_amount_crypto) @@ -224,7 +162,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) asset="USDT", amount=str(int(amount // RUB_TO_USDT)), description=f"Пополнения баланса на {amount} руб", - payload=f"{message.from_user.id}:{amount}", + payload=f"{message.chat.id}:{amount}", ) if hasattr(invoice, "bot_invoice_url"): @@ -233,18 +171,11 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) builder.row( InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"), ) - await message.message.edit_text( + await message.answer( text=f"Вы выбрали пополнение на {amount} рублей.", reply_markup=builder.as_markup(), ) - else: - await send_message_with_deletion( - message.from_user.id, - "Ошибка при создании платежа.", - state=state, - ) except Exception as e: logger.error(f"Ошибка при создании платежа: {e}") - await message.answer("Произошла ошибка при создании платежа.") else: await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:") diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 3b356b9f..b3c26790 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -12,9 +12,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web import requests -from bot import bot from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID from database import add_payment, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS router = Router() @@ -65,16 +65,6 @@ async def create_payment(user_id, amount, email, ip): return None -async def send_payment_success_notification(user_id, amount): - try: - await bot.send_message( - chat_id=user_id, - text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", - ) - except Exception as e: - logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") - - async def freekassa_webhook(request): data = await request.json() logging.debug(f"Получен вебхук от FreeKassa: {data}") @@ -94,8 +84,6 @@ async def freekassa_webhook(request): @router.callback_query(lambda c: c.data == "pay_freekassa") async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id - builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -124,16 +112,12 @@ async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, st ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile")) - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - - await bot.send_message( - chat_id=tg_id, + await callback_query.message.answer( text="Выберите сумму пополнения через FreeKassa:", reply_markup=builder.as_markup(), ) await state.set_state(ReplenishBalanceState.choosing_amount_freekassa) - await callback_query.answer() @router.callback_query(F.data.startswith("freekassa_amount|")) @@ -143,12 +127,12 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F try: amount = int(amount_str) except ValueError: - await bot.send_message(callback_query.from_user.id, "Некорректная сумма.") + await callback_query.message.answer("Некорректная сумма.") return - user_email = f"{callback_query.from_user.id}@solo.net" + user_email = f"{callback_query.message.chat.id}@solo.net" user_ip = callback_query.message.chat.id - payment_url = await create_payment(callback_query.from_user.id, amount, user_email, user_ip) + payment_url = await create_payment(callback_query.message.chat.id, amount, user_email, user_ip) if payment_url: confirm_keyboard = InlineKeyboardMarkup( @@ -158,25 +142,20 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F ] ) - await bot.send_message( - callback_query.from_user.id, + await callback_query.message.answer( f"Вы выбрали оплату на {amount} рублей. Перейдите по ссылке для завершения оплаты:", reply_markup=confirm_keyboard, ) else: - await bot.send_message( - callback_query.from_user.id, + await callback_query.message.answer( "Ошибка при создании платежа. Попробуйте позже.", ) - await callback_query.answer() - @router.callback_query(F.data == "enter_custom_amount_freekassa") async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.edit_text(text="Введите сумму пополнения:") + await callback_query.message.answer(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount_freekassa) - await callback_query.answer() @router.message(ReplenishBalanceState.entering_custom_amount_freekassa) @@ -187,9 +166,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:") return - user_email = f"{message.from_user.id}@solo.net" + user_email = f"{message.chat.id}@solo.net" user_ip = message.chat.id - payment_url = await create_payment(message.from_user.id, amount, user_email, user_ip) + payment_url = await create_payment(message.chat.id, amount, user_email, user_ip) if payment_url: keyboard = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]]) diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 23725876..f9248aec 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -1,4 +1,5 @@ import hashlib +from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -8,9 +9,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web from robokassa import HashAlgorithm, Robokassa -from bot import bot from config import ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -47,30 +48,9 @@ def generate_payment_link(amount, inv_id, description, tg_id): return payment_link -async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key="last_message_id"): - if state: - try: - state_data = await state.get_data() - previous_message_id = state_data.get(message_key) - - if previous_message_id: - logger.debug(f"Deleting previous message with ID: {previous_message_id}") - await bot.delete_message(chat_id=chat_id, message_id=previous_message_id) - - sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup) - await state.update_data({message_key: sent_message.message_id}) - - logger.debug(f"Sent new message with ID: {sent_message.message_id}") - except Exception as e: - logger.error(f"Ошибка при удалении/отправке сообщения: {e}") - return None - - return sent_message - - @router.callback_query(F.data == "pay_robokassa") -async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id +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.") builder = InlineKeyboardBuilder() @@ -106,23 +86,15 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0) + await add_connection(tg_id, balance=0.0, trial=0, session=session) logger.info(f"Created new connection for user {tg_id} with balance 0.0.") - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - logger.debug(f"Deleted message with ID: {callback_query.message.message_id}") - except Exception as e: - logger.error(f"Не удалось удалить сообщение: {e}") - - await bot.send_message( - chat_id=tg_id, + await callback_query.message.answer( text="Выберите сумму пополнения:", reply_markup=builder.as_markup(), ) await state.set_state(ReplenishBalanceState.choosing_amount_robokassa) logger.info(f"Displayed amount selection for user {tg_id}.") - await callback_query.answer() @router.callback_query(F.data.startswith("robokassa_amount|")) @@ -132,12 +104,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F data = callback_query.data.split("|") if len(data) != 3 or data[1] != "amount": logger.error("Ошибка: callback_data не соответствует формату.") - await send_message_with_deletion( - chat_id=callback_query.from_user.id, - text="Неверные данные для выбора суммы.", - state=state, - ) - await callback_query.answer("Ошибка: данные повреждены.") + await callback_query.message.answer("Ошибка: данные повреждены.") return amount_str = data[2] @@ -147,22 +114,17 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F raise ValueError("Сумма должна быть положительным числом.") except ValueError as e: logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}") - await send_message_with_deletion( - chat_id=callback_query.from_user.id, - text="Некорректная сумма. Попробуйте снова.", - state=state, - ) - await callback_query.answer("Некорректная сумма.") + await callback_query.message.answer("Некорректная сумма.") return await state.update_data(amount=amount) - logger.info(f"User {callback_query.from_user.id} selected amount: {amount}.") + logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.") inv_id = 0 - tg_id = callback_query.from_user.id + tg_id = callback_query.message.chat.id payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id) - logger.info(f"Payment URL for user {callback_query.from_user.id}: {payment_url}") + logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}") confirm_keyboard = InlineKeyboardMarkup( inline_keyboard=[ @@ -171,12 +133,11 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F ] ) - await callback_query.message.edit_text( + await callback_query.message.answer( text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:", reply_markup=confirm_keyboard, ) - logger.info(f"Payment link sent to user {callback_query.from_user.id}.") - await callback_query.answer() + logger.info(f"Payment link sent to user {callback_query.message.chat.id}.") async def robokassa_webhook(request): @@ -238,31 +199,18 @@ def check_payment_signature(params): return signature_value.upper() == expected_signature.upper() -async def send_payment_success_notification(user_id: int, amount: float): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")) - - await bot.send_message( - chat_id=user_id, - text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", - reply_markup=builder.as_markup(), - ) - logger.info(f"Sent payment success notification to user {user_id}.") - - @router.callback_query(F.data == "enter_custom_amount_robokassa") async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id + tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} chose to enter a custom amount.") - await callback_query.message.edit_text(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):") + await callback_query.message.answer(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):") await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) - await callback_query.answer() @router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) async def handle_custom_amount_input(message: types.Message, state: FSMContext): - tg_id = message.from_user.id + tg_id = message.chat.id logger.info(f"User {tg_id} entered custom amount: {message.text}") inv_id = 0 diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index 1131079c..7e5730a5 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -1,12 +1,14 @@ +from typing import Any + from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder -from bot import bot from config import RUB_TO_XTR from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -19,28 +21,9 @@ class ReplenishBalanceState(StatesGroup): entering_custom_amount_stars = State() -async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key="last_message_id"): - if state: - try: - state_data = await state.get_data() - previous_message_id = state_data.get(message_key) - - if previous_message_id: - await bot.delete_message(chat_id=chat_id, message_id=previous_message_id) - - sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup) - await state.update_data({message_key: sent_message.message_id}) - - except Exception as e: - logger.error(f"Ошибка при удалении/отправке сообщения: {e}") - return None - - return sent_message - - @router.callback_query(F.data == "pay_stars") -async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id +async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext, session: Any): + tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) @@ -77,21 +60,19 @@ async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0) + await add_connection(tg_id, balance=0.0, trial=0, session=session) try: await callback_query.message.delete() except Exception as e: logger.error(f"Не удалось удалить сообщение: {e}") - await bot.send_message( - chat_id=tg_id, + await callback_query.message.answer( text="Выберите сумму пополнения:", reply_markup=builder.as_markup(), ) await state.set_state(ReplenishBalanceState.choosing_amount_stars) - await callback_query.answer() @router.callback_query(F.data.startswith("stars_amount|")) @@ -146,27 +127,11 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F logger.error(f"Ошибка при создании платежа: {e}") await callback_query.message.answer("Произошла ошибка при создании платежа.") - await callback_query.answer() - - -async def send_payment_success_notification(user_id: int, amount: float): - try: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")) - await bot.send_message( - chat_id=user_id, - text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", - reply_markup=builder.as_markup(), - ) - except Exception as e: - logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") - @router.callback_query(F.data == "enter_custom_amount_stars") async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.edit_text(text="Введите сумму пополнения:") + await callback_query.message.answer(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount_stars) - await callback_query.answer() @router.message(ReplenishBalanceState.entering_custom_amount_stars) @@ -213,7 +178,7 @@ async def on_successful_payment( message: types.Message, ): try: - user_id = int(message.from_user.id) + user_id = int(message.chat.id) amount = float(message.successful_payment.invoice_payload.split("_")[0]) logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}") await add_payment(int(user_id), float(amount), "stars") diff --git a/handlers/payments/utils.py b/handlers/payments/utils.py new file mode 100644 index 00000000..21015090 --- /dev/null +++ b/handlers/payments/utils.py @@ -0,0 +1,18 @@ +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from bot import bot +from logger import logger + + +async def send_payment_success_notification(user_id: int, amount: float): + try: + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await bot.send_message( + chat_id=user_id, + text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", + reply_markup=builder.as_markup(), + ) + except Exception as e: + logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index 8a89c532..edc9f071 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -1,3 +1,4 @@ +from typing import Any import uuid from aiogram import F, Router, types @@ -8,9 +9,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web from yookassa import Configuration, Payment -from bot import bot from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -29,28 +30,9 @@ class ReplenishBalanceState(StatesGroup): entering_custom_amount_yookassa = State() -async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key="last_message_id"): - if state: - try: - state_data = await state.get_data() - previous_message_id = state_data.get(message_key) - - if previous_message_id: - await bot.delete_message(chat_id=chat_id, message_id=previous_message_id) - - sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup) - await state.update_data({message_key: sent_message.message_id}) - - except Exception as e: - logger.error(f"Ошибка при удалении/отправке сообщения: {e}") - return None - - return sent_message - - @router.callback_query(F.data == "pay_yookassa") -async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id +async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any): + tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() @@ -79,28 +61,21 @@ async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, sta callback_data="enter_custom_amount_yookassa", ) ) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) key_count = await get_key_count(tg_id) if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0) + await add_connection(tg_id, balance=0.0, trial=0, session=session) - try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - except Exception as e: - logger.error(f"Не удалось удалить сообщение: {e}") - - await bot.send_message( - chat_id=tg_id, + await callback_query.message.answer( text="Выберите сумму пополнения:", reply_markup=builder.as_markup(), ) await state.set_state(ReplenishBalanceState.choosing_amount_yookassa) - await callback_query.answer() @router.callback_query(F.data.startswith("yookassa_amount|")) @@ -108,24 +83,12 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F data = callback_query.data.split("|", 1) if len(data) != 2: - await send_message_with_deletion( - callback_query.from_user.id, - "Неверные данные для выбора суммы.", - state=state, - message_key="amount_error_message_id", - ) return amount_str = data[1] try: amount = int(amount_str) except ValueError: - await send_message_with_deletion( - callback_query.from_user.id, - "Некорректная сумма.", - state=state, - message_key="amount_error_message_id", - ) return await state.update_data(amount=amount) @@ -133,7 +96,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F # state_data = await state.get_data() customer_name = callback_query.from_user.full_name - customer_id = callback_query.from_user.id + customer_id = callback_query.message.chat.id customer_email = f"{customer_id}@solo.net" @@ -176,32 +139,12 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F ] ) - await callback_query.message.edit_text( + await callback_query.message.answer( text=f"Вы выбрали пополнение на {amount} рублей.", reply_markup=confirm_keyboard, ) else: - await send_message_with_deletion( - callback_query.from_user.id, - "Ошибка при создании платежа.", - state=state, - ) - - await callback_query.answer() - - -async def send_payment_success_notification(user_id: int, amount: float): - try: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")) - - await bot.send_message( - chat_id=user_id, - text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", - reply_markup=builder.as_markup(), - ) - except Exception as e: - logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") + await callback_query.message.answer("Ошибка при создании платежа.") async def yookassa_webhook(request): @@ -225,9 +168,8 @@ async def yookassa_webhook(request): @router.callback_query(F.data == "enter_custom_amount_yookassa") async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.edit_text(text="Введите сумму пополнения:") + await callback_query.message.answer(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount_yookassa) - await callback_query.answer() @router.message(ReplenishBalanceState.entering_custom_amount_yookassa) @@ -254,7 +196,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) "receipt": { "customer": { "full_name": message.from_user.full_name, - "email": f"{message.from_user.id}@solo.net", + "email": f"{message.chat.id}@solo.net", "phone": "79000000000", }, "items": [ @@ -269,7 +211,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) } ], }, - "metadata": {"user_id": message.from_user.id}, + "metadata": {"user_id": message.chat.id}, }, uuid.uuid4(), ) diff --git a/handlers/profile.py b/handlers/profile.py index 2b8ffcf9..e4d67c20 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -5,91 +5,68 @@ from aiogram.fsm.context import FSMContext from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from bot import bot from config import CHANNEL_URL from database import get_balance, get_key_count, get_referral_stats from handlers.texts import RENEWAL_PLANS, get_referral_link, invite_message_send, profile_message_send -from logger import logger router = Router() +@router.callback_query(F.data == "profile") async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext, admin: bool): - chat_id = callback_query.from_user.id + chat_id = callback_query.message.chat.id username = callback_query.from_user.full_name - image_path = os.path.join("img", "pic.jpg") + key_count = await get_key_count(chat_id) + balance = await get_balance(chat_id) + if balance is None: + balance = 0 - try: - key_count = await get_key_count(chat_id) - balance = await get_balance(chat_id) - if balance is None: - balance = 0 + profile_message = profile_message_send(username, chat_id, balance, key_count) - profile_message = profile_message_send(username, chat_id, balance, key_count) + if key_count == 0: + profile_message += "\n🔧 Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение" - if key_count == 0: - profile_message += "\n🔧 Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение" - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL)) - builder.row(InlineKeyboardButton(text="💡 Тарифы", callback_data="view_tariffs")) - builder.row( - InlineKeyboardButton(text="➕ Устройство", callback_data="create_key"), - InlineKeyboardButton(text="📱 Мои устройства", callback_data="view_keys"), + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL)) + builder.row(InlineKeyboardButton(text="💡 Тарифы", callback_data="view_tariffs")) + builder.row( + InlineKeyboardButton(text="➕ Устройство", callback_data="create_key"), + InlineKeyboardButton(text="📱 Мои устройства", callback_data="view_keys"), + ) + builder.row( + InlineKeyboardButton( + text="💳 Пополнить баланс", + callback_data="pay", ) - builder.row( - InlineKeyboardButton( - text="💳 Пополнить баланс", - callback_data="pay", - ) - ) - builder.row( - InlineKeyboardButton(text="👥 Пригласить друзей", callback_data="invite"), - InlineKeyboardButton(text="📘 Инструкции", callback_data="instructions"), - ) - builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) - if admin: - builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) - builder.row(InlineKeyboardButton(text="⬅️ Главное меню", callback_data="back_to_menu")) + ) + builder.row( + InlineKeyboardButton(text="👥 Пригласить друзей", callback_data="invite"), + InlineKeyboardButton(text="📘 Инструкции", callback_data="instructions"), + ) + builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) + if admin: + builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) + builder.row(InlineKeyboardButton(text="⬅️ Главное меню", callback_data="start")) - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"❗ Ошибка при удалении сообщения: {e}") - - if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: - await bot.send_photo( - chat_id=chat_id, - photo=BufferedInputFile(image_file.read(), filename="pic.jpg"), - caption=profile_message, - parse_mode="HTML", - reply_markup=builder.as_markup(), - ) - else: - await bot.send_message( - chat_id=chat_id, - text=profile_message, - parse_mode="HTML", + if os.path.isfile(image_path): + with open(image_path, "rb") as image_file: + await callback_query.message.answer_photo( + photo=BufferedInputFile(image_file.read(), filename="pic.jpg"), + caption=profile_message, reply_markup=builder.as_markup(), ) - - except Exception as e: - await bot.send_message( - chat_id, - f"❗️ Не удалось загрузить профиль. Техническая ошибка: {e}", + else: + await callback_query.message.answer( + text=profile_message, + reply_markup=builder.as_markup(), ) @router.callback_query(F.data == "view_tariffs") async def view_tariffs_handler(callback_query: types.CallbackQuery): - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( "🚀 Доступные тарифы VPN:\n\n" @@ -101,15 +78,13 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): for months in sorted(RENEWAL_PLANS.keys(), key=int) ] ), - parse_mode="HTML", reply_markup=builder.as_markup(), ) - await callback_query.answer() @router.callback_query(F.data == "invite") async def invite_handler(callback_query: types.CallbackQuery): - chat_id = callback_query.from_user.id + chat_id = callback_query.message.chat.id referral_link = get_referral_link(chat_id) referral_stats = await get_referral_stats(chat_id) @@ -119,42 +94,16 @@ async def invite_handler(callback_query: types.CallbackQuery): image_path = os.path.join("img", "pic_invite.jpg") builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")) - - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - - try: - if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: - await bot.send_photo( - chat_id=chat_id, - photo=BufferedInputFile(image_file.read(), filename="pic_invite.jpg"), - caption=invite_message, - parse_mode="HTML", - reply_markup=builder.as_markup(), - ) - else: - await bot.send_message( - chat_id=chat_id, - text=invite_message, - parse_mode="HTML", + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + if os.path.isfile(image_path): + with open(image_path, "rb") as image_file: + await callback_query.message.answer_photo( + photo=BufferedInputFile(image_file.read(), filename="pic_invite.jpg"), + caption=invite_message, reply_markup=builder.as_markup(), ) - except Exception as e: - await bot.send_message( - chat_id=chat_id, - text=f"❗️ Не удалось отправить сообщение. Техническая ошибка: {e}", - parse_mode="HTML", + else: + await callback_query.message.answer( + text=invite_message, reply_markup=builder.as_markup(), ) - - await callback_query.answer() - - -@router.callback_query(F.data == "view_profile") -async def view_profile_handler(callback_query: types.CallbackQuery, state: FSMContext, admin: bool = False): - await state.clear() - await process_callback_view_profile(callback_query, state, admin) diff --git a/handlers/start.py b/handlers/start.py index 471d47e2..a4b1a6f9 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,134 +1,86 @@ import os +from typing import Any from aiogram import F, Router +from aiogram.filters import Command +from aiogram.fsm.context import FSMContext from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg -from bot import bot -from config import ( - CHANNEL_URL, - CONNECT_ANDROID, - CONNECT_IOS, - DATABASE_URL, - DOWNLOAD_ANDROID, - DOWNLOAD_IOS, - SUPPORT_CHAT_URL, -) -from database import add_connection, add_referral, check_connection_exists, get_trial +from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL +from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn -from logger import logger router = Router() -async def send_welcome_message(chat_id: int, trial_status: int, admin: bool): +@router.callback_query(F.data == "start") +async def handle_start_callback_query(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool): + await start_command(callback_query.message, state, session, admin) + + +@router.message(Command("start")) +async def start_command(message: Message, state: FSMContext, session: Any, admin: bool): + if message.text: + try: + referrer_tg_id = int(message.text.split("referral_")[1]) + await add_referral(message.chat.id, referrer_tg_id, session) + except (ValueError, IndexError): + pass + connection_exists = await check_connection_exists(message.chat.id) + if not connection_exists: + await add_connection(tg_id=message.chat.id, session=session) + trial_status = await get_trial(message.chat.id, session) image_path = os.path.join("img", "pic.jpg") builder = InlineKeyboardBuilder() if trial_status == 0: builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")) - if admin: - builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) builder.row( InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL), ) builder.row( InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL), ) + if 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: - await bot.send_photo( - chat_id=chat_id, + await message.answer_photo( photo=BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"), caption=WELCOME_TEXT, - parse_mode="HTML", reply_markup=builder.as_markup(), ) else: - await bot.send_message( - chat_id=chat_id, + await message.answer( text=WELCOME_TEXT, - parse_mode="HTML", reply_markup=builder.as_markup(), ) -async def start_command(message: Message, admin: bool = False): - try: - logger.info(f"Получена команда /start. Текст сообщения: {message.text}, user_id: {message.from_user.id}") - - if "referral_" in message.text: - logger.info("Обнаружен реферальный код.") - try: - referrer_tg_id = int(message.text.split("referral_")[1]) - logger.info(f"ID пригласившего пользователя: {referrer_tg_id}") - except ValueError: - logger.error("Ошибка парсинга реферального ID.") - return - - connection_exists = await check_connection_exists(message.from_user.id) - logger.info(f"Результат проверки подключения для user_id {message.from_user.id}: {connection_exists}") - if not connection_exists: - logger.info(f"Добавляем подключение для пользователя: {message.from_user.id}") - await add_connection(message.from_user.id) - logger.info(f"Добавляем реферал для пользователя {message.from_user.id}, приглашённым {referrer_tg_id}") - await add_referral(message.from_user.id, referrer_tg_id) - else: - logger.info(f"Пользователь {message.from_user.id} уже зарегистрирован.") - - logger.info(f"Проверяем статус пробного периода для user_id {message.from_user.id}") - trial_status = await get_trial(message.from_user.id) - logger.info(f"Статус пробного периода для user_id {message.from_user.id}: {trial_status}") - - logger.info(f"Отправка приветственного сообщения для user_id {message.from_user.id}") - await send_welcome_message(message.chat.id, trial_status, admin) - - except Exception as e: - logger.error(f"Ошибка в обработке команды /start для user_id {message.from_user.id}: {e}") - await message.answer("Произошла ошибка. Пожалуйста, попробуйте позже.") - - @router.callback_query(F.data == "connect_vpn") -async def handle_connect_vpn(callback_query: CallbackQuery): - await callback_query.message.delete() - user_id = callback_query.from_user.id +async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): + user_id = callback_query.message.chat.id - trial_key_info = await create_trial_key(user_id) + trial_key_info = await create_trial_key(user_id, session) if "error" in trial_key_info: await callback_query.message.answer(trial_key_info["error"]) else: - try: - - conn = await asyncpg.connect(DATABASE_URL) - - result = await conn.execute( - """ - UPDATE connections SET trial = 1 WHERE tg_id = $1 - """, - user_id, - ) - logger.info(f"Rows updated: {result}") - - await conn.close() - - except Exception as e: - logger.error(f"Ошибка при обновлении trial: {e}") - await callback_query.message.answer("Произошла ошибка при обновлении статуса.") + await use_trial(user_id, session) key_message = ( f"🔑 Ваш персональный ключ доступа:\n" - f"
{trial_key_info['key']}
\n\n" + f"{trial_key_info['key']}\n\n" f"📋 Быстрая инструкция по подключению:\n{INSTRUCTIONS_TRIAL}" ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) builder.row( InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS), InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), @@ -144,28 +96,19 @@ async def handle_connect_vpn(callback_query: CallbackQuery): ), ) - await callback_query.message.answer(key_message, parse_mode="HTML", reply_markup=builder.as_markup()) - - await callback_query.answer() + await callback_query.message.answer(key_message, reply_markup=builder.as_markup()) @router.callback_query(F.data == "about_vpn") async def handle_about_vpn(callback_query: CallbackQuery): - await callback_query.message.delete() - - about_vpn_message = get_about_vpn("3.1.1_Stable") - builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu")) + builder.row( + InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL), + ) + builder.row( + InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL), + ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start")) - await callback_query.message.answer(about_vpn_message, parse_mode="HTML", reply_markup=builder.as_markup()) - await callback_query.answer() - - -@router.callback_query(F.data == "back_to_menu") -async def handle_back_to_menu(callback_query: CallbackQuery, admin: bool = False): - await callback_query.message.delete() - trial_status = await get_trial(callback_query.from_user.id) - await send_welcome_message(callback_query.from_user.id, trial_status, admin) - await callback_query.answer() + await callback_query.message.answer(get_about_vpn("3.1.1_Stable"), reply_markup=builder.as_markup()) diff --git a/main.py b/main.py index 8857fef5..54d2efd0 100644 --- a/main.py +++ b/main.py @@ -55,23 +55,25 @@ async def on_shutdown(app): try: await asyncio.gather(*asyncio.all_tasks(), return_exceptions=True) except Exception as e: - logger.error(f"Error during shutdown: {e}") + logger.error(f"Ошибка при завершении работы: {e}") async def shutdown_site(site): - logger.info("Остановка сайта...") + logger.info("Остановка сайт...") await site.stop() - logger.info("Сервер остановлен.") + logger.info("Сервер сайт.") async def main(): dp.include_router(router) if DEV_MODE: + logger.info("Запуск в режиме разработки...") await bot.delete_webhook() await init_db() await dp.start_polling(bot) else: + logger.info("Запуск в production режиме...") app = web.Application() app.on_startup.append(on_startup) app.on_shutdown.append(on_shutdown) @@ -96,7 +98,7 @@ async def main(): site = web.TCPSite(runner, host=WEBAPP_HOST, port=WEBAPP_PORT) await site.start() - logger.info(f"Webhook URL: {WEBHOOK_URL}") + logger.info(f"URL вебхука: {WEBHOOK_URL}") loop = asyncio.get_event_loop() for sig in (signal.SIGINT, signal.SIGTERM): @@ -107,7 +109,10 @@ async def main(): finally: pending = asyncio.all_tasks() for task in pending: - task.cancel() + try: + task.cancel() + except Exception as e: + logger.error(e) await asyncio.gather(*pending, return_exceptions=True) diff --git a/middlewares/admin.py b/middlewares/admin.py index 70d98204..57aa0692 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -4,7 +4,6 @@ from aiogram import BaseMiddleware from aiogram.types import TelegramObject from config import ADMIN_ID -from logger import logger class AdminMiddleware(BaseMiddleware): @@ -20,10 +19,8 @@ class AdminMiddleware(BaseMiddleware): def _check_admin_access(self, event: TelegramObject) -> bool: try: admin_ids: Union[int, list[int]] = ADMIN_ID - if isinstance(admin_ids, list): return event.from_user.id in admin_ids return event.from_user.id == admin_ids - except Exception as e: - logger.error(f"Ошибка проверки администратора: {e}") + except Exception: return False diff --git a/middlewares/database.py b/middlewares/database.py index d529dad4..b1687aa6 100644 --- a/middlewares/database.py +++ b/middlewares/database.py @@ -14,10 +14,9 @@ class DatabaseMiddleware(BaseMiddleware): event: TelegramObject, data: Dict[str, Any], ) -> Any: - async with await asyncpg.create_pool(DATABASE_URL) as pool: - async with pool.acquire() as session: - data["session"] = session - try: - return await handler(event, data) - finally: - await pool.release(session) + conn = await asyncpg.connect(DATABASE_URL) + try: + data["session"] = conn + return await handler(event, data) + finally: + await conn.close() diff --git a/middlewares/delete.py b/middlewares/delete.py new file mode 100644 index 00000000..e3911cf8 --- /dev/null +++ b/middlewares/delete.py @@ -0,0 +1,24 @@ +from typing import Any, Awaitable, Callable, Dict + +from aiogram import BaseMiddleware +from aiogram.types import CallbackQuery, Message, TelegramObject + + +class DeleteMessageMiddleware(BaseMiddleware): + async def __call__( + self, + handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: Dict[str, Any], + ) -> Any: + if isinstance(event, (Message, CallbackQuery)): + if isinstance(event, Message): + try: + await event.bot.delete_message(event.chat.id, event.message_id - 1) + except Exception: + pass + await event.delete() + elif isinstance(event, CallbackQuery): + await event.answer() + await event.message.delete() + return await handler(event, data)