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 db48ef93..bc3cf86e 100644 --- a/database.py +++ b/database.py @@ -8,22 +8,6 @@ from logger import logger async def init_db(): conn = await asyncpg.connect(DATABASE_URL) - - # Таблица для хранения информации о платежах - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS payments ( - id SERIAL PRIMARY KEY, - tg_id BIGINT NOT NULL, - amount REAL NOT NULL, - payment_system TEXT NOT NULL, - status TEXT DEFAULT 'success', - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (tg_id) REFERENCES users(tg_id) - ) - """ - ) - # Таблица для хранения основной информации о пользователях из Telegram await conn.execute( """ @@ -40,6 +24,21 @@ async def init_db(): """ ) + # Таблица для хранения информации о платежах + await conn.execute( + """ + CREATE TABLE IF NOT EXISTS payments ( + id SERIAL PRIMARY KEY, + tg_id BIGINT NOT NULL, + amount REAL NOT NULL, + payment_system TEXT NOT NULL, + status TEXT DEFAULT 'success', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tg_id) REFERENCES users(tg_id) + ) + """ + ) + # Таблица для хранения информации о пользователях await conn.execute( """ @@ -183,7 +182,6 @@ async def restore_trial(tg_id: int): try: await conn.execute("UPDATE connections SET trial = 0 WHERE tg_id = $1", tg_id) except Exception as e: - logger.error(f"Ошибка при установке значения триала: {e}") finally: await conn.close() 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..49b0f23e 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -18,44 +18,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): try: - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - coupons = await get_all_coupons() 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 +46,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,17 +60,10 @@ 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()) @@ -98,45 +74,29 @@ async def handle_delete_coupon(callback_query: types.CallbackQuery): result = await delete_coupon_from_db(coupon_code) if result: - try: - await callback_query.message.delete() - except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") - await show_coupon_list(callback_query) else: await callback_query.message.answer( f"❌ Купон с кодом {coupon_code} не найден.", - parse_mode="HTML", ) await show_coupon_list(callback_query) 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()) @@ -147,15 +107,13 @@ async def process_coupon_data(message: types.Message, state: FSMContext): 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,9 +123,9 @@ 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 @@ -175,31 +133,16 @@ async def process_coupon_data(message: types.Message, state: FSMContext): await create_coupon(coupon_code, coupon_amount, usage_limit) 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..6520ffd9 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..6f1ea996 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,125 +29,114 @@ 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()) @@ -160,7 +148,7 @@ async def handle_restore_trial(callback_query: types.CallbackQuery): 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..7d9a5ced 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.from_user.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..e6160205 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..16d9094d 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( + await callback_query.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): +async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.from_user.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..4869f884 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -1,27 +1,16 @@ 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 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, 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,39 +25,24 @@ 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() +async def select_server(callback_query: CallbackQuery, state: FSMContext, session: Any): + existing_connection = await session.fetchrow( + "SELECT trial FROM connections WHERE tg_id = $1", + callback_query.from_user.id, + ) trial_status = existing_connection["trial"] if existing_connection else 0 if trial_status == 1: - await bot.send_message( - chat_id=callback_query.from_user.id, + await callback_query.message.answer( text=KEY, - parse_mode="HTML", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ @@ -77,23 +51,17 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext): callback_data="confirm_create_new_key", ) ], - [InlineKeyboardButton(text="↩️ Назад", callback_data="cancel_create_key")], + [InlineKeyboardButton(text="↩️ Назад", callback_data="profile")], ] ), ) 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 @@ -101,7 +69,7 @@ async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContex balance = await get_balance(tg_id) if balance < RENEWAL_PLANS["1"]["price"]: - replenish_button = InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile") + replenish_button = InlineKeyboardButton(text="Перейти в профиль", callback_data="profile") keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]]) await callback_query.message.edit_text(NULL_BALANCE, reply_markup=keyboard) await state.clear() @@ -114,17 +82,9 @@ async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContex 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): +async def handle_key_name_input(message: Message, state: FSMContext, session: Any): tg_id = message.from_user.id key_name = sanitize_key_name(message.text) @@ -135,36 +95,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() + logger.info(f"Checking trial status for user {tg_id}.") + existing_connection = await session.fetchrow("SELECT trial FROM connections WHERE tg_id = $1", tg_id) trial_status = existing_connection["trial"] if existing_connection else 0 @@ -174,10 +125,9 @@ 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") + replenish_button = InlineKeyboardButton(text="Перейти в профиль", callback_data="profile") keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]]) - await message.bot.send_message( - tg_id, + await message.answer( "💳 Недостаточно средств для создания подписки на новое устройство. Пополните баланс в личном кабинете.", reply_markup=keyboard, ) @@ -196,7 +146,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): button_support = InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL) - button_profile = InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile") + button_profile = InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") button_iphone = InlineKeyboardButton(text="🍏 Подключить", url=f"{CONNECT_IOS}{public_link}") button_android = InlineKeyboardButton( text="🤖 Подключить", @@ -224,7 +174,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=keyboard) try: least_loaded_cluster = await get_least_loaded_cluster() @@ -244,16 +194,12 @@ 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.") + existing_connection = await session.fetchrow("SELECT * FROM connections WHERE tg_id = $1", tg_id) + if existing_connection: + await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id) + else: + await add_connection(tg_id, 0, 1) logger.info(f"Storing key for user {tg_id} in the database.") await store_key( @@ -267,17 +213,4 @@ async def handle_key_name_input(message: Message, state: FSMContext): 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..7c0626dd 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -2,22 +2,12 @@ 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 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 +33,157 @@ router = Router() @router.callback_query(F.data == "view_keys") -async def process_callback_view_keys(callback_query: types.CallbackQuery): +async def process_callback_view_keys(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.from_user.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: - buttons = [] - for record in records: - key_name = record["email"] - button = types.InlineKeyboardButton( - text=f"🔑 {key_name}", - callback_data=f"view_key|{key_name}", - ) - - 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" "👇 Выберите устройство для управления подпиской:" + 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") + buttons.append([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}") + back_button = types.InlineKeyboardButton(text="🔙 Назад", callback_data="profile") + buttons.append([back_button]) - 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, + inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) + response_message = ( + "🔑 Список ваших устройств\n\n" "👇 Выберите устройство для управления подпиской:" + ) + + 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, - parse_mode="HTML", ) - else: - response_message = NO_KEYS - create_key_button = types.InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_key") - back_button = types.InlineKeyboardButton(text="🔙 Назад", callback_data="view_profile") + await callback_query.message.answer( + text=response_message, + reply_markup=inline_keyboard, + ) - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]]) + else: + response_message = NO_KEYS + create_key_button = types.InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_key") + back_button = types.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 = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]]) - image_path = os.path.join("img", "pic_keys.jpg") + 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): +async def process_callback_view_key(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.from_user.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) + + 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 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 + 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="profile") - 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}" + inline_keyboard = [ + [download_iphone_button, download_android_button], + [connect_iphone_button, connect_android_button], + [connect_pc_button], + [renew_button, delete_button], + ] - formatted_expiry_date = expiry_date.strftime("%d %B %Y года") - response_message = key_message(key, formatted_expiry_date, days_left_message, server_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): + update_subscription_button = types.InlineKeyboardButton( + text="🔄 Обновить подписку", + callback_data=f"update_subscription|{key_name}", ) + inline_keyboard.append([update_subscription_button]) - 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") + inline_keyboard.append([back_button]) - inline_keyboard = [ - [download_iphone_button, download_android_button], - [connect_iphone_button, connect_android_button], - [connect_pc_button], - [renew_button, delete_button], - ] + keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard) - 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 +191,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, -): +async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.from_user.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} обновлена!" + back_button = types.InlineKeyboardButton(text="🔙 Назад в профиль", callback_data="profile") + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + await callback_query.message.answer( + response_message, + reply_markup=keyboard, + ) + 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,176 +282,128 @@ 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): +async def process_callback_renew_key(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.from_user.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"] + + 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="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 callback_query.message.answer( + text=response_message, + reply_markup=keyboard, + ) + 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): +async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.from_user.id plan, client_id = ( callback_query.data.split("|")[1], @@ -565,83 +415,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: + replenish_button = types.InlineKeyboardButton(text="Пополнить баланс", callback_data="pay") + view_profile = types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [view_profile]]) + + await callback_query.message.answer( + INSUFFICIENT_FUNDS_MSG, + reply_markup=keyboard, + ) + return + + response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]["months"]) + back_button = types.InlineKeyboardButton(text="Назад", callback_data="profile") + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + + await callback_query.message.answer(response_message, reply_markup=keyboard) + + 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/notifications.py b/handlers/notifications.py index 7a45072f..280fb145 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -112,7 +112,7 @@ async def notify_10h_keys( [ types.InlineKeyboardButton( text="👤 Личный кабинет", - callback_data="view_profile", + callback_data="profile", ) ], ] @@ -192,7 +192,7 @@ async def notify_24h_keys( [ types.InlineKeyboardButton( text="👤 Личный кабинет", - callback_data="view_profile", + callback_data="profile", ) ], ] @@ -245,7 +245,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..06ff1dc3 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 6ee1416b..302125f3 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -6,9 +6,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 +24,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 - builder = InlineKeyboardBuilder() - for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): builder.row( @@ -75,27 +53,16 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st ) ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile")) - - key_count = await get_key_count(tg_id) - + key_count = await get_key_count(callback_query.from_user.id) if key_count == 0: - exists = await check_connection_exists(tg_id) + exists = await check_connection_exists(callback_query.from_user.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(callback_query.from_user.id, balance=0.0, trial=0) + 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 +70,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 +77,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,11 +84,6 @@ 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)), @@ -143,8 +95,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F 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 +103,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 +139,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) @@ -233,18 +167,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..6c91b841 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,7 +127,7 @@ 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" @@ -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 state.set_state(ReplenishBalanceState.entering_custom_amount_freekassa) - await callback_query.answer() @router.message(ReplenishBalanceState.entering_custom_amount_freekassa) diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 23725876..bf5489f4 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -8,9 +8,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,27 +47,6 @@ 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 @@ -109,20 +88,12 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st await add_connection(tg_id, balance=0.0, trial=0) 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 +103,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,12 +113,7 @@ 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) @@ -176,7 +137,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F reply_markup=confirm_keyboard, ) logger.info(f"Payment link sent to user {callback_query.from_user.id}.") - await callback_query.answer() async def robokassa_webhook(request): @@ -238,18 +198,6 @@ 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 @@ -257,7 +205,6 @@ async def process_custom_amount_selection(callback_query: types.CallbackQuery, s await callback_query.message.edit_text(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):") await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) - await callback_query.answer() @router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index 1131079c..8d277426 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -4,9 +4,9 @@ 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,25 +19,6 @@ 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 @@ -84,14 +65,12 @@ async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: 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 +125,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 state.set_state(ReplenishBalanceState.entering_custom_amount_stars) - await callback_query.answer() @router.message(ReplenishBalanceState.entering_custom_amount_stars) diff --git a/handlers/payments/utils.py b/handlers/payments/utils.py new file mode 100644 index 00000000..6f807f53 --- /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..5cec58b3 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -8,9 +8,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,25 +29,6 @@ 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 @@ -79,7 +60,7 @@ 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) @@ -88,19 +69,12 @@ async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, sta 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, + 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 +82,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) @@ -181,27 +143,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F 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 +167,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) diff --git a/handlers/profile.py b/handlers/profile.py index 2b8ffcf9..9b1f52cd 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 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,10 +78,8 @@ 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") @@ -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..7b8ecdf5 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,125 +1,77 @@ 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, restore_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): + await start_command(callback_query.message, state) + + +@router.message(Command("start")) +async def start_command(message: Message, admin: bool): + if message.text: + try: + referrer_tg_id = int(message.text.split("referral_")[1]) + await add_referral(message.from_user.id, referrer_tg_id) + except (ValueError,IndexError): + pass + connection_exists = await check_connection_exists(message.from_user.id) + if not connection_exists: + await add_connection(message.from_user.id) + trial_status = await get_trial(message.from_user.id) 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() +async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): user_id = callback_query.from_user.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 restore_trial(user_id) key_message = ( f"🔑 Ваш персональный ключ доступа:\n" @@ -128,7 +80,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery): ) 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/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/delete.py b/middlewares/delete.py new file mode 100644 index 00000000..3f355ab9 --- /dev/null +++ b/middlewares/delete.py @@ -0,0 +1,20 @@ +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): + await event.delete() + elif isinstance(event, CallbackQuery): + await event.answer() + await event.message.delete() + return await handler(event, data)