From 84def9c3e37aff5e46401d73be06f883a1be4d01 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Mon, 11 Nov 2024 21:51:34 +0300 Subject: [PATCH] Add crypto(beta), fix import, fix requirements.txt --- bot.py | 9 +- handlers/admin/admin.py | 34 ++-- handlers/admin/admin_panel.py | 262 ++++++++++++++---------------- handlers/commands.py | 137 ++++++++-------- handlers/filters/admin.py | 9 + handlers/notifications.py | 2 +- handlers/payment/cryprobot_pay.py | 243 ++++++++++++++++++++++++++- handlers/payment/freekassa_pay.py | 32 ++-- handlers/payment/yookassa_pay.py | 77 +++------ main.py | 2 +- middlewares/admin.py | 28 ---- nginx.conf | 105 ++++++++++++ requirements.txt | 3 +- 13 files changed, 614 insertions(+), 329 deletions(-) create mode 100644 handlers/filters/admin.py delete mode 100644 middlewares/admin.py create mode 100644 nginx.conf diff --git a/bot.py b/bot.py index 89dfdc50..75f607c6 100644 --- a/bot.py +++ b/bot.py @@ -1,8 +1,7 @@ from aiogram import Bot, Dispatcher, Router from aiogram.fsm.storage.memory import MemoryStorage -from config import API_TOKEN, FREEKASSA_ENABLE, YOOKASSA_ENABLE -from middlewares.admin import AdminMiddleware +from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, YOOKASSA_ENABLE from middlewares.logging import UserActivityMiddleware bot = Bot(token=API_TOKEN) @@ -13,7 +12,7 @@ router = Router() from handlers import commands, notifications, profile, start from handlers.admin import admin, admin_panel, user_editor from handlers.keys import key_management, keys -from handlers.payment import freekassa_pay, yookassa_pay +from handlers.payment import cryprobot_pay, freekassa_pay, yookassa_pay dp.include_router(admin.router) dp.include_router(admin_panel.router) @@ -27,9 +26,9 @@ if YOOKASSA_ENABLE: dp.include_router(yookassa_pay.router) if FREEKASSA_ENABLE: dp.include_router(freekassa_pay.router) +if CRYPTO_BOT_ENABLE: + dp.include_router(cryprobot_pay.router) dp.include_router(notifications.router) -dp.message.middleware(AdminMiddleware()) -dp.callback_query.middleware(AdminMiddleware()) dp.message.middleware(UserActivityMiddleware()) dp.callback_query.middleware(UserActivityMiddleware()) diff --git a/handlers/admin/admin.py b/handlers/admin/admin.py index b17f5286..9022880d 100644 --- a/handlers/admin/admin.py +++ b/handlers/admin/admin.py @@ -1,27 +1,27 @@ from aiogram import Router, types from aiogram.filters import Command +from filters.admin import IsAdminFilter from database import add_balance_to_client, check_connection_exists router = Router() -@router.message(Command("add_balance")) -async def cmd_add_balance(message: types.Message, is_admin: bool): - if is_admin: - try: - _, client_id, amount = message.text.split() - amount = float(amount) +@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 + 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( - "Пожалуйста, используйте формат: /add_balance " - ) - except Exception as e: - await message.reply(f"Произошла ошибка: {e}") + await add_balance_to_client(int(client_id), amount) + await message.reply(f"Баланс клиента {client_id} увеличен на {amount} у.е.") + except ValueError: + await message.reply( + "Пожалуйста, используйте формат: /add_balance " + ) + except Exception as e: + await message.reply(f"Произошла ошибка: {e}") diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 1f5af451..cd12fca4 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -8,6 +8,7 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder +from filters.admin import IsAdminFilter from backup import backup_database from bot import bot @@ -22,162 +23,143 @@ class UserEditorState(StatesGroup): displaying_user_info = State() -@router.message(Command("admin")) -async def handle_admin_command(message: types.Message, is_admin: bool): - if is_admin: +@router.message(Command("admin"), IsAdminFilter()) +async def handle_admin_command(message: types.Message): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="Статистика пользователей", callback_data="user_stats" + ) + ) + builder.row( + InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor") + ) + builder.row( + InlineKeyboardButton( + text="Отправить сообщение всем клиентам", callback_data="send_to_alls" + ) + ) + builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups")) + builder.row( + InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot") + ) + await bot.send_message( + message.chat.id, "Панель администратора.", 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) + 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") + + active_keys = await conn.fetchval( + "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", + int(datetime.utcnow().timestamp() * 1000), + ) + expired_keys = total_keys - active_keys + + stats_message = ( + f"🔹 Общая статистика пользователей:\n" + f"• Всего пользователей: {total_users}\n" + f"• Всего ключей: {total_keys}\n" + f"• Всего рефералов: {total_referrals}\n" + f"• Активные ключи: {active_keys}\n" + f"• Истекшие ключи: {expired_keys}" + ) + builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="Статистика пользователей", callback_data="user_stats" - ) - ) - builder.row( - InlineKeyboardButton( - text="Редактор пользователей", callback_data="user_editor" - ) - ) - builder.row( - InlineKeyboardButton( - text="Отправить сообщение всем клиентам", callback_data="send_to_alls" - ) - ) - builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups")) - builder.row( - InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot") - ) - await bot.send_message( - message.chat.id, "Панель администратора.", reply_markup=builder.as_markup() - ) - else: - await bot.send_message(message.chat.id, "У вас нет доступа к этой команде.") - - -@router.callback_query(F.data == "user_stats") -async def user_stats_menu(callback_query: CallbackQuery, is_admin: bool): - if is_admin: - conn = await asyncpg.connect(DATABASE_URL) - 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") - - active_keys = await conn.fetchval( - "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", - int(datetime.utcnow().timestamp() * 1000), - ) - expired_keys = total_keys - active_keys - - stats_message = ( - f"🔹 Общая статистика пользователей:\n" - f"• Всего пользователей: {total_users}\n" - f"• Всего ключей: {total_keys}\n" - f"• Всего рефералов: {total_referrals}\n" - f"• Активные ключи: {active_keys}\n" - f"• Истекшие ключи: {expired_keys}" - ) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu") - ) - - await callback_query.message.edit_text( - stats_message, reply_markup=builder.as_markup(), parse_mode="HTML" - ) - finally: - await conn.close() - await callback_query.answer() - - -@router.callback_query(F.data == "send_to_alls") -async def handle_send_to_all( - callback_query: CallbackQuery, state: FSMContext, is_admin: bool -): - if is_admin: - await send_message_to_all_clients( - callback_query.message, state, from_panel=True - ) - await callback_query.answer() - - -@router.callback_query(F.data == "backups") -async def handle_backup(message: Message, is_admin: bool): - if is_admin: - await message.answer("Запускаю бэкап базы данных...") - await backup_database() - await message.answer("Бэкап завершен и отправлен админу.") - - -@router.callback_query(F.data == "restart_bot") -async def handle_restart(callback_query: CallbackQuery, is_admin: bool): - if is_admin: - try: - subprocess.run( - ["sudo", "systemctl", "restart", "bot.service"], - check=True, - capture_output=True, - text=True, - ) - await callback_query.message.answer("Бот успешно перезапущен.") - except subprocess.CalledProcessError as e: - await callback_query.message.answer( - f"Бот будет перезапущен через 30 секунд {e.stderr}" - ) - - -@router.callback_query(F.data == "user_editor") -async def user_editor_menu(callback_query: CallbackQuery, is_admin: bool): - if is_admin: - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="Поиск по имени ключа", callback_data="search_by_key_name" - ) - ) - builder.row( - InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id") - ) builder.row( InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu") ) + await callback_query.message.edit_text( - "Выберите метод поиска:", reply_markup=builder.as_markup() + stats_message, reply_markup=builder.as_markup(), parse_mode="HTML" + ) + finally: + await conn.close() + await callback_query.answer() + + +@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() + + +@router.callback_query(F.data == "backups", IsAdminFilter()) +async def handle_backup(message: Message): + await message.answer("Запускаю бэкап базы данных...") + await backup_database() + await message.answer("Бэкап завершен и отправлен админу.") + + +@router.callback_query(F.data == "restart_bot", IsAdminFilter) +async def handle_restart(callback_query: CallbackQuery): + try: + subprocess.run( + ["sudo", "systemctl", "restart", "bot.service"], + check=True, + capture_output=True, + text=True, + ) + await callback_query.message.answer("Бот успешно перезапущен.") + except subprocess.CalledProcessError as e: + await callback_query.message.answer( + f"Бот будет перезапущен через 30 секунд {e.stderr}" ) -@router.callback_query(F.data == "back_to_admin_menu") -async def back_to_admin_menu(callback_query: CallbackQuery, is_admin: bool): +@router.callback_query(F.data == "user_editor", IsAdminFilter()) +async def user_editor_menu(callback_query: CallbackQuery): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="Поиск по имени ключа", callback_data="search_by_key_name" + ) + ) + builder.row( + InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id") + ) + builder.row(InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")) + await callback_query.message.edit_text( + "Выберите метод поиска:", reply_markup=builder.as_markup() + ) + + +@router.callback_query(F.data == "back_to_admin_menu", IsAdminFilter()) +async def back_to_admin_menu(callback_query: CallbackQuery): try: await callback_query.message.delete() except Exception: pass tg_id = callback_query.from_user.id - if is_admin: - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="Статистика пользователей", callback_data="user_stats" - ) + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="Статистика пользователей", callback_data="user_stats" ) - builder.row( - InlineKeyboardButton( - text="Редактор пользователей", callback_data="user_editor" - ) - ) - builder.row( - InlineKeyboardButton( - text="Отправить сообщение всем клиентам", - callback_data="send_to_alls", - ) - ) - builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups")) - builder.row( - InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot") - ) - await bot.send_message( - tg_id, "Панель администратора", reply_markup=builder.as_markup() + ) + builder.row( + InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor") + ) + builder.row( + InlineKeyboardButton( + text="Отправить сообщение всем клиентам", + callback_data="send_to_alls", ) + ) + builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups")) + builder.row( + InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot") + ) + await bot.send_message( + tg_id, "Панель администратора", reply_markup=builder.as_markup() + ) async def handle_error(tg_id, callback_query, message): diff --git a/handlers/commands.py b/handlers/commands.py index f3e6614b..0df37208 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -4,6 +4,7 @@ from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import Message +from filters.admin import IsAdminFilter from loguru import logger from bot import bot @@ -25,14 +26,13 @@ class Form(StatesGroup): waiting_for_message = State() -@router.message(Command("backup")) -async def backup_command(message: Message, is_admin: bool): - if is_admin: - from backup import backup_database +@router.message(Command("backup"), IsAdminFilter()) +async def backup_command(message: Message): + from backup import backup_database - await message.answer("Запускаю бэкап базы данных...") - await backup_database() - await message.answer("Бэкап завершен и отправлен админу.") + await message.answer("Запускаю бэкап базы данных...") + await backup_database() + await message.answer("Бэкап завершен и отправлен админу.") @router.message(Command("start")) @@ -40,7 +40,7 @@ async def handle_start(message: types.Message, state: FSMContext): await start_command(message) -@router.message(Command("add_balance")) +@router.message(Command("add_balance"), IsAdminFilter()) async def handle_add_balance(message: types.Message, state: FSMContext): await cmd_add_balance(message) @@ -50,56 +50,53 @@ async def handle_menu(message: types.Message, state: FSMContext): await start_command(message) -@router.message(Command("send_trial")) -async def handle_send_trial_command( - message: types.Message, state: FSMContext, is_admin: bool -): - if is_admin: +@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: - conn = await asyncpg.connect(DATABASE_URL) - try: - records = await conn.fetch( - """ - SELECT tg_id FROM connections WHERE trial = 0 + records = await conn.fetch( """ + SELECT tg_id FROM connections WHERE trial = 0 + """ + ) + + if records: + for record in records: + tg_id = record["tg_id"] + trial_message = TRIAL + try: + await bot.send_message(chat_id=tg_id, text=trial_message) + except Exception as e: + if "Forbidden: bot was blocked by the user" in str(e): + logger.info( + f"Бот заблокирован пользователем с tg_id: {tg_id}" + ) + else: + logger.error( + f"Ошибка при отправке сообщения пользователю {tg_id}: {e}" + ) + + await message.answer( + "Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом." + ) + else: + await message.answer( + "Нет пользователей с не использованными пробными ключами." ) - if records: - for record in records: - tg_id = record["tg_id"] - trial_message = TRIAL - try: - await bot.send_message(chat_id=tg_id, text=trial_message) - except Exception as e: - if "Forbidden: bot was blocked by the user" in str(e): - logger.info( - f"Бот заблокирован пользователем с tg_id: {tg_id}" - ) - else: - logger.error( - f"Ошибка при отправке сообщения пользователю {tg_id}: {e}" - ) + finally: + await conn.close() - await message.answer( - "Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом." - ) - else: - await message.answer( - "Нет пользователей с не использованными пробными ключами." - ) - - finally: - await conn.close() - - except Exception as e: - await message.answer(f"Ошибка при отправке сообщений: {e}") + except Exception as e: + await message.answer(f"Ошибка при отправке сообщений: {e}") -@router.message(Command("send_to_all")) +@router.message(Command("send_to_all"), IsAdminFilter()) async def send_message_to_all_clients( - message: types.Message, state: FSMContext, is_admin: bool, from_panel=False + message: types.Message, state: FSMContext, from_panel=False ): - if from_panel and is_admin: + if from_panel: await message.answer( "Введите текст сообщения, который вы хотите отправить всем клиентам:" @@ -107,32 +104,32 @@ async def send_message_to_all_clients( await state.set_state(Form.waiting_for_message) -@router.message(Form.waiting_for_message) +@router.message(Form.waiting_for_message, IsAdminFilter()) async def process_message_to_all( - message: types.Message, state: FSMContext, is_admin: bool + message: types.Message, + state: FSMContext, ): - if is_admin: - text_message = message.text + text_message = message.text - try: - conn = await asyncpg.connect(DATABASE_URL) - tg_ids = await conn.fetch("SELECT tg_id FROM connections") + try: + conn = await asyncpg.connect(DATABASE_URL) + tg_ids = await conn.fetch("SELECT tg_id FROM connections") - for record in tg_ids: - tg_id = record["tg_id"] - try: - await bot.send_message(chat_id=tg_id, text=text_message) - except Exception as e: - logger.error( - f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя." - ) + for record in tg_ids: + tg_id = record["tg_id"] + try: + await bot.send_message(chat_id=tg_id, text=text_message) + except Exception as e: + logger.error( + f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя." + ) - await message.answer("Сообщение было отправлено всем клиентам.") - except Exception as e: - logger.error(f"Ошибка при подключении к базе данных: {e}") - await message.answer("Произошла ошибка при отправке сообщения.") - finally: - await conn.close() + await message.answer("Сообщение было отправлено всем клиентам.") + except Exception as e: + logger.error(f"Ошибка при подключении к базе данных: {e}") + await message.answer("Произошла ошибка при отправке сообщения.") + finally: + await conn.close() await state.clear() diff --git a/handlers/filters/admin.py b/handlers/filters/admin.py new file mode 100644 index 00000000..cce7dbed --- /dev/null +++ b/handlers/filters/admin.py @@ -0,0 +1,9 @@ +from aiogram.filters import BaseFilter +from aiogram.types import Message + +from config import ADMIN_ID + + +class IsAdminFilter(BaseFilter): + async def __call__(self, message: Message) -> bool: + return message.from_user.id == ADMIN_ID diff --git a/handlers/notifications.py b/handlers/notifications.py index 1603c0e2..434ebead 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -9,7 +9,7 @@ from py3xui import AsyncApi from client import delete_client, extend_client_key from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS from database import delete_key, get_balance, update_balance, update_key_expiry -from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWAL_FAILED, KEY_RENEWED, RENEWAL_PLANS +from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS router = Router() diff --git a/handlers/payment/cryprobot_pay.py b/handlers/payment/cryprobot_pay.py index 9fb260d4..b515e0ba 100644 --- a/handlers/payment/cryprobot_pay.py +++ b/handlers/payment/cryprobot_pay.py @@ -1,13 +1,187 @@ from aiocryptopay import AioCryptoPay, Networks +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 from aiohttp import web from loguru import logger -from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN +from bot import bot +from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT +from database import add_connection, check_connection_exists, get_key_count, update_balance +from handlers.profile import process_callback_view_profile +from handlers.texts import PAYMENT_OPTIONS + +router = Router() if CRYPTO_BOT_ENABLE: crypto = AioCryptoPay(token=CRYPTO_BOT_TOKEN, network=Networks.MAIN_NET) +class ReplenishBalanceState(StatesGroup): + choosing_amount = State() + waiting_for_payment_confirmation = State() + entering_custom_amount = 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 == "replenish_balance") +async def process_callback_replenish_balance( + 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( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=PAYMENT_OPTIONS[i]["callback_data"], + ), + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i + 1]["text"], + callback_data=PAYMENT_OPTIONS[i + 1]["callback_data"], + ), + ) + else: + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=PAYMENT_OPTIONS[i]["callback_data"], + ) + ) + + key_count = await get_key_count(tg_id) + + if key_count == 0: + exists = await check_connection_exists(tg_id) + if not exists: + await add_connection(tg_id, balance=0.0, trial=0) + + 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="Выберите сумму пополнения:", + reply_markup=builder.as_markup(), + ) + + await state.set_state(ReplenishBalanceState.choosing_amount) + await callback_query.answer() + + +@router.callback_query(F.data == "back_to_profile") +async def back_to_profile_handler( + callback_query: types.CallbackQuery, state: FSMContext +): + await process_callback_view_profile(callback_query, state) + + +@router.callback_query(F.data.startswith("amount|")) +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): + 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) + await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation) + + try: + invoice = await crypto.create_invoice( + asset="USDT", + amount=str(amount // RUB_TO_USDT), + description=f"Пополнения баланса на {amount//RUB_TO_USDT} руб", + payload=f"{callback_query.from_user.id}:{amount//RUB_TO_USDT}", + ) + + if hasattr(invoice, "bot_invoice_url"): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url), + InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"), + ) + await callback_query.message.edit_text( + text=f"Вы выбрали пополнение на {amount//RUB_TO_USDT} рублей.", + reply_markup=builder.as_markup(), + ) + else: + await send_message_with_deletion( + callback_query.from_user.id, "Ошибка при создании платежа.", state=state + ) + 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): try: data = await request.json() @@ -16,7 +190,9 @@ async def cryptobot_webhook(request): await process_crypto_payment(data["payload"]) return web.Response(status=200) else: - logger.warning(f"Неподдерживаемый тип обновления: {data.get('update_type')}") + logger.warning( + f"Неподдерживаемый тип обновления: {data.get('update_type')}" + ) return web.Response(status=400) except Exception as e: logger.error(f"Ошибка обработки вебхука: {e}") @@ -26,7 +202,66 @@ async def cryptobot_webhook(request): async def process_crypto_payment(payload): if payload["status"] == "paid": custom_payload = payload["payload"] - user_id, sub_type = custom_payload.split(":") - # TODO + user_id_str, amount_str = custom_payload.split(":") + try: + user_id = int(user_id_str) + amount = float(amount_str) + logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}") + await update_balance(user_id, amount) + await send_payment_success_notification(user_id, amount) + except ValueError as e: + logger.error(f"Ошибка конвертации user_id или amount: {e}") else: logger.warning(f"Получен неоплаченный инвойс: {payload}") + + +@router.callback_query(F.data == "enter_custom_amount") +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) + await callback_query.answer() + + +@router.message(State(ReplenishBalanceState.entering_custom_amount)) +async def process_custom_amount_input(message: types.Message, state: FSMContext): + if message.text.isdigit(): + amount = int(message.text) + if amount <= 0: + await message.answer( + "Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:" + ) + return + + await state.update_data(amount=amount) + await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation) + try: + invoice = await crypto.create_invoice( + asset="USDT", + amount=str(amount // RUB_TO_USDT), + description=f"Пополнения баланса на {amount//RUB_TO_USDT} руб", + payload=f"{message.from_user.id}:{amount//RUB_TO_USDT}", + ) + + if hasattr(invoice, "bot_invoice_url"): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url), + InlineKeyboardButton( + text="⬅️ Назад", callback_data="back_to_profile" + ), + ) + await message.message.edit_text( + text=f"Вы выбрали пополнение на {amount//RUB_TO_USDT} рублей.", + 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/payment/freekassa_pay.py b/handlers/payment/freekassa_pay.py index 63c67abc..1059ecbf 100644 --- a/handlers/payment/freekassa_pay.py +++ b/handlers/payment/freekassa_pay.py @@ -6,7 +6,8 @@ import requests from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web from loguru import logger @@ -95,16 +96,27 @@ async def process_callback_pay_freekassa( callback_query: types.CallbackQuery, state: FSMContext ): tg_id = callback_query.from_user.id - inline_keyboard = [] - for payment in PAYMENT_OPTIONS: - inline_keyboard.append( - InlineKeyboardButton( - text=payment.get("text"), callback_data=payment.get("callback_data") + builder = InlineKeyboardBuilder() + for i in range(0, len(PAYMENT_OPTIONS), 2): + if i + 1 < len(PAYMENT_OPTIONS): + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=PAYMENT_OPTIONS[i]["callback_data"], + ), + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i + 1]["text"], + callback_data=PAYMENT_OPTIONS[i + 1]["callback_data"], + ), + ) + else: + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=PAYMENT_OPTIONS[i]["callback_data"], + ) ) - ) - - amount_keyboard = InlineKeyboardMarkup(inline_keyboard) await bot.delete_message( chat_id=tg_id, message_id=callback_query.message.message_id @@ -113,7 +125,7 @@ async def process_callback_pay_freekassa( await bot.send_message( chat_id=tg_id, text="Выберите сумму пополнения через FreeKassa:", - reply_markup=amount_keyboard, + reply_markup=builder.as_markup(), ) await state.set_state(ReplenishBalanceState.choosing_amount) diff --git a/handlers/payment/yookassa_pay.py b/handlers/payment/yookassa_pay.py index 4755cfe1..b1d1f36c 100644 --- a/handlers/payment/yookassa_pay.py +++ b/handlers/payment/yookassa_pay.py @@ -17,10 +17,9 @@ from handlers.texts import PAYMENT_OPTIONS router = Router() -Configuration.account_id = YOOKASSA_SHOP_ID -Configuration.secret_key = YOOKASSA_SECRET_KEY - if YOOKASSA_ENABLE: + Configuration.account_id = YOOKASSA_SHOP_ID + Configuration.secret_key = YOOKASSA_SECRET_KEY logger.debug(f"Account ID: {YOOKASSA_SHOP_ID}") logger.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}") @@ -64,39 +63,25 @@ async def process_callback_replenish_balance( builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text=PAYMENT_OPTIONS[0]["text"], - callback_data=PAYMENT_OPTIONS[0]["callback_data"], - ), - InlineKeyboardButton( - text=PAYMENT_OPTIONS[1]["text"], - callback_data=PAYMENT_OPTIONS[1]["callback_data"], - ), - ) - builder.row( - InlineKeyboardButton( - text=PAYMENT_OPTIONS[2]["text"], - callback_data=PAYMENT_OPTIONS[2]["callback_data"], - ), - InlineKeyboardButton( - text=PAYMENT_OPTIONS[3]["text"], - callback_data=PAYMENT_OPTIONS[3]["callback_data"], - ), - ) - - builder.row( - InlineKeyboardButton( - text=PAYMENT_OPTIONS[4]["text"], - callback_data=PAYMENT_OPTIONS[4]["callback_data"], - ) - ) - builder.row( - InlineKeyboardButton( - text=PAYMENT_OPTIONS[5]["text"], - callback_data=PAYMENT_OPTIONS[5]["callback_data"], - ) - ) + for i in range(0, len(PAYMENT_OPTIONS), 2): + if i + 1 < len(PAYMENT_OPTIONS): + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=PAYMENT_OPTIONS[i]["callback_data"], + ), + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i + 1]["text"], + callback_data=PAYMENT_OPTIONS[i + 1]["callback_data"], + ), + ) + else: + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=PAYMENT_OPTIONS[i]["callback_data"], + ) + ) key_count = await get_key_count(tg_id) @@ -215,20 +200,15 @@ async def process_amount_selection( async def send_payment_success_notification(user_id: int, amount: float): try: - profile_keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [ - InlineKeyboardButton( - text="Перейти в профиль", callback_data="view_profile" - ) - ] - ] + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile") ) await bot.send_message( chat_id=user_id, text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", - reply_markup=profile_keyboard, + reply_markup=builder.as_markup(), ) except Exception as e: logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") @@ -236,26 +216,19 @@ async def send_payment_success_notification(user_id: int, amount: float): async def yookassa_webhook(request): event = await request.json() - logger.debug(f"Webhook event received: {event}") - if event["event"] == "payment.succeeded": user_id_str = event["object"]["metadata"]["user_id"] amount_str = event["object"]["amount"]["value"] - try: user_id = int(user_id_str) amount = float(amount_str) - logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}") await update_balance(user_id, amount) - await send_payment_success_notification(user_id, amount) - except ValueError as e: logger.error(f"Ошибка конвертации user_id или amount: {e}") return web.Response(status=400) - return web.Response(status=200) diff --git a/main.py b/main.py index c7b3cae5..00e6b6fe 100644 --- a/main.py +++ b/main.py @@ -11,9 +11,9 @@ from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, SUB_PATH, WEBAPP_HOST, W from database import init_db from handlers.keys.subscriptions import handle_subscription from handlers.notifications import notify_expiring_keys +from handlers.payment.cryprobot_pay import cryptobot_webhook from handlers.payment.freekassa_pay import freekassa_webhook from handlers.payment.yookassa_pay import yookassa_webhook -from handlers.payment.cryprobot_pay import cryptobot_webhook async def periodic_notifications(): diff --git a/middlewares/admin.py b/middlewares/admin.py deleted file mode 100644 index 753d8d3f..00000000 --- a/middlewares/admin.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any, Awaitable, Callable, Dict - -from aiogram import BaseMiddleware -from aiogram.types import CallbackQuery, Message, TelegramObject - -from config import ADMIN_ID - - -class AdminMiddleware(BaseMiddleware): - async def __call__( - self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], - event: TelegramObject, - data: Dict[str, Any], - ) -> Any: - user_id = None - - if isinstance(event, Message): - user_id = event.from_user.id - elif isinstance(event, CallbackQuery): - user_id = event.from_user.id - - if user_id != int(ADMIN_ID): - data["is_admin"] = False - else: - data["is_admin"] = True - - return await handler(event, data) diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..5c671969 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,105 @@ +# Обработка HTTP-запросов, редирект на HTTPS +server { + listen 80; + server_name example.com; # Домен проекта + + location / { + # Перенаправление с HTTP на HTTPS + if ($arg_url != "") { + return 301 $arg_url; + } + + return 404 "URL-аргумент отсутствует"; + } +} + +# Обработка HTTPS-запросов (статический сайт и прокси) +server { + listen 443 ssl; + server_name example.com; # Домен проекта + + # SSL настройки + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # Путь к SSL-сертификату + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Путь к приватному ключу + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Обработка подписок + location /sub { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + add_header Content-Type text/plain; + add_header Content-Disposition inline; + add_header Cache-Control no-store; + add_header Pragma no-cache; + } + + # Статический сайт + root /var/www/website; # Путь к статическому сайту + index index.html; # Основной файл + + location / { + # Перенаправление с HTTP на HTTPS + if ($arg_url != "") { + return 301 $arg_url; + } + + return 404 "URL-аргумент отсутствует"; + } + + # Вебхуки из main.py + location /webhook { + proxy_pass http://localhost:3001/webhook; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Вебхук Юкассы + location /yookassa/webhook { + proxy_pass http://localhost:3001/yookassa/webhook; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Вебхук FreeCassa + location /freekassa/webhook { + proxy_pass http://localhost:3001/freekassa/webhook; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Вебхук CryptoBot + location /cryptobot/webhook { + proxy_pass http://localhost:3001/cryptobot/webhook; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } +} diff --git a/requirements.txt b/requirements.txt index da96b1f2..2ee02a5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,4 +26,5 @@ wrapt==1.16.0 yarl==1.15.5 yookassa==3.3.0 loguru -aiocryptopay \ No newline at end of file +aiocryptopay +py3xui \ No newline at end of file