From fe59470256e65df5942331d891721058ed18686c Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 01:18:42 +0300 Subject: [PATCH 01/21] Refactoring --- .gitignore | 3 +- Dockerfile | 23 - Makefile | 2 +- backup.py | 3 +- bot.py | 29 +- database.py | 32 +- filters/admin.py | 3 - handlers/admin/admin_commands.py | 156 ----- handlers/admin/admin_coupons.py | 105 +--- handlers/admin/admin_panel.py | 113 ++-- handlers/admin/admin_user_editor.py | 686 +++++++++------------- handlers/commands.py | 17 - handlers/coupons.py | 111 ++-- handlers/donate.py | 46 +- handlers/instructions/instructions.py | 102 +--- handlers/keys/key_management.py | 147 ++--- handlers/keys/key_utils.py | 8 +- handlers/keys/keys.py | 800 ++++++++++---------------- handlers/notifications.py | 6 +- handlers/pay.py | 16 +- handlers/payments/cryprobot_pay.py | 91 +-- handlers/payments/freekassa_pay.py | 31 +- handlers/payments/robokassa_pay.py | 61 +- handlers/payments/stars_pay.py | 41 +- handlers/payments/utils.py | 18 + handlers/payments/yookassa_pay.py | 69 +-- handlers/profile.py | 145 ++--- handlers/start.py | 137 ++--- main.py | 15 +- middlewares/admin.py | 5 +- middlewares/delete.py | 20 + 31 files changed, 1012 insertions(+), 2029 deletions(-) delete mode 100644 Dockerfile delete mode 100644 handlers/admin/admin_commands.py delete mode 100644 handlers/commands.py create mode 100644 handlers/payments/utils.py create mode 100644 middlewares/delete.py 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/main.py b/main.py index 8857fef5..54d2efd0 100644 --- a/main.py +++ b/main.py @@ -55,23 +55,25 @@ async def on_shutdown(app): try: await asyncio.gather(*asyncio.all_tasks(), return_exceptions=True) except Exception as e: - logger.error(f"Error during shutdown: {e}") + logger.error(f"Ошибка при завершении работы: {e}") async def shutdown_site(site): - logger.info("Остановка сайта...") + logger.info("Остановка сайт...") await site.stop() - logger.info("Сервер остановлен.") + logger.info("Сервер сайт.") async def main(): dp.include_router(router) if DEV_MODE: + logger.info("Запуск в режиме разработки...") await bot.delete_webhook() await init_db() await dp.start_polling(bot) else: + logger.info("Запуск в production режиме...") app = web.Application() app.on_startup.append(on_startup) app.on_shutdown.append(on_shutdown) @@ -96,7 +98,7 @@ async def main(): site = web.TCPSite(runner, host=WEBAPP_HOST, port=WEBAPP_PORT) await site.start() - logger.info(f"Webhook URL: {WEBHOOK_URL}") + logger.info(f"URL вебхука: {WEBHOOK_URL}") loop = asyncio.get_event_loop() for sig in (signal.SIGINT, signal.SIGTERM): @@ -107,7 +109,10 @@ async def main(): finally: pending = asyncio.all_tasks() for task in pending: - task.cancel() + try: + task.cancel() + except Exception as e: + logger.error(e) await asyncio.gather(*pending, return_exceptions=True) diff --git a/middlewares/admin.py b/middlewares/admin.py index 70d98204..57aa0692 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -4,7 +4,6 @@ from aiogram import BaseMiddleware from aiogram.types import TelegramObject from config import ADMIN_ID -from logger import logger class AdminMiddleware(BaseMiddleware): @@ -20,10 +19,8 @@ class AdminMiddleware(BaseMiddleware): def _check_admin_access(self, event: TelegramObject) -> bool: try: admin_ids: Union[int, list[int]] = ADMIN_ID - if isinstance(admin_ids, list): return event.from_user.id in admin_ids return event.from_user.id == admin_ids - except Exception as e: - logger.error(f"Ошибка проверки администратора: {e}") + except Exception: return False diff --git a/middlewares/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) From 0867ebdd61be0d448b26ed46df4b9266e1d2e5eb Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 01:19:00 +0300 Subject: [PATCH 02/21] Fix typing --- database.py | 75 ++++++++++---- handlers/admin/admin_panel.py | 2 +- handlers/admin/admin_user_editor.py | 4 +- handlers/coupons.py | 2 +- handlers/donate.py | 4 +- handlers/instructions/instructions.py | 6 +- handlers/keys/key_management.py | 65 +++++------- handlers/keys/keys.py | 140 ++++++++++++-------------- handlers/keys/trial_key.py | 120 ++++++---------------- handlers/pay.py | 2 +- handlers/payments/utils.py | 2 +- handlers/payments/yookassa_pay.py | 2 +- handlers/profile.py | 4 +- handlers/start.py | 20 ++-- middlewares/database.py | 13 ++- 15 files changed, 210 insertions(+), 251 deletions(-) diff --git a/database.py b/database.py index bc3cf86e..a372c3fa 100644 --- a/database.py +++ b/database.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Any import asyncpg @@ -177,19 +178,64 @@ async def delete_coupon_from_db(coupon_code: str): await conn.close() -async def restore_trial(tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) +async def restore_trial(tg_id: int, session: Any): + """ + Восстанавливает возможность использования триального периода для пользователя. + + Args: + tg_id (int): Telegram ID пользователя + session (Any): Сессия базы данных + + Returns: + bool: True, если триал успешно восстановлен, False в случае ошибки + """ try: - await conn.execute("UPDATE connections SET trial = 0 WHERE tg_id = $1", tg_id) + response = await session.execute( + """ + INSERT INTO connections (tg_id, trial) + VALUES ($1, 0) + ON CONFLICT (tg_id) + DO UPDATE SET trial = 0 + """, + tg_id, + ) + logger.info(response) + return True except Exception as e: logger.error(f"Ошибка при установке значения триала: {e}") - finally: - await conn.close() + return False -async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( +async def use_trial(tg_id: int, session: Any): + """ + Отмечает использование триального периода для пользователя. + + Args: + tg_id (int): Telegram ID пользователя + session (Any): Сессия базы данных + + Returns: + bool: True, если триал успешно использован, False в случае ошибки + """ + try: + response = await session.execute( + """ + INSERT INTO connections (tg_id, trial) + VALUES ($1, 1) + ON CONFLICT (tg_id) + DO UPDATE SET trial = 1 + """, + tg_id, + ) + logger.info(response) + return True + except Exception as e: + logger.error(f"Ошибка при использовании триала: {e}") + return False + + +async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None): + await session.execute( """ INSERT INTO connections (tg_id, balance, trial) VALUES ($1, $2, $3) @@ -198,7 +244,6 @@ async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0): balance, trial, ) - await conn.close() async def check_connection_exists(tg_id: int): @@ -298,10 +343,8 @@ async def update_balance(tg_id: int, amount: float): await conn.close() -async def get_trial(tg_id: int) -> int: - conn = await asyncpg.connect(DATABASE_URL) - trial = await conn.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) - await conn.close() +async def get_trial(tg_id: int, session: Any) -> int: + trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) return trial if trial is not None else 0 @@ -316,9 +359,8 @@ async def get_all_users(conn): return await conn.fetch("SELECT tg_id FROM connections") -async def add_referral(referred_tg_id: int, referrer_tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( +async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): + await session.execute( """ INSERT INTO referrals (referred_tg_id, referrer_tg_id) VALUES ($1, $2) @@ -326,7 +368,6 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int): referred_tg_id, referrer_tg_id, ) - await conn.close() async def handle_referral_on_balance_update(tg_id: int, amount: float): diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 6520ffd9..ce7466f6 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -40,7 +40,7 @@ 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="profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer("🤖 Панель администратора", reply_markup=builder.as_markup()) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 6f1ea996..878834f2 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -140,10 +140,10 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: @router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter()) -async def handle_restore_trial(callback_query: types.CallbackQuery): +async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any): tg_id = int(callback_query.data.split("_")[2]) - await restore_trial(tg_id) + await restore_trial(tg_id, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin")) diff --git a/handlers/coupons.py b/handlers/coupons.py index 7d9a5ced..9d79076a 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -20,7 +20,7 @@ router = Router() @router.callback_query(F.data == "activate_coupon") async def handle_activate_coupon(callback_query: types.CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( "🎫 Введите код купона:\n\n" diff --git a/handlers/donate.py b/handlers/donate.py index e6160205..d441463a 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -29,7 +29,7 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext) callback_data="enter_custom_donate_amount", ) ) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( text="🌟 Поддержите наш проект! 💪\n\n" @@ -89,7 +89,7 @@ async def on_successful_donate(message: types.Message, state: FSMContext): try: amount = float(message.successful_payment.invoice_payload.split("_")[0]) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Вернуться в профиль", callback_data="profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer( text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖", reply_markup=builder.as_markup(), diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 16d9094d..ce82d45a 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -22,11 +22,11 @@ async def send_instructions(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL)) builder.row( - InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="profile"), + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"), ) with open(image_path, "rb") as image_from_buffer: - await callback_query.answer_photo( + await callback_query.message.answer_photo( BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"), caption=instructions_message, reply_markup=builder.as_markup(), @@ -59,6 +59,6 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): 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")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) 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 4869f884..7085b2cc 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -6,7 +6,8 @@ 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 +from aiogram.types import CallbackQuery, InlineKeyboardButton, Message +from aiogram.utils.keyboard import InlineKeyboardBuilder 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 @@ -41,19 +42,15 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext, sessio trial_status = existing_connection["trial"] if existing_connection else 0 if trial_status == 1: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="✅ Да, подключить новое устройство", callback_data="confirm_create_new_key") + ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await callback_query.message.answer( text=KEY, - reply_markup=InlineKeyboardMarkup( - inline_keyboard=[ - [ - InlineKeyboardButton( - text="✅ Да, подключить новое устройство", - callback_data="confirm_create_new_key", - ) - ], - [InlineKeyboardButton(text="↩️ Назад", callback_data="profile")], - ] - ), + reply_markup=builder.as_markup(), ) await state.update_data(creating_new_key=True) else: @@ -69,9 +66,9 @@ 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="profile") - keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]]) - await callback_query.message.edit_text(NULL_BALANCE, reply_markup=keyboard) + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await callback_query.message.edit_text(NULL_BALANCE, reply_markup=builder.as_markup()) await state.clear() return @@ -125,11 +122,11 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An else: balance = await get_balance(tg_id) if balance < RENEWAL_PLANS["1"]["price"]: - replenish_button = InlineKeyboardButton(text="Перейти в профиль", callback_data="profile") - keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]]) + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer( "💳 Недостаточно средств для создания подписки на новое устройство. Пополните баланс в личном кабинете.", - reply_markup=keyboard, + reply_markup=builder.as_markup(), ) logger.warning(f"User {tg_id} has insufficient funds for key creation.") await state.clear() @@ -144,29 +141,17 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An logger.info(f"Generated public link for the key: {public_link}") - button_support = InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL) - - button_profile = InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - button_iphone = InlineKeyboardButton(text="🍏 Подключить", url=f"{CONNECT_IOS}{public_link}") - button_android = InlineKeyboardButton( - text="🤖 Подключить", - url=f"{CONNECT_ANDROID}{public_link}", + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL)) + builder.row( + InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS), + InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), ) - - button_download_ios = InlineKeyboardButton(text="🍏 Скачать", url=DOWNLOAD_IOS) - button_download_android = InlineKeyboardButton( - text="🤖 Скачать", - url=DOWNLOAD_ANDROID, - ) - - keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [button_support], - [button_download_ios, button_download_android], - [button_iphone, button_android], - [button_profile], - ] + builder.row( + InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{public_link}"), + InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{public_link}"), ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) remaining_time = expiry_time - current_time days = remaining_time.days @@ -174,7 +159,7 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An logger.info(f"Sending key message to user {tg_id} with the public link.") - await message.answer(key_message, reply_markup=keyboard) + await message.answer(key_message, reply_markup=builder.as_markup()) try: least_loaded_cluster = await get_least_loaded_cluster() diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 7c0626dd..9a668893 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -5,7 +5,8 @@ import os from typing import Any from aiogram import F, Router, types -from aiogram.types import BufferedInputFile +from aiogram.types import BufferedInputFile, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder 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 @@ -44,20 +45,14 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio ) if records: - buttons = [] + builder = InlineKeyboardBuilder() for record in records: key_name = record["email"] - button = types.InlineKeyboardButton( - text=f"🔑 {key_name}", - callback_data=f"view_key|{key_name}", - ) + builder.row(InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}")) - buttons.append([button]) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - back_button = types.InlineKeyboardButton(text="🔙 Назад", callback_data="profile") - buttons.append([back_button]) - - inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) + inline_keyboard = builder.as_markup() response_message = ( "🔑 Список ваших устройств\n\n" "👇 Выберите устройство для управления подпиской:" ) @@ -78,10 +73,11 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio else: response_message = NO_KEYS - create_key_button = types.InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_key") - back_button = types.InlineKeyboardButton(text="🔙 Назад", callback_data="profile") + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_key")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]]) + keyboard = builder.as_markup() image_path = os.path.join("img", "pic_keys.jpg") @@ -135,38 +131,33 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session formatted_expiry_date = expiry_date.strftime("%d %B %Y года") response_message = key_message(key, formatted_expiry_date, days_left_message, server_name) - download_android_button = types.InlineKeyboardButton(text="🤖 Скачать", url=DOWNLOAD_ANDROID) - download_iphone_button = types.InlineKeyboardButton(text="🍏 Скачать", url=DOWNLOAD_IOS) + builder = InlineKeyboardBuilder() - 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}", + builder.row( + InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS), + InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), ) - 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") + builder.row( + InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{key}"), + InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{key}"), + ) - inline_keyboard = [ - [download_iphone_button, download_android_button], - [connect_iphone_button, connect_android_button], - [connect_pc_button], - [renew_button, delete_button], - ] + builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{key_name}")) + + builder.row( + InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"), + InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}"), + ) if not key.startswith(PUBLIC_LINK): - update_subscription_button = types.InlineKeyboardButton( - text="🔄 Обновить подписку", - callback_data=f"update_subscription|{key_name}", + builder.row( + InlineKeyboardButton(text="🔄 Обновить подписку", callback_data=f"update_subscription|{key_name}") ) - inline_keyboard.append([update_subscription_button]) - inline_keyboard.append([back_button]) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard) + keyboard = builder.as_markup() image_path = os.path.join("img", "pic_view.jpg") @@ -251,12 +242,12 @@ async def process_callback_update_subscription(callback_query: types.CallbackQue 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]]) + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( response_message, - reply_markup=keyboard, + reply_markup=builder.as_markup(), ) else: await callback_query.message.answer( @@ -309,36 +300,31 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio 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")], - ] + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', + callback_data=f"renew_plan|1|{client_id}", + ) + ) + builder.row( + InlineKeyboardButton( + text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)', + callback_data=f"renew_plan|3|{client_id}", + ) + ) + builder.row( + InlineKeyboardButton( + text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)', + callback_data=f"renew_plan|6|{client_id}", + ) + ) + builder.row( + InlineKeyboardButton( + text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)', + callback_data=f"renew_plan|12|{client_id}", + ) ) - balance = await get_balance(tg_id) response_message = PLAN_SELECTION_MSG.format( @@ -348,7 +334,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio await callback_query.message.answer( text=response_message, - reply_markup=keyboard, + reply_markup=builder.as_markup(), ) else: await callback_query.message.answer("Ключ не найден.") @@ -434,21 +420,21 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi 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]]) + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="Пополнить баланс", callback_data="pay")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( INSUFFICIENT_FUNDS_MSG, - reply_markup=keyboard, + reply_markup=builder.as_markup(), ) 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]]) + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await callback_query.message.answer(response_message, reply_markup=keyboard) + await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) async def renew_key_on_servers(): tasks = [] diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 85f02c33..13c74362 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -1,107 +1,53 @@ -import asyncio from datetime import datetime, timedelta +from typing import Any import uuid -import asyncpg from py3xui import AsyncApi from client import add_client -from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, PUBLIC_LINK, TRIAL_TIME -from database import store_key +from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, PUBLIC_LINK, TRIAL_TIME +from database import store_key, use_trial from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email, get_least_loaded_cluster -async def create_trial_key(tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) - try: - client_id = str(uuid.uuid4()) - email = generate_random_email() +async def create_trial_key(tg_id: int, session: Any): + client_id = str(uuid.uuid4()) + email = generate_random_email() + public_link = f"{PUBLIC_LINK}{email}/{tg_id}" + instructions = INSTRUCTIONS + result = {"key": public_link, "instructions": instructions} + current_time = datetime.utcnow() + expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3) + expiry_timestamp = int(expiry_time.timestamp() * 1000) - public_link = f"{PUBLIC_LINK}{email}/{tg_id}" - instructions = INSTRUCTIONS + least_loaded_cluster = await get_least_loaded_cluster() + for server_id, server in CLUSTERS[least_loaded_cluster].items(): + xui = AsyncApi( + CLUSTERS[least_loaded_cluster][server_id]["API_URL"], + username=ADMIN_USERNAME, + password=ADMIN_PASSWORD, + ) - result = {"key": public_link, "instructions": instructions} - - asyncio.create_task(generate_and_store_keys(tg_id, client_id, email, public_link)) - - return result - - finally: - await conn.close() - - -async def generate_and_store_keys(tg_id: int, client_id: str, email: str, public_link: str): - conn = await asyncpg.connect(DATABASE_URL) - try: - current_time = datetime.utcnow() - expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3) - expiry_timestamp = int(expiry_time.timestamp() * 1000) - - least_loaded_cluster = await get_least_loaded_cluster() - - tasks = [] - for server_id, server in CLUSTERS[least_loaded_cluster].items(): - task = create_key_on_server( - least_loaded_cluster, - server_id, - client_id, - email, - tg_id, - expiry_timestamp, - ) - tasks.append(task) - - await asyncio.gather(*tasks) - - await store_key( - tg_id, + await add_client( + xui, client_id, email, - expiry_timestamp, - public_link, - server_id=least_loaded_cluster, - ) - - await conn.execute( - """ - INSERT INTO connections (tg_id, trial) - VALUES ($1, 1) - ON CONFLICT (tg_id) - DO UPDATE SET trial = 1 - """, tg_id, + limit_ip=1, + total_gb=0, + expiry_time=expiry_timestamp, + enable=True, + flow="xtls-rprx-vision", ) - finally: - await conn.close() - -async def create_key_on_server( - cluster_id: str, - server_id: str, - client_id: str, - email: str, - tg_id: int, - expiry_timestamp: int, -): - """Создает ключ на сервере в указанном кластере и возвращает результат.""" - - xui = AsyncApi( - CLUSTERS[cluster_id][server_id]["API_URL"], - username=ADMIN_USERNAME, - password=ADMIN_PASSWORD, - ) - - response = await add_client( - xui, + await store_key( + tg_id, client_id, email, - tg_id, - limit_ip=1, - total_gb=0, - expiry_time=expiry_timestamp, - enable=True, - flow="xtls-rprx-vision", + expiry_timestamp, + public_link, + server_id=least_loaded_cluster, ) - - return response + await use_trial(tg_id, session) + return result diff --git a/handlers/pay.py b/handlers/pay.py index 06ff1dc3..9c588072 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -48,7 +48,7 @@ async def handle_pay(callback_query: CallbackQuery): ) builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon")) - builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( "💸 Выберите удобный способ пополнения баланса:\n\n" diff --git a/handlers/payments/utils.py b/handlers/payments/utils.py index 6f807f53..21015090 100644 --- a/handlers/payments/utils.py +++ b/handlers/payments/utils.py @@ -8,7 +8,7 @@ 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")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await bot.send_message( chat_id=user_id, text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index 5cec58b3..0278fa0f 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -60,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="profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) key_count = await get_key_count(tg_id) diff --git a/handlers/profile.py b/handlers/profile.py index 9b1f52cd..2ab7cffe 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -66,7 +66,7 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta @router.callback_query(F.data == "view_tariffs") async def view_tariffs_handler(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="profile")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( "🚀 Доступные тарифы VPN:\n\n" @@ -94,7 +94,7 @@ 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="profile")) + 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( diff --git a/handlers/start.py b/handlers/start.py index 7b8ecdf5..8737f2cc 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -8,30 +8,32 @@ from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder 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 database import add_connection, add_referral, check_connection_exists, get_trial, use_trial from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn +from logger import logger router = Router() @router.callback_query(F.data == "start") -async def handle_start_callback_query(callback_query: CallbackQuery, state: FSMContext): - await start_command(callback_query.message, state) +async def handle_start_callback_query(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool): + await start_command(callback_query.message, state, session, admin) @router.message(Command("start")) -async def start_command(message: Message, admin: bool): +async def start_command(message: Message, state: FSMContext, session: Any, admin: bool): if message.text: try: referrer_tg_id = int(message.text.split("referral_")[1]) - await add_referral(message.from_user.id, referrer_tg_id) - except (ValueError,IndexError): + await add_referral(message.from_user.id, referrer_tg_id, session) + 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) + await add_connection(message.from_user.id, session) + trial_status = await get_trial(message.from_user.id, session) + logger.info(f'trial_status {trial_status}') image_path = os.path.join("img", "pic.jpg") builder = InlineKeyboardBuilder() @@ -71,7 +73,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): if "error" in trial_key_info: await callback_query.message.answer(trial_key_info["error"]) else: - await restore_trial(user_id) + await use_trial(user_id, session) key_message = ( f"🔑 Ваш персональный ключ доступа:\n" diff --git a/middlewares/database.py b/middlewares/database.py index d529dad4..b1687aa6 100644 --- a/middlewares/database.py +++ b/middlewares/database.py @@ -14,10 +14,9 @@ class DatabaseMiddleware(BaseMiddleware): event: TelegramObject, data: Dict[str, Any], ) -> Any: - async with await asyncpg.create_pool(DATABASE_URL) as pool: - async with pool.acquire() as session: - data["session"] = session - try: - return await handler(event, data) - finally: - await pool.release(session) + conn = await asyncpg.connect(DATABASE_URL) + try: + data["session"] = conn + return await handler(event, data) + finally: + await conn.close() From cfc3cedc2e85f317fcc57dba2855a3dbb9dc7451 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 01:34:39 +0300 Subject: [PATCH 03/21] Add notify_inactive_trial_users --- handlers/notifications.py | 49 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/handlers/notifications.py b/handlers/notifications.py index 280fb145..ba6438e0 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -6,7 +6,7 @@ import asyncpg from py3xui import AsyncApi from client import delete_client -from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB +from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB, TRIAL_TIME from database import delete_key, get_balance, update_balance, update_key_expiry from handlers.keys.key_utils import renew_key_in_cluster from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS @@ -27,6 +27,8 @@ async def notify_expiring_keys(bot: Bot): logger.info("Начало обработки уведомлений.") + await notify_inactive_trial_users(bot, conn) + await asyncio.sleep(1) await notify_10h_keys(bot, conn, current_time, threshold_time_10h) await asyncio.sleep(1) await notify_24h_keys(bot, conn, current_time, threshold_time_24h) @@ -212,6 +214,51 @@ async def notify_24h_keys( await asyncio.sleep(1) +async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): + logger.info("Проверка пользователей, не активировавших пробный период...") + + inactive_trial_users = await conn.fetch( + """ + SELECT tg_id, username FROM users + WHERE tg_id IN ( + SELECT tg_id FROM connections + WHERE trial = 0 + ) AND tg_id NOT IN ( + SELECT DISTINCT tg_id FROM keys + ) + """ + ) + logger.info(f"Найдено {len(inactive_trial_users)} неактивных пользователей.") + + for user in inactive_trial_users: + tg_id = user['tg_id'] + username = user.get('username', 'Пользователь') + + try: + if not await is_bot_blocked(bot, tg_id): + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key")], + [types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")], + ] + ) + + message = ( + f"👋 Привет, {username}!\n\n" + f"🎉 У тебя есть бесплатный пробный период на {TRIAL_TIME} дней!\n" + "🕒 Не упусти возможность попробовать наш VPN прямо сейчас.\n\n" + "💡 Нажми на кнопку ниже, чтобы активировать пробный доступ." + ) + + await bot.send_message(tg_id, message, reply_markup=keyboard) + logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.") + + except Exception as e: + logger.error(f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}") + + await asyncio.sleep(1) # Небольшая задержка между отправками + + async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float): logger.info("Проверка истекших ключей...") From a672a0ddb0a994966b3726720e0fcc0006a29a4d Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 01:47:37 +0300 Subject: [PATCH 04/21] Fix --- database.py | 108 ++++++++++++++++++-------------- handlers/admin/admin_coupons.py | 14 +++-- 2 files changed, 68 insertions(+), 54 deletions(-) diff --git a/database.py b/database.py index 7b5853af..4edc4d7d 100644 --- a/database.py +++ b/database.py @@ -9,22 +9,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( """ @@ -140,31 +124,49 @@ async def init_db(): await conn.close() -async def create_coupon(coupon_code: str, amount: float, usage_limit: int): - conn = await asyncpg.connect(DATABASE_URL) +async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any): + """ + Создает новый купон в базе данных. + Args: + coupon_code (str): Уникальный код купона. + amount (float): Сумма, которую дает купон. + usage_limit (int): Максимальное количество использований купона. + session (Any): Сессия базы данных для выполнения запроса. + + Raises: + Exception: В случае ошибки при создании купона. + + Example: + await create_coupon('SALE50', 50.0, 5, session) + """ + await session.execute( + """ + INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used) + VALUES ($1, $2, $3, 0, FALSE) + """, + coupon_code, + amount, + usage_limit, + ) + + +async def get_all_coupons(session: Any): + """ + Получает список всех купонов из базы данных. + + Returns: + list: Список словарей с информацией о купонах, каждый словарь содержит: + - code (str): Код купона + - amount (int): Сумма купона + - usage_limit (int): Максимальное количество использований + - usage_count (int): Текущее количество использований купона + + Raises: + Exception: В случае ошибки при получении данных из базы + """ try: - await conn.execute( - """ - INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used) - VALUES ($1, $2, $3, 0, FALSE) - """, - coupon_code, - amount, - usage_limit, - ) - except Exception as e: - logger.error(f"Ошибка при создании купона: {e}") - raise - finally: - await conn.close() - - -async def get_all_coupons(): - """Получить список всех купонов""" - conn = await asyncpg.connect(DATABASE_URL) - try: - coupons = await conn.fetch( + coupons = await session.fetch( """ SELECT code, amount, usage_limit, usage_count FROM coupons @@ -174,16 +176,28 @@ async def get_all_coupons(): except Exception as e: logger.error(f"Ошибка при получении купонов: {e}") return [] - finally: - await conn.close() -async def delete_coupon_from_db(coupon_code: str): - """Удалить купон из базы данных по его коду""" - conn = await asyncpg.connect(DATABASE_URL) +async def delete_coupon_from_db(coupon_code: str, session: Any): + """ + Удаляет купон из базы данных по его коду. + + Args: + coupon_code (str): Код купона для удаления + session (Any): Сессия базы данных для выполнения запроса + + Returns: + bool: True, если купон успешно удален, False если купон не найден или произошла ошибка + + Raises: + Exception: В случае ошибки при выполнении запроса к базе данных + + Example: + result = await delete_coupon_from_db('SALE50', session) + """ try: - coupon_record = await conn.fetchrow( + coupon_record = await session.fetchrow( """ SELECT id FROM coupons WHERE code = $1 """, @@ -193,7 +207,7 @@ async def delete_coupon_from_db(coupon_code: str): if not coupon_record: return False - await conn.execute( + await session.execute( """ DELETE FROM coupons WHERE code = $1 """, @@ -205,8 +219,6 @@ async def delete_coupon_from_db(coupon_code: str): except Exception as e: logger.error(f"Ошибка при удалении купона: {e}") return False - finally: - await conn.close() async def restore_trial(tg_id: int, session: Any): diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 49b0f23e..5f7a3904 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -1,3 +1,5 @@ +from typing import Any + from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup @@ -27,9 +29,9 @@ async def show_coupon_management_menu(callback_query: types.CallbackQuery, state @router.callback_query(F.data == "coupons", IsAdminFilter()) -async def show_coupon_list(callback_query: types.CallbackQuery): +async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): try: - coupons = await get_all_coupons() + coupons = await get_all_coupons(session) if not coupons: builder = InlineKeyboardBuilder() @@ -67,11 +69,11 @@ async def show_coupon_list(callback_query: types.CallbackQuery): @router.callback_query(F.data.startswith("delete_coupon_"), IsAdminFilter()) -async def handle_delete_coupon(callback_query: types.CallbackQuery): +async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any): coupon_code = callback_query.data[len("delete_coupon_") :] try: - result = await delete_coupon_from_db(coupon_code) + result = await delete_coupon_from_db(coupon_code, session) if result: await show_coupon_list(callback_query) @@ -100,7 +102,7 @@ async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMCo @router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter()) -async def process_coupon_data(message: types.Message, state: FSMContext): +async def process_coupon_data(message: types.Message, state: FSMContext, session: Any): text = message.text.strip() parts = text.split() @@ -130,7 +132,7 @@ async def process_coupon_data(message: types.Message, state: FSMContext): return try: - await create_coupon(coupon_code, coupon_amount, usage_limit) + await create_coupon(coupon_code, coupon_amount, usage_limit, session) result_message = ( f"✅ Купон с кодом {coupon_code} успешно создан! 🎉\n" From 53dc006f2d37fce1237b0680c59c433f5deb94e1 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 01:57:42 +0300 Subject: [PATCH 05/21] Update InlineKeyboardBuilder --- handlers/notifications.py | 79 +++++++++++++++------------------------ 1 file changed, 31 insertions(+), 48 deletions(-) diff --git a/handlers/notifications.py b/handlers/notifications.py index ba6438e0..505e0db2 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -2,6 +2,7 @@ import asyncio from datetime import datetime, timedelta from aiogram import Bot, Router, types +from aiogram.utils.keyboard import InlineKeyboardBuilder import asyncpg from py3xui import AsyncApi @@ -97,28 +98,12 @@ async def notify_10h_keys( if not await is_bot_blocked(bot, tg_id): try: - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text="🔄 Продлить VPN", - callback_data=f'renew_key|{record["client_id"]}', - ) - ], - [ - types.InlineKeyboardButton( - text="💳 Пополнить баланс", - callback_data="pay", - ) - ], - [ - types.InlineKeyboardButton( - text="👤 Личный кабинет", - callback_data="profile", - ) - ], - ] - ) + keyboard = types.InlineKeyboardBuilder() + keyboard.button(text="🔄 Продлить VPN", callback_data=f'renew_key|{record["client_id"]}') + keyboard.button(text="💳 Пополнить баланс", callback_data="pay") + keyboard.button(text="👤 Личный кабинет", callback_data="profile") + keyboard.adjust(1) + keyboard = keyboard.as_markup() await bot.send_message(tg_id, message, reply_markup=keyboard) logger.info(f"Уведомление отправлено пользователю {tg_id}.") except Exception as e: @@ -177,28 +162,26 @@ async def notify_24h_keys( if not await is_bot_blocked(bot, tg_id): try: - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text="🔄 Продлить VPN", - callback_data=f'renew_key|{record["client_id"]}', - ) - ], - [ - types.InlineKeyboardButton( - text="💳 Пополнить баланс", - callback_data="pay", - ) - ], - [ - types.InlineKeyboardButton( - text="👤 Личный кабинет", - callback_data="profile", - ) - ], - ] + builder = InlineKeyboardBuilder() + builder.row( + types.InlineKeyboardButton( + text="🔄 Продлить VPN", + callback_data=f'renew_key|{record["client_id"]}', + ) ) + builder.row( + types.InlineKeyboardButton( + text="💳 Пополнить баланс", + callback_data="pay", + ) + ) + builder.row( + types.InlineKeyboardButton( + text="👤 Личный кабинет", + callback_data="profile", + ) + ) + keyboard = builder.as_markup() await bot.send_message(tg_id, message_24h, reply_markup=keyboard) logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.") except Exception as e: @@ -236,12 +219,12 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): try: if not await is_bot_blocked(bot, tg_id): - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key")], - [types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")], - ] + builder = InlineKeyboardBuilder() + builder.row( + types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key") ) + builder.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + keyboard = builder.as_markup() message = ( f"👋 Привет, {username}!\n\n" From 7d4a33e6796cdc9a580f9673e43c4edfc7881ace Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 08:13:52 +0300 Subject: [PATCH 06/21] Fix --- handlers/keys/key_management.py | 4 ++-- handlers/payments/freekassa_pay.py | 2 +- handlers/payments/robokassa_pay.py | 4 ++-- handlers/payments/stars_pay.py | 2 +- handlers/payments/yookassa_pay.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 7085b2cc..c1f10428 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -68,13 +68,13 @@ async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContex if balance < RENEWAL_PLANS["1"]["price"]: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await callback_query.message.edit_text(NULL_BALANCE, reply_markup=builder.as_markup()) + await callback_query.message.answer(NULL_BALANCE, reply_markup=builder.as_markup()) await state.clear() return logger.info(f"Balance for user {tg_id} is sufficient. Asking for device name.") - await callback_query.message.edit_text("🔑 Пожалуйста, введите имя подключаемого устройства:") + await callback_query.message.answer("🔑 Пожалуйста, введите имя подключаемого устройства:") await state.set_state(Form.waiting_for_key_name) logger.info(f"State set to waiting_for_key_name for user {tg_id}") await state.update_data(creating_new_key=True) diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 6c91b841..88ebe04f 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -154,7 +154,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F @router.callback_query(F.data == "enter_custom_amount_freekassa") async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.edit_text(text="Введите сумму пополнения:") + await callback_query.message.answer(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount_freekassa) diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index bf5489f4..ea1c332f 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -132,7 +132,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F ] ) - await callback_query.message.edit_text( + await callback_query.message.answer( text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:", reply_markup=confirm_keyboard, ) @@ -203,7 +203,7 @@ async def process_custom_amount_selection(callback_query: types.CallbackQuery, s tg_id = callback_query.from_user.id logger.info(f"User {tg_id} chose to enter a custom amount.") - await callback_query.message.edit_text(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):") + await callback_query.message.answer(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):") await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index 8d277426..a0db97a4 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -128,7 +128,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F @router.callback_query(F.data == "enter_custom_amount_stars") async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.edit_text(text="Введите сумму пополнения:") + await callback_query.message.answer(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount_stars) diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index 0278fa0f..5dddef4b 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -138,7 +138,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F ] ) - await callback_query.message.edit_text( + await callback_query.message.answer( text=f"Вы выбрали пополнение на {amount} рублей.", reply_markup=confirm_keyboard, ) From fb64afe06e958c531e6d194e53073fd9b1abcb36 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 08:27:27 +0300 Subject: [PATCH 07/21] Fix trial_status --- handlers/keys/key_management.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index c1f10428..a1afa07a 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -10,7 +10,7 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder 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 database import add_connection, get_balance, get_trial, store_key, update_balance from handlers.keys.key_utils import create_key_on_cluster 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 @@ -34,13 +34,8 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC 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 - + trial_status = get_trial(callback_query.message.from_user.id,session) + logger.info(f'trial_status {trial_status}') if trial_status == 1: builder = InlineKeyboardBuilder() builder.row( From 592afcf56ebc9ef66075cade251987cc9615e442 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 09:15:09 +0300 Subject: [PATCH 08/21] Add logging --- database.py | 843 ++++++++++++++++++++++++-------- handlers/keys/key_management.py | 23 +- 2 files changed, 643 insertions(+), 223 deletions(-) diff --git a/database.py b/database.py index 4edc4d7d..c660a762 100644 --- a/database.py +++ b/database.py @@ -140,15 +140,20 @@ async def create_coupon(coupon_code: str, amount: float, usage_limit: int, sessi Example: await create_coupon('SALE50', 50.0, 5, session) """ - await session.execute( - """ - INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used) - VALUES ($1, $2, $3, 0, FALSE) - """, - coupon_code, - amount, - usage_limit, - ) + try: + await session.execute( + """ + INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used) + VALUES ($1, $2, $3, 0, FALSE) + """, + coupon_code, + amount, + usage_limit, + ) + logger.info(f"Успешно создан купон с кодом {coupon_code} на сумму {amount}") + except Exception as e: + logger.error(f"Ошибка при создании купона {coupon_code}: {e}") + raise async def get_all_coupons(session: Any): @@ -166,15 +171,22 @@ async def get_all_coupons(session: Any): Exception: В случае ошибки при получении данных из базы """ try: + # Выполняем запрос на получение всех купонов из базы данных coupons = await session.fetch( """ SELECT code, amount, usage_limit, usage_count FROM coupons """ ) + + # Логируем успешное получение списка купонов + logger.info(f"Успешно получено {len(coupons)} купонов из базы данных") + return coupons except Exception as e: - logger.error(f"Ошибка при получении купонов: {e}") + # Подробное логирование ошибки при получении купонов + logger.error(f"Критическая ошибка при получении списка купонов: {e}") + logger.exception("Трассировка стека ошибки получения купонов") return [] @@ -196,7 +208,7 @@ async def delete_coupon_from_db(coupon_code: str, session: Any): result = await delete_coupon_from_db('SALE50', session) """ try: - + # Проверяем существование купона в базе данных coupon_record = await session.fetchrow( """ SELECT id FROM coupons WHERE code = $1 @@ -204,9 +216,12 @@ async def delete_coupon_from_db(coupon_code: str, session: Any): coupon_code, ) + # Если купон не найден, возвращаем False if not coupon_record: + logger.info(f"Купон {coupon_code} не найден в базе данных") return False + # Удаляем купон из базы данных await session.execute( """ DELETE FROM coupons WHERE code = $1 @@ -214,10 +229,13 @@ async def delete_coupon_from_db(coupon_code: str, session: Any): coupon_code, ) + # Логируем успешное удаление купона + logger.info(f"Купон {coupon_code} успешно удален из базы данных") return True except Exception as e: - logger.error(f"Ошибка при удалении купона: {e}") + # Логируем ошибку на русском с подробным описанием + logger.error(f"Произошла ошибка при удалении купона {coupon_code}: {e}") return False @@ -233,7 +251,7 @@ async def restore_trial(tg_id: int, session: Any): bool: True, если триал успешно восстановлен, False в случае ошибки """ try: - response = await session.execute( + await session.execute( """ INSERT INTO connections (tg_id, trial) VALUES ($1, 0) @@ -242,10 +260,10 @@ async def restore_trial(tg_id: int, session: Any): """, tg_id, ) - logger.info(response) + logger.info(f"Триальный период успешно восстановлен для пользователя {tg_id}") return True except Exception as e: - logger.error(f"Ошибка при установке значения триала: {e}") + logger.error(f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}") return False @@ -261,7 +279,7 @@ async def use_trial(tg_id: int, session: Any): bool: True, если триал успешно использован, False в случае ошибки """ try: - response = await session.execute( + await session.execute( """ INSERT INTO connections (tg_id, trial) VALUES ($1, 1) @@ -270,273 +288,643 @@ async def use_trial(tg_id: int, session: Any): """, tg_id, ) - logger.info(response) + logger.info(f"Триальный период успешно использован для пользователя {tg_id}") return True except Exception as e: - logger.error(f"Ошибка при использовании триала: {e}") + logger.error(f"Ошибка при использовании триала для пользователя {tg_id}: {e}") return False async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None): - await session.execute( - """ - INSERT INTO connections (tg_id, balance, trial) - VALUES ($1, $2, $3) - """, - tg_id, - balance, - trial, - ) + """ + Добавляет новое подключение для пользователя в базу данных. + + Args: + tg_id (int): Telegram ID пользователя + balance (float, optional): Начальный баланс пользователя. По умолчанию 0.0. + trial (int, optional): Статус триального периода. По умолчанию 0. + session (Any, optional): Сессия базы данных. + + Raises: + Exception: Если возникает ошибка при добавлении подключения в базу данных. + """ + try: + await session.execute( + """ + INSERT INTO connections (tg_id, balance, trial) + VALUES ($1, $2, $3) + """, + tg_id, + balance, + trial, + ) + logger.info( + f"Успешно добавлено новое подключение для пользователя {tg_id} с балансом {balance} и статусом триала {trial}" + ) + except Exception as e: + logger.error(f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}") + raise async def check_connection_exists(tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) - exists = await conn.fetchval( - """ - SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1) - """, - tg_id, - ) - await conn.close() - return exists + """ + Проверяет существование подключения для указанного пользователя в базе данных. + + Args: + tg_id (int): Telegram ID пользователя для проверки. + + Returns: + bool: True, если подключение существует, иначе False. + + Raises: + Exception: В случае ошибки при подключении к базе данных. + """ + try: + conn = await asyncpg.connect(DATABASE_URL) + exists = await conn.fetchval( + """ + SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1) + """, + tg_id, + ) + logger.info( + f"Проверка существования подключения для пользователя {tg_id}: {'найдено' if exists else 'не найдено'}" + ) + return exists + except Exception as e: + logger.error(f"Ошибка при проверке подключения для пользователя {tg_id}: {e}") + raise + finally: + if conn: + await conn.close() -async def store_key( - tg_id: int, - client_id: str, - email: str, - expiry_time: int, - key: str, - server_id: str, -): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id) - VALUES ($1, $2, $3, $4, $5, $6, $7) - """, - tg_id, - client_id, - email, - int(datetime.utcnow().timestamp() * 1000), - expiry_time, - key, - server_id, - ) - await conn.close() +async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str, session: Any): + """ + Сохраняет информацию о ключе в базу данных. + + Args: + tg_id (int): Telegram ID пользователя + client_id (str): Уникальный идентификатор клиента + email (str): Электронная почта или имя устройства + expiry_time (int): Время истечения ключа в миллисекундах + key (str): Ключ доступа + server_id (str): Идентификатор сервера + + Raises: + Exception: Если возникает ошибка при сохранении ключа в базу данных + """ + try: + await session.execute( + """ + INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id) + VALUES ($1, $2, $3, $4, $5, $6, $7) + """, + tg_id, + client_id, + email, + int(datetime.utcnow().timestamp() * 1000), + expiry_time, + key, + server_id, + ) + logger.info(f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}") + except Exception as e: + logger.error(f"Ошибка при сохранении ключа для пользователя {tg_id}: {e}") + raise async def get_keys(tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) - records = await conn.fetch( - """ - SELECT client_id, email, created_at, key - FROM keys - WHERE tg_id = $1 - """, - tg_id, - ) - await conn.close() - return records + """ + Получает список ключей для указанного пользователя. + + Args: + tg_id (int): Telegram ID пользователя + + Returns: + list: Список записей ключей с информацией о клиенте, электронной почте, времени создания и ключе + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + records = await conn.fetch( + """ + SELECT client_id, email, created_at, key + FROM keys + WHERE tg_id = $1 + """, + tg_id, + ) + logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id}") + return records + except Exception as e: + logger.error(f"Ошибка при получении ключей для пользователя {tg_id}: {e}") + raise + finally: + if conn: + await conn.close() async def get_keys_by_server(tg_id: int, server_id: str): - conn = await asyncpg.connect(DATABASE_URL) - records = await conn.fetch( - """ - SELECT client_id, email, created_at, key - FROM keys - WHERE tg_id = $1 AND server_id = $2 - """, - tg_id, - server_id, - ) - await conn.close() - return records + """ + Получает список ключей для указанного пользователя на определенном сервере. + + Args: + tg_id (int): Telegram ID пользователя + server_id (str): Идентификатор сервера + + Returns: + list: Список записей ключей с информацией о клиенте, электронной почте, времени создания и ключе + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + records = await conn.fetch( + """ + SELECT client_id, email, created_at, key + FROM keys + WHERE tg_id = $1 AND server_id = $2 + """, + tg_id, + server_id, + ) + logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}") + return records + except Exception as e: + logger.error(f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}") + raise + finally: + if conn: + await conn.close() async def has_active_key(tg_id: int) -> bool: - conn = await asyncpg.connect(DATABASE_URL) - count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - await conn.close() - return count > 0 + """ + Проверяет наличие активных ключей для указанного пользователя. + + Args: + tg_id (int): Telegram ID пользователя + + Returns: + bool: True, если у пользователя есть активные ключи, иначе False + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) + logger.info(f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}") + return count > 0 + except Exception as e: + logger.error(f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}") + raise + finally: + if conn: + await conn.close() async def get_balance(tg_id: int) -> float: - conn = await asyncpg.connect(DATABASE_URL) - balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - await conn.close() - return balance if balance is not None else 0.0 + """ + Получает баланс пользователя из базы данных. + + Args: + tg_id (int): Telegram ID пользователя + + Returns: + float: Баланс пользователя, 0.0 если баланс не найден + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + logger.info(f"Получен баланс для пользователя {tg_id}: {balance}") + return balance if balance is not None else 0.0 + except Exception as e: + logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}") + return 0.0 + finally: + if conn: + await conn.close() async def update_balance(tg_id: int, amount: float): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - UPDATE connections - SET balance = balance + $1 - WHERE tg_id = $2 - """, - amount, - tg_id, - ) + """ + Обновляет баланс пользователя в базе данных. - await handle_referral_on_balance_update(tg_id, amount) + Args: + tg_id (int): Telegram ID пользователя + amount (float): Сумма для обновления баланса - await conn.close() + Raises: + Exception: В случае ошибки при подключении к базе данных или обновлении баланса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute( + """ + UPDATE connections + SET balance = balance + $1 + WHERE tg_id = $2 + """, + amount, + tg_id, + ) + logger.info(f"Баланс пользователя {tg_id} обновлен на сумму {amount}") + + await handle_referral_on_balance_update(tg_id, amount) + + except Exception as e: + logger.error(f"Ошибка при обновлении баланса для пользователя {tg_id}: {e}") + raise + finally: + if conn: + await conn.close() async def get_trial(tg_id: int, session: Any) -> int: - trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) - return trial if trial is not None else 0 + """ + Получает статус триала для пользователя из базы данных. + + Args: + tg_id (int): Telegram ID пользователя + session (Any): Сессия базы данных + + Returns: + int: Статус триала (0 - не использован, 1 - использован) + """ + try: + trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) + logger.info(f"Получен статус триала для пользователя {tg_id}: {trial}") + return trial if trial is not None else 0 + except Exception as e: + logger.error(f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}") + return 0 async def get_key_count(tg_id: int) -> int: - conn = await asyncpg.connect(DATABASE_URL) - count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - await conn.close() - return count if count is not None else 0 + """ + Получает количество ключей для указанного пользователя. + + Args: + tg_id (int): Telegram ID пользователя + + Returns: + int: Количество ключей пользователя, 0 если ключей нет + + Raises: + Exception: В случае ошибки при подключении к базе данных + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) + logger.info(f"Получено количество ключей для пользователя {tg_id}: {count}") + return count if count is not None else 0 + except Exception as e: + logger.error(f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}") + return 0 + finally: + if conn: + await conn.close() async def get_all_users(conn): - return await conn.fetch("SELECT tg_id FROM connections") + """ + Получает список всех пользователей из базы данных. + + Args: + conn: Подключение к базе данных + + Returns: + list: Список Telegram ID всех пользователей + + Raises: + Exception: В случае ошибки при получении данных + """ + try: + пользователи = await conn.fetch("SELECT tg_id FROM connections") + logger.info(f"Получен список всех пользователей. Количество: {len(пользователи)}") + return пользователи + except Exception as e: + logger.error(f"Ошибка при получении списка пользователей: {e}") + raise async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): - await session.execute( - """ - INSERT INTO referrals (referred_tg_id, referrer_tg_id) - VALUES ($1, $2) - """, - referred_tg_id, - referrer_tg_id, - ) + try: + # Вставка записи о реферале в базу данных + await session.execute( + """ + INSERT INTO referrals (referred_tg_id, referrer_tg_id) + VALUES ($1, $2) + """, + referred_tg_id, + referrer_tg_id, + ) + logger.info(f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}") + except Exception as e: + logger.error(f"Ошибка при добавлении реферала: {e}") + raise async def handle_referral_on_balance_update(tg_id: int, amount: float): - conn = await asyncpg.connect(DATABASE_URL) + """ + Обработка реферальной системы при обновлении баланса. - referral = await conn.fetchrow( - """ - SELECT referrer_tg_id FROM referrals WHERE referred_tg_id = $1 - """, - tg_id, - ) + Эта функция проверяет, есть ли у пользователя реферер, + и если есть, начисляет бонус за пополнение баланса. - if referral: - referrer_tg_id = referral["referrer_tg_id"] + Args: + tg_id (int): Telegram ID пользователя + amount (float): Сумма пополнения баланса - bonus = amount * BONUS_PERCENT + Returns: + None + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для обработки реферала пользователя {tg_id}") - if bonus < 0: - bonus = 0 - - await update_balance(referrer_tg_id, bonus) - - await conn.execute( + referral = await conn.fetchrow( """ - UPDATE referrals SET reward_issued = TRUE - WHERE referrer_tg_id = $1 AND referred_tg_id = $2 - """, - referrer_tg_id, + SELECT referrer_tg_id FROM referrals WHERE referred_tg_id = $1 + """, tg_id, ) - await conn.close() + if referral: + referrer_tg_id = referral["referrer_tg_id"] + logger.info(f"Найден реферер {referrer_tg_id} для пользователя {tg_id}") + + bonus = amount * BONUS_PERCENT + bonus = max(bonus, 0) # Гарантируем неотрицательный бонус + + logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id}") + await update_balance(referrer_tg_id, bonus) + + await conn.execute( + """ + UPDATE referrals SET reward_issued = TRUE + WHERE referrer_tg_id = $1 AND referred_tg_id = $2 + """, + referrer_tg_id, + tg_id, + ) + logger.info(f"Обновлен статус реферала для {referrer_tg_id}") + + except Exception as e: + logger.error(f"Ошибка при обработке реферала для пользователя {tg_id}: {e}") + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def get_referral_stats(referrer_tg_id: int): - conn = await asyncpg.connect(DATABASE_URL) - total_referrals = await conn.fetchval( - """ - SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 - """, - referrer_tg_id, - ) + """ + Получение статистики рефералов для указанного пользователя. - active_referrals = await conn.fetchval( - """ - SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE - """, - referrer_tg_id, - ) + Args: + referrer_tg_id (int): Telegram ID пользователя, для которого запрашивается статистика рефералов. - await conn.close() + Returns: + dict: Словарь со статистикой рефералов, содержащий: + - total_referrals (int): Общее количество рефералов + - active_referrals (int): Количество активных рефералов (с начисленным бонусом) - return { - "total_referrals": total_referrals, - "active_referrals": active_referrals, - } + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info( + f"Установлено подключение к базе данных для получения статистики рефералов пользователя {referrer_tg_id}" + ) + + total_referrals = await conn.fetchval( + """ + SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 + """, + referrer_tg_id, + ) + logger.debug(f"Получено общее количество рефералов: {total_referrals}") + + active_referrals = await conn.fetchval( + """ + SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE + """, + referrer_tg_id, + ) + logger.debug(f"Получено количество активных рефералов: {active_referrals}") + + return { + "total_referrals": total_referrals, + "active_referrals": active_referrals, + } + + except Exception as e: + logger.error(f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}") + raise + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def update_key_expiry(client_id: str, new_expiry_time: int): """ - Обновление времени истечения ключа на новое значение. + Обновление времени истечения ключа для указанного клиента. + + Args: + client_id (str): Уникальный идентификатор клиента + new_expiry_time (int): Новое время истечения ключа + + Raises: + Exception: В случае ошибки при подключении к базе данных или обновлении ключа """ - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - UPDATE keys - SET expiry_time = $1, notified = FALSE, notified_24h = FALSE - WHERE client_id = $2 - """, - new_expiry_time, - client_id, - ) - await conn.close() + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}") + + await conn.execute( + """ + UPDATE keys + SET expiry_time = $1, notified = FALSE, notified_24h = FALSE + WHERE client_id = $2 + """, + new_expiry_time, + client_id, + ) + logger.info(f"Успешно обновлено время истечения ключа для клиента {client_id}") + + except Exception as e: + logger.error(f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}") + raise + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def delete_key(client_id: str): """ - Удаление ключа из базы данных. + Удаление ключа из базы данных для указанного клиента. + + Args: + client_id (str): Уникальный идентификатор клиента, ключ которого будет удален + + Raises: + Exception: В случае ошибки при подключении к базе данных или удалении ключа """ - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - DELETE FROM keys - WHERE client_id = $1 - """, - client_id, - ) - await conn.close() + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для удаления ключа клиента {client_id}") + + await conn.execute( + """ + DELETE FROM keys + WHERE client_id = $1 + """, + client_id, + ) + logger.info(f"Успешно удален ключ для клиента {client_id}") + + except Exception as e: + logger.error(f"Ошибка при удалении ключа для клиента {client_id}: {e}") + raise + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def add_balance_to_client(client_id: str, amount: float): - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - UPDATE connections - SET balance = balance + $1 - WHERE tg_id = $2 - """, - amount, - client_id, - ) - await conn.close() + """ + Добавление баланса клиенту по его идентификатору Telegram. + + Args: + client_id (str): Идентификатор клиента в Telegram + amount (float): Сумма для пополнения баланса + + Raises: + Exception: В случае ошибки при подключении к базе данных или обновлении баланса + """ + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}") + + await conn.execute( + """ + UPDATE connections + SET balance = balance + $1 + WHERE tg_id = $2 + """, + amount, + client_id, + ) + logger.info(f"Успешно пополнен баланс клиента {client_id} на сумму {amount}") + + except Exception as e: + logger.error(f"Ошибка при пополнении баланса для клиента {client_id}: {e}") + raise + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def get_client_id_by_email(email: str): """ - Получение client_id по email. + Получение идентификатора клиента по электронной почте. + + Args: + email (str): Электронная почта клиента + + Returns: + str: Идентификатор клиента или None, если клиент не найден + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса """ - conn = await asyncpg.connect(DATABASE_URL) - client_id = await conn.fetchval( - """ - SELECT client_id FROM keys WHERE email = $1 - """, - email, - ) - await conn.close() - return client_id + conn = None + try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для поиска client_id по email: {email}") + + client_id = await conn.fetchval( + """ + SELECT client_id FROM keys WHERE email = $1 + """, + email, + ) + + if client_id: + logger.info(f"Найден client_id для email: {email}") + else: + logger.warning(f"Не найден client_id для email: {email}") + + return client_id + + except Exception as e: + logger.error(f"Ошибка при получении client_id для email {email}: {e}") + raise + finally: + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def get_tg_id_by_client_id(client_id: str): - conn = await asyncpg.connect(DATABASE_URL) + """ + Получение Telegram ID по идентификатору клиента. + + Args: + client_id (str): Идентификатор клиента + + Returns: + int или None: Telegram ID клиента, если найден, иначе None + + Raises: + Exception: В случае ошибки при подключении к базе данных или выполнении запроса + """ + conn = None try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}") + result = await conn.fetchrow("SELECT tg_id FROM keys WHERE client_id = $1", client_id) - return result["tg_id"] if result else None + + if result: + logger.info(f"Найден Telegram ID для client_id: {client_id}") + return result["tg_id"] + else: + logger.warning(f"Не найден Telegram ID для client_id: {client_id}") + return None + + except Exception as e: + logger.error(f"Ошибка при получении Telegram ID для client_id {client_id}: {e}") + raise finally: - await conn.close() + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def upsert_user( @@ -547,8 +935,25 @@ async def upsert_user( language_code: str = None, is_bot: bool = False, ): - conn = await asyncpg.connect(DATABASE_URL) + """ + Обновляет или вставляет информацию о пользователе в базу данных. + + Args: + tg_id (int): Идентификатор пользователя в Telegram + username (str, optional): Имя пользователя в Telegram + first_name (str, optional): Имя пользователя + last_name (str, optional): Фамилия пользователя + language_code (str, optional): Код языка пользователя + is_bot (bool, optional): Флаг, указывающий является ли пользователь ботом + + Raises: + Exception: В случае ошибки при работе с базой данных + """ + conn = None try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}") + await conn.execute( """ INSERT INTO users (tg_id, username, first_name, last_name, language_code, is_bot, created_at, updated_at) @@ -569,13 +974,33 @@ async def upsert_user( language_code, is_bot, ) + logger.info(f"Успешно обновлена информация о пользователе {tg_id}") + except Exception as e: + logger.error(f"Ошибка при обновлении информации о пользователе {tg_id}: {e}") + raise finally: - await conn.close() + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных") async def add_payment(tg_id: int, amount: float, payment_system: str): - conn = await asyncpg.connect(DATABASE_URL) + """ + Добавляет информацию о платеже в базу данных. + + Args: + tg_id (int): Идентификатор пользователя в Telegram + amount (float): Сумма платежа + payment_system (str): Система оплаты + + Raises: + Exception: В случае ошибки при добавлении платежа + """ + conn = None try: + conn = await asyncpg.connect(DATABASE_URL) + logger.info(f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}") + await conn.execute( """ INSERT INTO payments (tg_id, amount, payment_system, status) @@ -585,7 +1010,11 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): amount, payment_system, ) + logger.info(f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}") except Exception as e: - logger.error(f"Ошибка при добавлении платежа: {e}") + logger.error(f"Ошибка при добавлении платежа для пользователя {tg_id}: {e}") + raise finally: - await conn.close() + if conn: + await conn.close() + logger.info("Закрытие подключения к базе данных после добавления платежа") diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index a1afa07a..6cede92f 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -10,7 +10,7 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, SUPPORT_CHAT_URL -from database import add_connection, get_balance, get_trial, store_key, update_balance +from database import add_connection, get_balance, get_trial, store_key, update_balance, use_trial from handlers.keys.key_utils import create_key_on_cluster 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 @@ -34,7 +34,7 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC async def select_server(callback_query: CallbackQuery, state: FSMContext, session: Any): - trial_status = get_trial(callback_query.message.from_user.id,session) + trial_status = get_trial(callback_query.message.from_user.id, session) logger.info(f'trial_status {trial_status}') if trial_status == 1: builder = InlineKeyboardBuilder() @@ -107,9 +107,7 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An expiry_time = None 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 + trial_status = get_trial(message.from_user.id, session) if trial_status == 0: expiry_time = current_time + timedelta(days=1, hours=3) @@ -175,21 +173,14 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An await asyncio.gather(*tasks) 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) + trial_status = await get_trial(message.from_user.id, session) + if trial_status == 0: + await use_trial(message.from_user.id, session) else: await add_connection(tg_id, 0, 1) logger.info(f"Storing key for user {tg_id} in the database.") - await store_key( - tg_id, - client_id, - email, - expiry_timestamp, - public_link, - least_loaded_cluster, - ) + await store_key(tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster, session) except Exception as e: logger.error(f"Error while creating the key for user {tg_id}: {e}") From c6d372c080b6512c0e4477f95df204aa70758859 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 10:35:12 +0300 Subject: [PATCH 09/21] Fix get_trial --- handlers/keys/key_management.py | 4 ++-- handlers/notifications.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 6cede92f..afbbaa9f 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -34,7 +34,7 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC async def select_server(callback_query: CallbackQuery, state: FSMContext, session: Any): - trial_status = get_trial(callback_query.message.from_user.id, session) + trial_status = await get_trial(callback_query.message.from_user.id, session) logger.info(f'trial_status {trial_status}') if trial_status == 1: builder = InlineKeyboardBuilder() @@ -107,7 +107,7 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An expiry_time = None logger.info(f"Checking trial status for user {tg_id}.") - trial_status = get_trial(message.from_user.id, session) + trial_status = await get_trial(message.from_user.id, session) if trial_status == 0: expiry_time = current_time + timedelta(days=1, hours=3) diff --git a/handlers/notifications.py b/handlers/notifications.py index 505e0db2..5432ecc5 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -98,7 +98,7 @@ async def notify_10h_keys( if not await is_bot_blocked(bot, tg_id): try: - keyboard = types.InlineKeyboardBuilder() + keyboard = InlineKeyboardBuilder() keyboard.button(text="🔄 Продлить VPN", callback_data=f'renew_key|{record["client_id"]}') keyboard.button(text="💳 Пополнить баланс", callback_data="pay") keyboard.button(text="👤 Личный кабинет", callback_data="profile") From 2b73c601d0d648189aeb944e1469a6ca0a4814e9 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 10:54:08 +0300 Subject: [PATCH 10/21] Fix --- handlers/keys/key_management.py | 3 +-- handlers/notifications.py | 4 ++-- handlers/start.py | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index afbbaa9f..72cfa08b 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -34,8 +34,7 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC async def select_server(callback_query: CallbackQuery, state: FSMContext, session: Any): - trial_status = await get_trial(callback_query.message.from_user.id, session) - logger.info(f'trial_status {trial_status}') + trial_status = await get_trial(callback_query.from_user.id, session) if trial_status == 1: builder = InlineKeyboardBuilder() builder.row( diff --git a/handlers/notifications.py b/handlers/notifications.py index 5432ecc5..3bbe1bf4 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -28,8 +28,8 @@ async def notify_expiring_keys(bot: Bot): logger.info("Начало обработки уведомлений.") - await notify_inactive_trial_users(bot, conn) - await asyncio.sleep(1) + # await notify_inactive_trial_users(bot, conn) + # await asyncio.sleep(1) await notify_10h_keys(bot, conn, current_time, threshold_time_10h) await asyncio.sleep(1) await notify_24h_keys(bot, conn, current_time, threshold_time_24h) diff --git a/handlers/start.py b/handlers/start.py index 8737f2cc..5794983f 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -33,7 +33,6 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin if not connection_exists: await add_connection(message.from_user.id, session) trial_status = await get_trial(message.from_user.id, session) - logger.info(f'trial_status {trial_status}') image_path = os.path.join("img", "pic.jpg") builder = InlineKeyboardBuilder() From 97d4031f0c6385de5075970eca26367b09715bbb Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 11:07:52 +0300 Subject: [PATCH 11/21] Update id --- filters/admin.py | 4 ++-- handlers/coupons.py | 2 +- handlers/instructions/instructions.py | 2 +- handlers/keys/key_management.py | 12 ++++++------ handlers/keys/keys.py | 10 +++++----- handlers/payments/cryprobot_pay.py | 10 +++++----- handlers/payments/freekassa_pay.py | 8 ++++---- handlers/payments/robokassa_pay.py | 14 +++++++------- handlers/payments/stars_pay.py | 4 ++-- handlers/payments/yookassa_pay.py | 8 ++++---- handlers/profile.py | 4 ++-- handlers/start.py | 11 +++++------ middlewares/admin.py | 4 ++-- 13 files changed, 46 insertions(+), 47 deletions(-) diff --git a/filters/admin.py b/filters/admin.py index b902294b..06cbcad7 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -11,7 +11,7 @@ class IsAdminFilter(BaseFilter): 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 + return message.chat.id in admin_ids + return message.chat.id == admin_ids except Exception: return False diff --git a/handlers/coupons.py b/handlers/coupons.py index 9d79076a..ed487671 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -33,7 +33,7 @@ async def handle_activate_coupon(callback_query: types.CallbackQuery, state: FSM @router.message(CouponActivationState.waiting_for_coupon_code) 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, session) + activation_result = await activate_coupon(message.chat.id, coupon_code, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index ce82d45a..8a1e9f97 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -35,7 +35,7 @@ async def send_instructions(callback_query: types.CallbackQuery): @router.callback_query(F.data.startswith("connect_pc|")) async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id key_name = callback_query.data.split("|")[1] record = await session.fetchrow( diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 72cfa08b..47ab10dc 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -34,7 +34,7 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC async def select_server(callback_query: CallbackQuery, state: FSMContext, session: Any): - trial_status = await get_trial(callback_query.from_user.id, session) + trial_status = await get_trial(callback_query.chat.id, session) if trial_status == 1: builder = InlineKeyboardBuilder() builder.row( @@ -54,7 +54,7 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext, sessio @router.callback_query(F.data == "confirm_create_new_key") async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id logger.info(f"User {tg_id} confirmed creation of a new key.") @@ -76,7 +76,7 @@ async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContex @router.message(Form.waiting_for_key_name) async def handle_key_name_input(message: Message, state: FSMContext, session: Any): - tg_id = message.from_user.id + tg_id = message.chat.id key_name = sanitize_key_name(message.text) logger.info(f"User {tg_id} is attempting to create a key with the name: {key_name}") @@ -106,7 +106,7 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An expiry_time = None logger.info(f"Checking trial status for user {tg_id}.") - trial_status = await get_trial(message.from_user.id, session) + trial_status = await get_trial(message.chat.id, session) if trial_status == 0: expiry_time = current_time + timedelta(days=1, hours=3) @@ -172,9 +172,9 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An await asyncio.gather(*tasks) logger.info(f"Updating trial status for user {tg_id} in the database.") - trial_status = await get_trial(message.from_user.id, session) + trial_status = await get_trial(message.chat.id, session) if trial_status == 0: - await use_trial(message.from_user.id, session) + await use_trial(message.chat.id, session) else: await add_connection(tg_id, 0, 1) diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 9a668893..816bd9b8 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -35,7 +35,7 @@ router = Router() @router.callback_query(F.data == "view_keys") async def process_callback_view_keys(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id try: records = await session.fetch( """ @@ -99,7 +99,7 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio @router.callback_query(F.data.startswith("view_key|")) async def process_callback_view_key(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id key_name = callback_query.data.split("|")[1] try: record = await session.fetchrow( @@ -185,7 +185,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session @router.callback_query(F.data.startswith("update_subscription|")) async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id email = callback_query.data.split("|")[1] try: record = await session.fetchrow( @@ -284,7 +284,7 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery): @router.callback_query(F.data.startswith("renew_key|")) async def process_callback_renew_key(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id key_name = callback_query.data.split("|")[1] try: record = await session.fetchrow( @@ -390,7 +390,7 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s @router.callback_query(F.data.startswith("renew_plan|")) async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id plan, client_id = ( callback_query.data.split("|")[1], callback_query.data.split("|")[2], diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index 302125f3..b7a5306a 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -53,11 +53,11 @@ 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(callback_query.from_user.id) + key_count = await get_key_count(callback_query.chat.id) if key_count == 0: - exists = await check_connection_exists(callback_query.from_user.id) + exists = await check_connection_exists(callback_query.chat.id) if not exists: - await add_connection(callback_query.from_user.id, balance=0.0, trial=0) + await add_connection(callback_query.chat.id, balance=0.0, trial=0) await callback_query.message.answer( "Выберите сумму пополнения:", reply_markup=builder.as_markup(), @@ -88,7 +88,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F asset="USDT", amount=str(int(amount // RUB_TO_USDT)), description=f"Пополнения баланса на {amount} руб", - payload=f"{callback_query.from_user.id}:{int(amount)}", + payload=f"{callback_query.chat.id}:{int(amount)}", ) if hasattr(invoice, "bot_invoice_url"): @@ -158,7 +158,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) asset="USDT", amount=str(int(amount // RUB_TO_USDT)), description=f"Пополнения баланса на {amount} руб", - payload=f"{message.from_user.id}:{amount}", + payload=f"{message.chat.id}:{amount}", ) if hasattr(invoice, "bot_invoice_url"): diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 88ebe04f..ca9e67da 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -130,9 +130,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F await callback_query.message.answer("Некорректная сумма.") return - user_email = f"{callback_query.from_user.id}@solo.net" + user_email = f"{callback_query.chat.id}@solo.net" user_ip = callback_query.message.chat.id - payment_url = await create_payment(callback_query.from_user.id, amount, user_email, user_ip) + payment_url = await create_payment(callback_query.chat.id, amount, user_email, user_ip) if payment_url: confirm_keyboard = InlineKeyboardMarkup( @@ -166,9 +166,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:") return - user_email = f"{message.from_user.id}@solo.net" + user_email = f"{message.chat.id}@solo.net" user_ip = message.chat.id - payment_url = await create_payment(message.from_user.id, amount, user_email, user_ip) + payment_url = await create_payment(message.chat.id, amount, user_email, user_ip) if payment_url: keyboard = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]]) diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index ea1c332f..b06ebc36 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -49,7 +49,7 @@ def generate_payment_link(amount, inv_id, description, tg_id): @router.callback_query(F.data == "pay_robokassa") async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id logger.info(f"User {tg_id} initiated Robokassa payment.") builder = InlineKeyboardBuilder() @@ -117,13 +117,13 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F return await state.update_data(amount=amount) - logger.info(f"User {callback_query.from_user.id} selected amount: {amount}.") + logger.info(f"User {callback_query.chat.id} selected amount: {amount}.") inv_id = 0 - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id) - logger.info(f"Payment URL for user {callback_query.from_user.id}: {payment_url}") + logger.info(f"Payment URL for user {callback_query.chat.id}: {payment_url}") confirm_keyboard = InlineKeyboardMarkup( inline_keyboard=[ @@ -136,7 +136,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:", reply_markup=confirm_keyboard, ) - logger.info(f"Payment link sent to user {callback_query.from_user.id}.") + logger.info(f"Payment link sent to user {callback_query.chat.id}.") async def robokassa_webhook(request): @@ -200,7 +200,7 @@ def check_payment_signature(params): @router.callback_query(F.data == "enter_custom_amount_robokassa") async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id + tg_id = callback_query.chat.id logger.info(f"User {tg_id} chose to enter a custom amount.") await callback_query.message.answer(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):") @@ -209,7 +209,7 @@ async def process_custom_amount_selection(callback_query: types.CallbackQuery, s @router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) async def handle_custom_amount_input(message: types.Message, state: FSMContext): - tg_id = message.from_user.id + tg_id = message.chat.id logger.info(f"User {tg_id} entered custom amount: {message.text}") inv_id = 0 diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index a0db97a4..dec6b6aa 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -21,7 +21,7 @@ class ReplenishBalanceState(StatesGroup): @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 + tg_id = callback_query.chat.id builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) @@ -176,7 +176,7 @@ async def on_successful_payment( message: types.Message, ): try: - user_id = int(message.from_user.id) + user_id = int(message.chat.id) amount = float(message.successful_payment.invoice_payload.split("_")[0]) logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}") await add_payment(int(user_id), float(amount), "stars") diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index 5dddef4b..c2bc30a6 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -31,7 +31,7 @@ class ReplenishBalanceState(StatesGroup): @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 + tg_id = callback_query.chat.id builder = InlineKeyboardBuilder() @@ -95,7 +95,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F # state_data = await state.get_data() customer_name = callback_query.from_user.full_name - customer_id = callback_query.from_user.id + customer_id = callback_query.chat.id customer_email = f"{customer_id}@solo.net" @@ -195,7 +195,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) "receipt": { "customer": { "full_name": message.from_user.full_name, - "email": f"{message.from_user.id}@solo.net", + "email": f"{message.chat.id}@solo.net", "phone": "79000000000", }, "items": [ @@ -210,7 +210,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) } ], }, - "metadata": {"user_id": message.from_user.id}, + "metadata": {"user_id": message.chat.id}, }, uuid.uuid4(), ) diff --git a/handlers/profile.py b/handlers/profile.py index 2ab7cffe..bdf8edba 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -14,7 +14,7 @@ router = Router() @router.callback_query(F.data == "profile") async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext, admin: bool): - chat_id = callback_query.from_user.id + chat_id = callback_query.chat.id username = callback_query.from_user.full_name image_path = os.path.join("img", "pic.jpg") key_count = await get_key_count(chat_id) @@ -84,7 +84,7 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): @router.callback_query(F.data == "invite") async def invite_handler(callback_query: types.CallbackQuery): - chat_id = callback_query.from_user.id + chat_id = callback_query.chat.id referral_link = get_referral_link(chat_id) referral_stats = await get_referral_stats(chat_id) diff --git a/handlers/start.py b/handlers/start.py index 5794983f..2aafbccc 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -11,7 +11,6 @@ from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn -from logger import logger router = Router() @@ -26,13 +25,13 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin if message.text: try: referrer_tg_id = int(message.text.split("referral_")[1]) - await add_referral(message.from_user.id, referrer_tg_id, session) + await add_referral(message.chat.id, referrer_tg_id, session) except (ValueError, IndexError): pass - connection_exists = await check_connection_exists(message.from_user.id) + connection_exists = await check_connection_exists(message.chat.id) if not connection_exists: - await add_connection(message.from_user.id, session) - trial_status = await get_trial(message.from_user.id, session) + await add_connection(message.chat.id, session) + trial_status = await get_trial(message.chat.id, session) image_path = os.path.join("img", "pic.jpg") builder = InlineKeyboardBuilder() @@ -65,7 +64,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin @router.callback_query(F.data == "connect_vpn") async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): - user_id = callback_query.from_user.id + user_id = callback_query.chat.id trial_key_info = await create_trial_key(user_id, session) diff --git a/middlewares/admin.py b/middlewares/admin.py index 57aa0692..5a06adaf 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -20,7 +20,7 @@ class AdminMiddleware(BaseMiddleware): 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 + return event.chat.id in admin_ids + return event.chat.id == admin_ids except Exception: return False From 5ca72a4a6583280d763ab3f19783215398240e52 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 11:26:54 +0300 Subject: [PATCH 12/21] Fix srssion --- handlers/instructions/instructions.py | 2 +- handlers/keys/key_management.py | 6 +++--- handlers/keys/keys.py | 10 +++++----- handlers/payments/cryprobot_pay.py | 11 ++++++----- handlers/payments/freekassa_pay.py | 4 ++-- handlers/payments/robokassa_pay.py | 17 +++++++++-------- handlers/payments/stars_pay.py | 7 ++++--- handlers/payments/yookassa_pay.py | 9 +++++---- handlers/profile.py | 4 ++-- handlers/start.py | 4 ++-- 10 files changed, 39 insertions(+), 35 deletions(-) diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 8a1e9f97..dd7dc5dd 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -35,7 +35,7 @@ async def send_instructions(callback_query: types.CallbackQuery): @router.callback_query(F.data.startswith("connect_pc|")) async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] record = await session.fetchrow( diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 47ab10dc..0a0666d2 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -34,7 +34,7 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC async def select_server(callback_query: CallbackQuery, state: FSMContext, session: Any): - trial_status = await get_trial(callback_query.chat.id, session) + trial_status = await get_trial(callback_query.message.chat.id, session) if trial_status == 1: builder = InlineKeyboardBuilder() builder.row( @@ -54,7 +54,7 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext, sessio @router.callback_query(F.data == "confirm_create_new_key") async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext): - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} confirmed creation of a new key.") @@ -176,7 +176,7 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An if trial_status == 0: await use_trial(message.chat.id, session) else: - await add_connection(tg_id, 0, 1) + await add_connection(tg_id=tg_id, balance=0, trial=1, session=session) logger.info(f"Storing key for user {tg_id} in the database.") await store_key(tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster, session) diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 816bd9b8..689526bf 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -35,7 +35,7 @@ router = Router() @router.callback_query(F.data == "view_keys") async def process_callback_view_keys(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id try: records = await session.fetch( """ @@ -99,7 +99,7 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio @router.callback_query(F.data.startswith("view_key|")) async def process_callback_view_key(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] try: record = await session.fetchrow( @@ -185,7 +185,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session @router.callback_query(F.data.startswith("update_subscription|")) async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id email = callback_query.data.split("|")[1] try: record = await session.fetchrow( @@ -284,7 +284,7 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery): @router.callback_query(F.data.startswith("renew_key|")) async def process_callback_renew_key(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] try: record = await session.fetchrow( @@ -390,7 +390,7 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s @router.callback_query(F.data.startswith("renew_plan|")) async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id plan, client_id = ( callback_query.data.split("|")[1], callback_query.data.split("|")[2], diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index b7a5306a..acda2a1a 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -1,3 +1,4 @@ +from typing import Any from aiocryptopay import AioCryptoPay, Networks from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -25,7 +26,7 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_cryptobot") -async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext): +async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext,session:Any): builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -53,11 +54,11 @@ 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(callback_query.chat.id) + key_count = await get_key_count(callback_query.message.chat.id) if key_count == 0: - exists = await check_connection_exists(callback_query.chat.id) + exists = await check_connection_exists(callback_query.message.chat.id) if not exists: - await add_connection(callback_query.chat.id, balance=0.0, trial=0) + await add_connection(tg_id=callback_query.message.chat.id, balance=0.0, trial=0,session=session) await callback_query.message.answer( "Выберите сумму пополнения:", reply_markup=builder.as_markup(), @@ -88,7 +89,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F asset="USDT", amount=str(int(amount // RUB_TO_USDT)), description=f"Пополнения баланса на {amount} руб", - payload=f"{callback_query.chat.id}:{int(amount)}", + payload=f"{callback_query.message.chat.id}:{int(amount)}", ) if hasattr(invoice, "bot_invoice_url"): diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index ca9e67da..b3c26790 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -130,9 +130,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F await callback_query.message.answer("Некорректная сумма.") return - user_email = f"{callback_query.chat.id}@solo.net" + user_email = f"{callback_query.message.chat.id}@solo.net" user_ip = callback_query.message.chat.id - payment_url = await create_payment(callback_query.chat.id, amount, user_email, user_ip) + payment_url = await create_payment(callback_query.message.chat.id, amount, user_email, user_ip) if payment_url: confirm_keyboard = InlineKeyboardMarkup( diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index b06ebc36..4a21eac8 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -1,4 +1,5 @@ import hashlib +from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -48,8 +49,8 @@ def generate_payment_link(amount, inv_id, description, tg_id): @router.callback_query(F.data == "pay_robokassa") -async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.chat.id +async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext,session:Any): + tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} initiated Robokassa payment.") builder = InlineKeyboardBuilder() @@ -85,7 +86,7 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0) + await add_connection(tg_id, balance=0.0, trial=0,session=session) logger.info(f"Created new connection for user {tg_id} with balance 0.0.") await callback_query.message.answer( @@ -117,13 +118,13 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F return await state.update_data(amount=amount) - logger.info(f"User {callback_query.chat.id} selected amount: {amount}.") + logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.") inv_id = 0 - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id) - logger.info(f"Payment URL for user {callback_query.chat.id}: {payment_url}") + logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}") confirm_keyboard = InlineKeyboardMarkup( inline_keyboard=[ @@ -136,7 +137,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:", reply_markup=confirm_keyboard, ) - logger.info(f"Payment link sent to user {callback_query.chat.id}.") + logger.info(f"Payment link sent to user {callback_query.message.chat.id}.") async def robokassa_webhook(request): @@ -200,7 +201,7 @@ def check_payment_signature(params): @router.callback_query(F.data == "enter_custom_amount_robokassa") async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.chat.id + tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} chose to enter a custom amount.") await callback_query.message.answer(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):") diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index dec6b6aa..b55bff13 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -1,3 +1,4 @@ +from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup @@ -20,8 +21,8 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_stars") -async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.chat.id +async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext,session:Any): + tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) @@ -58,7 +59,7 @@ async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0) + await add_connection(tg_id, balance=0.0, trial=0,session=session) try: await callback_query.message.delete() diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index c2bc30a6..c8ec2ebf 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -1,3 +1,4 @@ +from typing import Any import uuid from aiogram import F, Router, types @@ -30,8 +31,8 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_yookassa") -async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.chat.id +async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext,session:Any): + tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() @@ -67,7 +68,7 @@ async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, sta if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0) + await add_connection(tg_id, balance=0.0, trial=0,session=session) await callback_query.message.answer( text="Выберите сумму пополнения:", @@ -95,7 +96,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F # state_data = await state.get_data() customer_name = callback_query.from_user.full_name - customer_id = callback_query.chat.id + customer_id = callback_query.message.chat.id customer_email = f"{customer_id}@solo.net" diff --git a/handlers/profile.py b/handlers/profile.py index bdf8edba..e4d67c20 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -14,7 +14,7 @@ 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.chat.id + chat_id = callback_query.message.chat.id username = callback_query.from_user.full_name image_path = os.path.join("img", "pic.jpg") key_count = await get_key_count(chat_id) @@ -84,7 +84,7 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): @router.callback_query(F.data == "invite") async def invite_handler(callback_query: types.CallbackQuery): - chat_id = callback_query.chat.id + chat_id = callback_query.message.chat.id referral_link = get_referral_link(chat_id) referral_stats = await get_referral_stats(chat_id) diff --git a/handlers/start.py b/handlers/start.py index 2aafbccc..b199fc9a 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -30,7 +30,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin pass connection_exists = await check_connection_exists(message.chat.id) if not connection_exists: - await add_connection(message.chat.id, session) + await add_connection(tg_id=message.chat.id, session=session) trial_status = await get_trial(message.chat.id, session) image_path = os.path.join("img", "pic.jpg") @@ -64,7 +64,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin @router.callback_query(F.data == "connect_vpn") async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): - user_id = callback_query.chat.id + user_id = callback_query.message.chat.id trial_key_info = await create_trial_key(user_id, session) From e99c41ae0cd0b6d2c42a085fb0a8cf05deaf4a1e Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 11:42:29 +0300 Subject: [PATCH 13/21] Update connection_exists --- handlers/keys/key_management.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 0a0666d2..2870aadf 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -10,7 +10,7 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, SUPPORT_CHAT_URL -from database import add_connection, get_balance, get_trial, store_key, update_balance, use_trial +from database import add_connection, check_connection_exists, get_balance, get_trial, store_key, update_balance, use_trial from handlers.keys.key_utils import create_key_on_cluster 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 @@ -172,8 +172,8 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An await asyncio.gather(*tasks) logger.info(f"Updating trial status for user {tg_id} in the database.") - trial_status = await get_trial(message.chat.id, session) - if trial_status == 0: + connection_exists = await check_connection_exists(message.chat.id) + if connection_exists: await use_trial(message.chat.id, session) else: await add_connection(tg_id=tg_id, balance=0, trial=1, session=session) From a79c51dce44f13f1be64659eebc4eafb02285503 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 11:51:17 +0300 Subject: [PATCH 14/21] Fix admin --- filters/admin.py | 4 ++-- middlewares/admin.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/filters/admin.py b/filters/admin.py index 06cbcad7..b902294b 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -11,7 +11,7 @@ class IsAdminFilter(BaseFilter): try: admin_ids: Union[int, list[int]] = ADMIN_ID if isinstance(admin_ids, list): - return message.chat.id in admin_ids - return message.chat.id == admin_ids + return message.from_user.id in admin_ids + return message.from_user.id == admin_ids except Exception: return False diff --git a/middlewares/admin.py b/middlewares/admin.py index 5a06adaf..57aa0692 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -20,7 +20,7 @@ class AdminMiddleware(BaseMiddleware): try: admin_ids: Union[int, list[int]] = ADMIN_ID if isinstance(admin_ids, list): - return event.chat.id in admin_ids - return event.chat.id == admin_ids + return event.from_user.id in admin_ids + return event.from_user.id == admin_ids except Exception: return False From dccc6166e83fa390872aae2aeefd395f2b5079a0 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 11:58:43 +0300 Subject: [PATCH 15/21] notify_inactive_trial_users --- handlers/notifications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/notifications.py b/handlers/notifications.py index 3bbe1bf4..5432ecc5 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -28,8 +28,8 @@ async def notify_expiring_keys(bot: Bot): logger.info("Начало обработки уведомлений.") - # await notify_inactive_trial_users(bot, conn) - # await asyncio.sleep(1) + await notify_inactive_trial_users(bot, conn) + await asyncio.sleep(1) await notify_10h_keys(bot, conn, current_time, threshold_time_10h) await asyncio.sleep(1) await notify_24h_keys(bot, conn, current_time, threshold_time_24h) From 3f7d0a26fdf0fa492f997dba282224281d1d1447 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 13:51:18 +0300 Subject: [PATCH 16/21] Fix delete msg --- handlers/admin/admin_coupons.py | 4 ++-- middlewares/delete.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 5f7a3904..d632a9eb 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -76,12 +76,12 @@ async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any result = await delete_coupon_from_db(coupon_code, session) if result: - await show_coupon_list(callback_query) + await show_coupon_list(callback_query,session) else: await callback_query.message.answer( f"❌ Купон с кодом {coupon_code} не найден.", ) - await show_coupon_list(callback_query) + await show_coupon_list(callback_query,session) except Exception as e: logger.error(f"Ошибка при удалении купона: {e}") diff --git a/middlewares/delete.py b/middlewares/delete.py index 3f355ab9..4ac4d812 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -1,9 +1,8 @@ from typing import Any, Awaitable, Callable, Dict - +from logger import logger from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, Message, TelegramObject - class DeleteMessageMiddleware(BaseMiddleware): async def __call__( self, @@ -13,6 +12,10 @@ class DeleteMessageMiddleware(BaseMiddleware): ) -> Any: if isinstance(event, (Message, CallbackQuery)): if isinstance(event, Message): + try: + await event.bot.delete_message(event.chat.id, event.message_id - 1) + except Exception: + pass await event.delete() elif isinstance(event, CallbackQuery): await event.answer() From 2cfbfc5fd05b92b5563cd4be0dc183fbc9cc4199 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 14:14:06 +0300 Subject: [PATCH 17/21] Update table --- database.py | 105 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/database.py b/database.py index c660a762..bd5c1c0e 100644 --- a/database.py +++ b/database.py @@ -25,21 +25,6 @@ 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( """ @@ -121,6 +106,18 @@ async def init_db(): """ ) + # Таблица для отслеживания отправленных уведомлений + await conn.execute( + """ + CREATE TABLE IF NOT EXISTS notifications ( + tg_id BIGINT NOT NULL, + last_notification_time TIMESTAMP NOT NULL DEFAULT NOW(), + notification_type TEXT NOT NULL, + PRIMARY KEY (tg_id, notification_type) + ) + """ + ) + await conn.close() @@ -1018,3 +1015,81 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): if conn: await conn.close() logger.info("Закрытие подключения к базе данных после добавления платежа") + + +async def add_notification(tg_id: int, notification_type: str, session: Any): + """ + Добавляет запись о notification в базу данных. + + Args: + tg_id (int): Идентификатор пользователя в Telegram + notification_type (str): Тип уведомления + session (Any): Сессия базы данных для выполнения запроса + + Raises: + Exception: В случае ошибки при добавлении notification + """ + try: + await session.execute( + """ + INSERT INTO notifications (tg_id, notification_type) + VALUES ($1, $2) + ON CONFLICT (tg_id, notification_type) + DO UPDATE SET last_notification_time = NOW() + """, + tg_id, + notification_type, + ) + logger.info(f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}") + except Exception as e: + logger.error(f"Ошибка при добавлении notification для пользователя {tg_id}: {e}") + raise + + +async def check_notification_time(tg_id: int, notification_type: str, hours: int = 12, session: Any = None) -> bool: + """ + Проверяет, прошло ли указанное количество часов с момента последнего уведомления. + + Args: + tg_id (int): Идентификатор пользователя в Telegram + notification_type (str): Тип уведомления + hours (int, optional): Количество часов для проверки. По умолчанию 12. + session (Any): Сессия базы данных для выполнения запроса + + Returns: + bool: True, если с момента последнего уведомления прошло больше указанного времени, иначе False + + Raises: + Exception: В случае ошибки при проверке времени уведомления + """ + try: + result = await session.fetchrow( + """ + SELECT + CASE + WHEN last_notification_time IS NULL THEN TRUE + WHEN NOW() - last_notification_time > INTERVAL '$1 hours' THEN TRUE + ELSE FALSE + END as can_notify + FROM notifications + WHERE tg_id = $2 AND notification_type = $3 + """, + hours, + tg_id, + notification_type + ) + + # Если записи нет, значит уведомление можно отправить + if result is None: + return True + + can_notify = result['can_notify'] + + logger.info(f"Проверка уведомления типа {notification_type} для пользователя {tg_id}: {'можно отправить' if can_notify else 'слишком рано'}") + + return can_notify + + except Exception as e: + logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}") + raise + From 89c5a4db2ce9ade9e494e76788c06047323824f0 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 19:38:59 +0300 Subject: [PATCH 18/21] Refactor referral system to support multi-level bonuses and enhance logging. Update database interactions for referral handling and statistics retrieval. Change key display format in notifications and admin user editor for better readability. --- database.py | 144 +++++++++++++++++++++------- handlers/admin/admin_user_editor.py | 4 +- handlers/notifications.py | 2 +- handlers/start.py | 2 +- 4 files changed, 114 insertions(+), 38 deletions(-) diff --git a/database.py b/database.py index bd5c1c0e..99f35b46 100644 --- a/database.py +++ b/database.py @@ -3,7 +3,7 @@ from typing import Any import asyncpg -from config import BONUS_PERCENT, DATABASE_URL +from config import REFERRAL_BONUS_PERCENTAGES, DATABASE_URL from logger import logger @@ -638,69 +638,91 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): async def handle_referral_on_balance_update(tg_id: int, amount: float): """ - Обработка реферальной системы при обновлении баланса. + Обработка многоуровневой реферальной системы при обновлении баланса пользователя. - Эта функция проверяет, есть ли у пользователя реферер, - и если есть, начисляет бонус за пополнение баланса. + Метод анализирует цепочку рефералов для указанного пользователя и начисляет + бонусы рефереерам на разных уровнях согласно настроенным процентам. Args: - tg_id (int): Telegram ID пользователя + tg_id (int): Идентификатор Telegram пользователя, пополнившего баланс amount (float): Сумма пополнения баланса - Returns: - None + Raises: + Exception: В случае ошибки при работе с базой данных """ conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для обработки реферала пользователя {tg_id}") + logger.info(f"Начало обработки реферальной системы для пользователя {tg_id}") - referral = await conn.fetchrow( - """ - SELECT referrer_tg_id FROM referrals WHERE referred_tg_id = $1 - """, - tg_id, - ) + # Максимальное количество уровней рефералов + MAX_REFERRAL_LEVELS = len(REFERRAL_BONUS_PERCENTAGES.keys()) - if referral: - referrer_tg_id = referral["referrer_tg_id"] - logger.info(f"Найден реферер {referrer_tg_id} для пользователя {tg_id}") + # Текущий уровень для отслеживания + current_tg_id = tg_id + referral_chain = [] - bonus = amount * BONUS_PERCENT + # Собираем цепочку рефералов + for level in range(1, MAX_REFERRAL_LEVELS + 1): + # Получаем информацию о рефере текущего уровня + referral = await conn.fetchrow( + """ + SELECT referrer_tg_id + FROM referrals + WHERE referred_tg_id = $1 + """, + current_tg_id, + ) + + # Если реферер не найден, прекращаем поиск + if not referral: + break + + referrer_tg_id = referral['referrer_tg_id'] + referral_chain.append({ + 'tg_id': referrer_tg_id, + 'level': level + }) + + # Переходим к следующему уровню + current_tg_id = referrer_tg_id + + # Начисляем бонусы по цепочке рефералов + for referral in referral_chain: + referrer_tg_id = referral['tg_id'] + level = referral['level'] + + # Расчет бонуса для текущего уровня + bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0) + bonus = amount * bonus_percent bonus = max(bonus, 0) # Гарантируем неотрицательный бонус - logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id}") - await update_balance(referrer_tg_id, bonus) - - await conn.execute( - """ - UPDATE referrals SET reward_issued = TRUE - WHERE referrer_tg_id = $1 AND referred_tg_id = $2 - """, - referrer_tg_id, - tg_id, - ) - logger.info(f"Обновлен статус реферала для {referrer_tg_id}") + if bonus > 0: + logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}") + + # Обновляем баланс реферера + await update_balance(referrer_tg_id, bonus) except Exception as e: - logger.error(f"Ошибка при обработке реферала для пользователя {tg_id}: {e}") + logger.error(f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}") finally: if conn: await conn.close() - logger.info("Закрытие подключения к базе данных") async def get_referral_stats(referrer_tg_id: int): """ - Получение статистики рефералов для указанного пользователя. + Получение подробной статистики рефералов для указанного пользователя. Args: referrer_tg_id (int): Telegram ID пользователя, для которого запрашивается статистика рефералов. Returns: - dict: Словарь со статистикой рефералов, содержащий: + dict: Словарь с детальной статистикой рефералов, содержащий: - total_referrals (int): Общее количество рефералов - active_referrals (int): Количество активных рефералов (с начисленным бонусом) + - referrals_by_level (dict): Количество рефералов по каждому уровню + - total_referral_bonus (float): Общая сумма бонусов от рефералов Raises: Exception: В случае ошибки при подключении к базе данных или выполнении запроса @@ -712,6 +734,7 @@ async def get_referral_stats(referrer_tg_id: int): f"Установлено подключение к базе данных для получения статистики рефералов пользователя {referrer_tg_id}" ) + # Общее количество рефералов total_referrals = await conn.fetchval( """ SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 @@ -720,6 +743,7 @@ async def get_referral_stats(referrer_tg_id: int): ) logger.debug(f"Получено общее количество рефералов: {total_referrals}") + # Активные рефералы active_referrals = await conn.fetchval( """ SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE @@ -728,9 +752,61 @@ async def get_referral_stats(referrer_tg_id: int): ) logger.debug(f"Получено количество активных рефералов: {active_referrals}") + # Рефералы по уровням + referrals_by_level_records = await conn.fetch( + """ + WITH RECURSIVE referral_levels AS ( + SELECT referred_tg_id, referrer_tg_id, 1 AS level + FROM referrals + WHERE referrer_tg_id = $1 + + UNION + + SELECT r.referred_tg_id, r.referrer_tg_id, rl.level + 1 + FROM referrals r + JOIN referral_levels rl ON r.referrer_tg_id = rl.referred_tg_id + WHERE rl.level < 5 + ) + SELECT level, + COUNT(*) AS level_count, + COUNT(CASE WHEN reward_issued = TRUE THEN 1 END) AS active_level_count + FROM referral_levels rl + JOIN referrals r ON rl.referred_tg_id = r.referred_tg_id + GROUP BY level + ORDER BY level + """, + referrer_tg_id, + ) + + # Преобразование результатов в словарь + referrals_by_level = { + record['level']: { + 'total': record['level_count'], + 'active': record['active_level_count'] + } for record in referrals_by_level_records + } + logger.debug(f"Получена статистика рефералов по уровням: {referrals_by_level}") + + # Общая сумма бонусов от рефералов + total_referral_bonus = await conn.fetchval( + """ + SELECT COALESCE(SUM(amount), 0) + FROM payments + WHERE tg_id IN ( + SELECT referred_tg_id + FROM referrals + WHERE referrer_tg_id = $1 + ) AND status = 'success' + """, + referrer_tg_id, + ) + logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}") + return { "total_referrals": total_referrals, "active_referrals": active_referrals, + "referrals_by_level": referrals_by_level, + "total_referral_bonus": total_referral_bonus, } except Exception as e: diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 878834f2..c485963f 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -243,7 +243,7 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): return response_message = ( - f"🔑 Ключ:
{key_details['key']}
\n" + f"🔑 Ключ: {key_details['key']}\n" f"⏰ Дата истечения: {key_details['expiry_date']}\n" f"💰 Баланс пользователя: {key_details['balance']}\n" f"🌐 Сервер: {key_details['server_name']}" @@ -295,7 +295,7 @@ async def handle_key_name_input(message: types.Message, state: FSMContext, sessi return response_message = ( - f"🔑 Ключ:
{key_details['key']}
\n" + f"🔑 Ключ: {key_details['key']}\n" f"⏰ Дата истечения: {key_details['expiry_date']}\n" f"💰 Баланс пользователя: {key_details['balance']}\n" f"🌐 Сервер: {key_details['server_name']}" diff --git a/handlers/notifications.py b/handlers/notifications.py index 5432ecc5..ea4b3f97 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -8,7 +8,7 @@ from py3xui import AsyncApi from client import delete_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB, TRIAL_TIME -from database import delete_key, get_balance, update_balance, update_key_expiry +from database import delete_key, get_balance, update_balance, update_key_expiry#,add_notification,check_notification_time from handlers.keys.key_utils import renew_key_in_cluster from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS from logger import logger diff --git a/handlers/start.py b/handlers/start.py index b199fc9a..a4b1a6f9 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -75,7 +75,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): key_message = ( f"🔑 Ваш персональный ключ доступа:\n" - f"
{trial_key_info['key']}
\n\n" + f"{trial_key_info['key']}\n\n" f"📋 Быстрая инструкция по подключению:\n{INSTRUCTIONS_TRIAL}" ) From 02120262cdff91443dec728bc8aac3e2b4603198 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 20:02:40 +0300 Subject: [PATCH 19/21] Enhance notification system for inactive trial users by adding notification time checks and logging. Implement functionality to record notifications sent to users, ensuring they are notified no more than once every 24 hours. --- handlers/notifications.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/handlers/notifications.py b/handlers/notifications.py index ea4b3f97..5a8049e7 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -8,7 +8,7 @@ from py3xui import AsyncApi from client import delete_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB, TRIAL_TIME -from database import delete_key, get_balance, update_balance, update_key_expiry#,add_notification,check_notification_time +from database import delete_key, get_balance, update_balance, update_key_expiry,add_notification,check_notification_time from handlers.keys.key_utils import renew_key_in_cluster from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS from logger import logger @@ -218,7 +218,15 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): username = user.get('username', 'Пользователь') try: - if not await is_bot_blocked(bot, tg_id): + # Проверяем, можно ли отправить уведомление + can_notify = await check_notification_time( + tg_id, + 'inactive_trial', + hours=24, # Уведомление не чаще, чем раз в 24 часа + session=conn + ) + + if can_notify and not await is_bot_blocked(bot, tg_id): builder = InlineKeyboardBuilder() builder.row( types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key") @@ -236,6 +244,13 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): await bot.send_message(tg_id, message, reply_markup=keyboard) logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.") + # Добавляем запись о notification + await add_notification( + tg_id, + 'inactive_trial', + session=conn + ) + except Exception as e: logger.error(f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}") From 55517c13984fcfa49475d447253a6f00133f23fe Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 20:29:22 +0300 Subject: [PATCH 20/21] Refactor database and notification handlers for improved readability and consistency. Clean up import statements, enhance logging messages, and streamline function calls across multiple files. Ensure proper session handling in payment processing functions. --- database.py | 58 ++++++++++++++++-------------- handlers/admin/admin_coupons.py | 4 +-- handlers/keys/key_management.py | 10 +++++- handlers/notifications.py | 20 +++++------ handlers/payments/cryprobot_pay.py | 5 +-- handlers/payments/robokassa_pay.py | 4 +-- handlers/payments/stars_pay.py | 5 +-- handlers/payments/yookassa_pay.py | 4 +-- middlewares/delete.py | 3 +- 9 files changed, 65 insertions(+), 48 deletions(-) diff --git a/database.py b/database.py index 99f35b46..08b934ce 100644 --- a/database.py +++ b/database.py @@ -3,7 +3,7 @@ from typing import Any import asyncpg -from config import REFERRAL_BONUS_PERCENTAGES, DATABASE_URL +from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES from logger import logger @@ -611,9 +611,9 @@ async def get_all_users(conn): Exception: В случае ошибки при получении данных """ try: - пользователи = await conn.fetch("SELECT tg_id FROM connections") - logger.info(f"Получен список всех пользователей. Количество: {len(пользователи)}") - return пользователи + users = await conn.fetch("SELECT tg_id FROM connections") + logger.info(f"Получен список всех пользователей. Количество: {len(users)}") + return users except Exception as e: logger.error(f"Ошибка при получении списка пользователей: {e}") raise @@ -679,10 +679,7 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): break referrer_tg_id = referral['referrer_tg_id'] - referral_chain.append({ - 'tg_id': referrer_tg_id, - 'level': level - }) + referral_chain.append({'tg_id': referrer_tg_id, 'level': level}) # Переходим к следующему уровню current_tg_id = referrer_tg_id @@ -691,7 +688,7 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): for referral in referral_chain: referrer_tg_id = referral['tg_id'] level = referral['level'] - + # Расчет бонуса для текущего уровня bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0) bonus = amount * bonus_percent @@ -699,7 +696,7 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): if bonus > 0: logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}") - + # Обновляем баланс реферера await update_balance(referrer_tg_id, bonus) @@ -777,13 +774,11 @@ async def get_referral_stats(referrer_tg_id: int): """, referrer_tg_id, ) - + # Преобразование результатов в словарь referrals_by_level = { - record['level']: { - 'total': record['level_count'], - 'active': record['active_level_count'] - } for record in referrals_by_level_records + record['level']: {'total': record['level_count'], 'active': record['active_level_count']} + for record in referrals_by_level_records } logger.debug(f"Получена статистика рефералов по уровням: {referrals_by_level}") @@ -1139,7 +1134,13 @@ async def check_notification_time(tg_id: int, notification_type: str, hours: int Exception: В случае ошибки при проверке времени уведомления """ try: - result = await session.fetchrow( + # Если сессия не передана, создаем новое подключение + if session is None: + conn = await asyncpg.connect(DATABASE_URL) + else: + conn = session + + result = await conn.fetchrow( """ SELECT CASE @@ -1149,23 +1150,28 @@ async def check_notification_time(tg_id: int, notification_type: str, hours: int END as can_notify FROM notifications WHERE tg_id = $2 AND notification_type = $3 - """, - hours, - tg_id, - notification_type + """, + str(hours), # Преобразуем hours в строку + tg_id, + notification_type, ) + # Если сессия не была передана, закрываем подключение + if session is None and conn: + await conn.close() + # Если записи нет, значит уведомление можно отправить if result is None: return True can_notify = result['can_notify'] - - logger.info(f"Проверка уведомления типа {notification_type} для пользователя {tg_id}: {'можно отправить' if can_notify else 'слишком рано'}") - + + logger.info( + f"Проверка уведомления типа {notification_type} для пользователя {tg_id}: {'можно отправить' if can_notify else 'слишком рано'}" + ) + return can_notify - + except Exception as e: logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}") - raise - + return True # По умолчанию разрешаем отправку уведомления diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index d632a9eb..5ea3ca98 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -76,12 +76,12 @@ async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any result = await delete_coupon_from_db(coupon_code, session) if result: - await show_coupon_list(callback_query,session) + await show_coupon_list(callback_query, session) else: await callback_query.message.answer( f"❌ Купон с кодом {coupon_code} не найден.", ) - await show_coupon_list(callback_query,session) + await show_coupon_list(callback_query, session) except Exception as e: logger.error(f"Ошибка при удалении купона: {e}") diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 2870aadf..780a7e71 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -10,7 +10,15 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, SUPPORT_CHAT_URL -from database import add_connection, check_connection_exists, get_balance, get_trial, store_key, update_balance, use_trial +from database import ( + add_connection, + check_connection_exists, + get_balance, + get_trial, + store_key, + update_balance, + use_trial, +) from handlers.keys.key_utils import create_key_on_cluster 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 diff --git a/handlers/notifications.py b/handlers/notifications.py index 5a8049e7..613f940a 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -8,7 +8,14 @@ from py3xui import AsyncApi from client import delete_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB, TRIAL_TIME -from database import delete_key, get_balance, update_balance, update_key_expiry,add_notification,check_notification_time +from database import ( + add_notification, + check_notification_time, + delete_key, + get_balance, + update_balance, + update_key_expiry, +) from handlers.keys.key_utils import renew_key_in_cluster from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS from logger import logger @@ -220,10 +227,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): try: # Проверяем, можно ли отправить уведомление can_notify = await check_notification_time( - tg_id, - 'inactive_trial', - hours=24, # Уведомление не чаще, чем раз в 24 часа - session=conn + tg_id, 'inactive_trial', hours=24, session=conn # Уведомление не чаще, чем раз в 24 часа ) if can_notify and not await is_bot_blocked(bot, tg_id): @@ -245,11 +249,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.") # Добавляем запись о notification - await add_notification( - tg_id, - 'inactive_trial', - session=conn - ) + await add_notification(tg_id, 'inactive_trial', session=conn) except Exception as e: logger.error(f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}") diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index acda2a1a..d25c263d 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -1,4 +1,5 @@ from typing import Any + from aiocryptopay import AioCryptoPay, Networks from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -26,7 +27,7 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_cryptobot") -async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext,session:Any): +async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext, session: Any): builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -58,7 +59,7 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st if key_count == 0: exists = await check_connection_exists(callback_query.message.chat.id) if not exists: - await add_connection(tg_id=callback_query.message.chat.id, balance=0.0, trial=0,session=session) + await add_connection(tg_id=callback_query.message.chat.id, balance=0.0, trial=0, session=session) await callback_query.message.answer( "Выберите сумму пополнения:", reply_markup=builder.as_markup(), diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 4a21eac8..f9248aec 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -49,7 +49,7 @@ def generate_payment_link(amount, inv_id, description, tg_id): @router.callback_query(F.data == "pay_robokassa") -async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext,session:Any): +async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} initiated Robokassa payment.") @@ -86,7 +86,7 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0,session=session) + await add_connection(tg_id, balance=0.0, trial=0, session=session) logger.info(f"Created new connection for user {tg_id} with balance 0.0.") await callback_query.message.answer( diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index b55bff13..7e5730a5 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -1,4 +1,5 @@ from typing import Any + from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup @@ -21,7 +22,7 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_stars") -async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext,session:Any): +async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext, session: Any): tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() @@ -59,7 +60,7 @@ async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0,session=session) + await add_connection(tg_id, balance=0.0, trial=0, session=session) try: await callback_query.message.delete() diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index c8ec2ebf..edc9f071 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -31,7 +31,7 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_yookassa") -async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext,session:Any): +async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any): tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() @@ -68,7 +68,7 @@ async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, sta if key_count == 0: exists = await check_connection_exists(tg_id) if not exists: - await add_connection(tg_id, balance=0.0, trial=0,session=session) + await add_connection(tg_id, balance=0.0, trial=0, session=session) await callback_query.message.answer( text="Выберите сумму пополнения:", diff --git a/middlewares/delete.py b/middlewares/delete.py index 4ac4d812..e3911cf8 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -1,8 +1,9 @@ from typing import Any, Awaitable, Callable, Dict -from logger import logger + from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, Message, TelegramObject + class DeleteMessageMiddleware(BaseMiddleware): async def __call__( self, From 0e5236184c851a06aab266a9295c56830c9b01e1 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 24 Nov 2024 20:48:16 +0300 Subject: [PATCH 21/21] Refactor database notification query to remove unnecessary string conversion for hours parameter, improving data handling efficiency. --- database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database.py b/database.py index 08b934ce..1585824f 100644 --- a/database.py +++ b/database.py @@ -1151,7 +1151,7 @@ async def check_notification_time(tg_id: int, notification_type: str, hours: int FROM notifications WHERE tg_id = $2 AND notification_type = $3 """, - str(hours), # Преобразуем hours в строку + hours, # Убираем преобразование в строку tg_id, notification_type, )