diff --git a/client.py b/client.py index fc865000..ecdb403d 100644 --- a/client.py +++ b/client.py @@ -1,6 +1,8 @@ import py3xui from loguru import logger +from config import TOTAL_GB + async def add_client( xui, @@ -64,9 +66,14 @@ async def extend_client_key(xui, email: str, new_expiry_time: int, client_id: st logger.info( f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}" ) + client.id = client_id client.expiry_time = new_expiry_time - + + if TOTAL_GB > 0: + client.total_gb = TOTAL_GB + logger.info(f"Установлен объем трафика для клиента {client.email}: {TOTAL_GB} ГБ") + await xui.client.update(client.id, client) logger.info( f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}." @@ -76,6 +83,7 @@ async def extend_client_key(xui, email: str, new_expiry_time: int, client_id: st logger.error(f"Ошибка при обновлении клиента с email {email}: {e}") + async def delete_client( xui, email: str, diff --git a/database.py b/database.py index fd03473b..901f1c4c 100644 --- a/database.py +++ b/database.py @@ -1,5 +1,5 @@ from datetime import datetime - +from loguru import logger import asyncpg from config import DATABASE_URL @@ -74,6 +74,20 @@ async def init_db(): await conn.close() +async def restore_trial(tg_id: int): + conn = await asyncpg.connect(DATABASE_URL) + 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() + + + async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0): conn = await asyncpg.connect(DATABASE_URL) diff --git a/handlers/admin/admin_commands.py b/handlers/admin/admin_commands.py index ebae8b95..8a701b18 100644 --- a/handlers/admin/admin_commands.py +++ b/handlers/admin/admin_commands.py @@ -3,7 +3,7 @@ from aiogram import Router, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from filters.admin import IsAdminFilter +from handlers.filters.admin import IsAdminFilter from loguru import logger from bot import bot diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 19dccfd2..1ab09779 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -3,17 +3,18 @@ from datetime import datetime import asyncpg 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.utils.keyboard import InlineKeyboardBuilder -from filters.admin import IsAdminFilter +from handlers.filters.admin import IsAdminFilter from backup import backup_database from bot import bot -from config import DATABASE_URL -from handlers.commands import send_message_to_all_clients +from config import DATABASE_URL, ADMIN_ID +from handlers.admin.admin_commands import send_message_to_all_clients router = Router() diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 305aae89..bab13ab0 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -7,12 +7,12 @@ 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 -from filters.admin import IsAdminFilter +from handlers.filters.admin import IsAdminFilter from loguru import logger from bot import bot from config import DATABASE_URL, SERVERS -from database import get_client_id_by_email, update_key_expiry +from database import get_client_id_by_email, update_key_expiry, restore_trial from handlers.admin.admin_panel import back_to_admin_menu from handlers.keys.key_utils import delete_key_from_server, renew_server_key from handlers.utils import sanitize_key_name @@ -55,7 +55,6 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext): builder = InlineKeyboardBuilder() - # Добавляем кнопки ключей for (email,) in key_records: builder.row( InlineKeyboardButton( @@ -63,14 +62,18 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext): ) ) - # Кнопка изменения баланса 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="back_to_user_editor") ) @@ -90,6 +93,23 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext): await conn.close() +@router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter()) +async def handle_restore_trial(callback_query: types.CallbackQuery): + tg_id = int(callback_query.data.split("_")[2]) + + await restore_trial(tg_id) + + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="back_to_user_editor") + ) + + await callback_query.message.edit_text( + "✅ Триал успешно восстановлен.", + reply_markup=builder.as_markup() + ) + + @router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter()) async def process_balance_change(callback_query: CallbackQuery, state: FSMContext): tg_id = int(callback_query.data.split("_")[2]) @@ -239,7 +259,15 @@ async def handle_key_name_input(message: types.Message, state: FSMContext): ) if not user_records: - await message.reply("🚫 Пользователь с указанным именем ключа не найден.") + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="back_to_user_editor") + ) + + await message.reply( + "🚫 Пользователь с указанным именем ключа не найден.", + reply_markup=builder.as_markup(), + ) await state.clear() return diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index aa8399b4..c2b46cbe 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -1,6 +1,7 @@ import asyncio import uuid from datetime import datetime, timedelta +from loguru import logger import asyncpg from aiogram import F, Router @@ -9,7 +10,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message from bot import bot, dp -from config import APP_URL, DATABASE_URL, PUBLIC_LINK, SERVERS +from config import DATABASE_URL, PUBLIC_LINK, SERVERS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, CONNECT_ANDROID, CONNECT_IOS 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_server @@ -92,6 +93,8 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext): async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id + logger.info(f"User {tg_id} confirmed creation of a new key.") + balance = await get_balance(tg_id) if balance < RENEWAL_PLANS["1"]["price"]: replenish_button = InlineKeyboardButton( @@ -102,33 +105,38 @@ async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContex await state.clear() return - await callback_query.message.edit_text( - "🔑 Пожалуйста, введите имя подключаемого устройства:" - ) + logger.info(f"Balance for user {tg_id} is sufficient. Asking for device name.") + + await callback_query.message.edit_text("🔑 Пожалуйста, введите имя подключаемого устройства:") await state.set_state(Form.waiting_for_key_name) + logger.info(f"State set to waiting_for_key_name for user {tg_id}") await state.update_data(creating_new_key=True) await callback_query.answer() - @dp.callback_query(F.data == "cancel_create_key") async def cancel_create_key(callback_query: CallbackQuery, state: FSMContext): await process_callback_view_profile(callback_query, state) await callback_query.answer() +@router.message(Form.waiting_for_key_name) async def handle_key_name_input(message: Message, state: FSMContext): tg_id = message.from_user.id key_name = sanitize_key_name(message.text) + logger.info(f"User {tg_id} is attempting to create a key with the name: {key_name}") + if not key_name: await message.bot.send_message( tg_id, "📝 Пожалуйста, назовите устройство на английском языке." ) + 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 in the database.") existing_key = await conn.fetchrow( "SELECT * FROM keys WHERE email = $1", key_name.lower() ) @@ -137,6 +145,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): tg_id, "❌ Упс! Это имя уже используется. Выберите другое уникальное название для ключа.", ) + logger.warning(f"Key name '{key_name}' already exists in the database for user {tg_id}.") await state.set_state(Form.waiting_for_key_name) return finally: @@ -149,6 +158,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): 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 ) @@ -159,6 +169,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): if trial_status == 0: expiry_time = current_time + timedelta(days=1, hours=3) + logger.info(f"Assigned 1-day trial to user {tg_id}.") else: balance = await get_balance(tg_id) if balance < RENEWAL_PLANS["1"]["price"]: @@ -171,32 +182,36 @@ async def handle_key_name_input(message: Message, state: FSMContext): "💳 Недостаточно средств для создания подписки на новое устройство. Пополните баланс в личном кабинете.", reply_markup=keyboard, ) + logger.warning(f"User {tg_id} has insufficient funds for key creation.") await state.clear() return await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"]) expiry_time = current_time + timedelta(days=30, hours=3) + logger.info(f"User {tg_id} balance deducted for key creation.") expiry_timestamp = int(expiry_time.timestamp() * 1000) public_link = f"{PUBLIC_LINK}{email}" + logger.info(f"Generated public link for the key: {public_link}") + button_profile = InlineKeyboardButton( text="👤 Личный кабинет", callback_data="view_profile" ) button_iphone = InlineKeyboardButton( - text="🍏 Подключить", url=f"{APP_URL}/?url=v2raytun://import/{public_link}" + text="🍏 Подключить", url=f"{CONNECT_IOS}{public_link}" ) button_android = InlineKeyboardButton( text="🤖 Подключить", - url=f"{APP_URL}/?url=v2raytun://import-sub?url={public_link}", + url=f"{CONNECT_ANDROID}{public_link}", ) button_download_ios = InlineKeyboardButton( - text="🍏 Скачать", url="https://apps.apple.com/ru/app/v2raytun/id6476628951" + text="🍏 Скачать", url=DOWNLOAD_IOS ) button_download_android = InlineKeyboardButton( text="🤖 Скачать", - url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru", + url=DOWNLOAD_ANDROID, ) keyboard = InlineKeyboardMarkup( @@ -211,6 +226,8 @@ async def handle_key_name_input(message: Message, state: FSMContext): days = remaining_time.days key_message = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅") + 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 ) @@ -230,6 +247,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): 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 ) @@ -242,16 +260,19 @@ async def handle_key_name_input(message: Message, state: FSMContext): finally: await conn.close() + logger.info(f"Storing key for user {tg_id} in the database.") await store_key( tg_id, client_id, email, expiry_timestamp, public_link, "all_servers" ) 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) diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 19e4fc96..9f494db8 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -9,7 +9,7 @@ from aiogram.types import BufferedInputFile from loguru import logger from bot import bot -from config import APP_URL, DATABASE_URL, PUBLIC_LINK, SERVERS +from config import DATABASE_URL, PUBLIC_LINK, SERVERS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, CONNECT_ANDROID, CONNECT_IOS from database import delete_key, get_balance, store_key, update_balance, update_key_expiry from handlers.keys.key_utils import delete_key_from_db, delete_key_from_server, renew_server_key, update_key_on_server from handlers.texts import INSUFFICIENT_FUNDS_MSG, KEY_NOT_FOUND_MSG, NO_KEYS, PLAN_SELECTION_MSG, RENEWAL_PLANS, SUCCESS_RENEWAL_MSG, key_message @@ -56,16 +56,32 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): "👆 Выберите устройство для управления подпиской:" ) - await bot.delete_message( - chat_id=tg_id, message_id=callback_query.message.message_id - ) + image_path = os.path.join(os.path.dirname(__file__), "pic_keys.jpg") + + try: + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) + 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=tg_id, + photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), + caption=response_message, + parse_mode="HTML", + reply_markup=inline_keyboard, + ) + else: + await bot.send_message( + chat_id=tg_id, + text=response_message, + reply_markup=inline_keyboard, + parse_mode="HTML", + ) - await bot.send_message( - chat_id=tg_id, - text=response_message, - reply_markup=inline_keyboard, - parse_mode="HTML", - ) else: response_message = NO_KEYS create_key_button = types.InlineKeyboardButton( @@ -79,16 +95,31 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): inline_keyboard=[[create_key_button], [back_button]] ) - await bot.delete_message( - chat_id=tg_id, message_id=callback_query.message.message_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 bot.send_message( - chat_id=tg_id, - text=response_message, - reply_markup=keyboard, - parse_mode="HTML", - ) + image_path = os.path.join(os.path.dirname(__file__), "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, + reply_markup=keyboard, + parse_mode="HTML", + ) finally: await conn.close() @@ -99,6 +130,7 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): await callback_query.answer() + @router.callback_query(F.data.startswith("view_key|")) async def process_callback_view_key(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id @@ -154,19 +186,19 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): download_android_button = types.InlineKeyboardButton( text="🤖 Скачать", - url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru", + url=DOWNLOAD_ANDROID, ) download_iphone_button = types.InlineKeyboardButton( text="🍏 Скачать", - url="https://apps.apple.com/ru/app/v2raytun/id6476628951", + url=DOWNLOAD_IOS, ) connect_iphone_button = types.InlineKeyboardButton( - text="🍏 Подключить", url=f"{APP_URL}/?url=v2raytun://import/{key}" + text="🍏 Подключить", url=f"{CONNECT_IOS}{key}" ) connect_android_button = types.InlineKeyboardButton( text="🤖 Подключить", - url=f"{APP_URL}/?url=v2raytun://import-sub?url={key}", + url=f"{CONNECT_ANDROID}{key}", ) renew_button = types.InlineKeyboardButton( diff --git a/handlers/keys/pic_keys.jpg b/handlers/keys/pic_keys.jpg new file mode 100644 index 00000000..a6ec6b6b Binary files /dev/null and b/handlers/keys/pic_keys.jpg differ diff --git a/handlers/notifications.py b/handlers/notifications.py index ecdf56f4..da1c0e05 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -217,13 +217,7 @@ async def notify_24h_keys( async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float): logger.info("Проверка истекших ключей...") - current_time = datetime.utcnow().timestamp() * 1000 - adjusted_current_time = current_time + (3 * 60 * 60 * 1000) - - logger.info( - f"Текущее время: {current_time}, Скорректированное текущее время: {adjusted_current_time}" - ) - + adjusted_current_time = current_time + (3 * 60 * 60 * 1000) expiring_keys = await conn.fetch( """ SELECT tg_id, client_id, expiry_time, email FROM keys @@ -231,7 +225,6 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: """, adjusted_current_time, ) - logger.info(f"Найдено {len(expiring_keys)} истекающих ключей.") for record in expiring_keys: @@ -239,17 +232,17 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: client_id = record["client_id"] email = record["email"] balance = await get_balance(tg_id) - expiry_time = record["expiry_time"] expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) current_date = datetime.utcnow() time_left = expiry_date - current_date + logger.info( f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}" ) message_expired = ( - "❌ Ваша подписка {email} истекла и была удалена!\n\n" + f"❌ Ваша подписка {email} истекла и была удалена!\n\n" "🔍 Перейдите в профиль для создания нового ключа.\n" "💡 Не откладывайте подключение VPN!" ) @@ -265,9 +258,6 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: (datetime.utcnow() + timedelta(days=30)).timestamp() * 1000 ) await update_key_expiry(client_id, new_expiry_time) - logger.info( - f"Ключ для клиента {tg_id} продлен до {datetime.utcfromtimestamp(new_expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}." - ) all_success = True for server_id in SERVERS: @@ -281,48 +271,38 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: ) if not success: all_success = False - logger.error( - f"Не удалось продлить ключ для пользователя {tg_id} на сервере {server_id}." - ) + logger.error(f"Не удалось продлить ключ для пользователя {tg_id} на сервере {server_id}.") if all_success: try: - await bot.send_message( - tg_id, KEY_RENEWED, reply_markup=keyboard - ) - logger.info( - f"Ключ для пользователя {tg_id} успешно продлен на месяц на всех серверах." - ) + await bot.send_message(tg_id, KEY_RENEWED, reply_markup=keyboard) + logger.info(f"Ключ для пользователя {tg_id} успешно продлен.") except Exception as e: - logger.error( - f"Ошибка при отправке уведомления о продлении ключа пользователю {tg_id}: {e}" - ) + logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") + else: try: - await bot.send_message( - tg_id, message_expired, reply_markup=keyboard - ) - await delete_key(client_id) - - for server_id in SERVERS: - xui = AsyncApi( - SERVERS[server_id]["API_URL"], - username=ADMIN_USERNAME, - password=ADMIN_PASSWORD, - ) - success = await delete_client(xui, email, client_id) - if success: - logger.info( - f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}." - ) - else: - logger.error( - f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}." - ) + await bot.send_message(tg_id, message_expired, reply_markup=keyboard) except Exception as e: - logger.error(f"Ошибка при удалении ключа для клиента {tg_id}: {e}") + if "chat not found" in str(e): + logger.warning(f"Чат для клиента {tg_id} не найден. Пропуск отправки сообщения.") + + await delete_key(client_id) + + for server_id in SERVERS: + xui = AsyncApi( + SERVERS[server_id]["API_URL"], + username=ADMIN_USERNAME, + password=ADMIN_PASSWORD, + ) + success = await delete_client(xui, email, client_id) + if success: + logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.") + else: + logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.") except Exception as e: logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}") await asyncio.sleep(1) + diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 4a0ce507..e0db3b22 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -11,7 +11,8 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web -from telegram import InlineKeyboardMarkup +from aiogram.types import InlineKeyboardMarkup + from bot import bot from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index deb49a52..e655407b 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -4,7 +4,8 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from loguru import logger -from telegram import LabeledPrice + +from aiogram.types import LabeledPrice from bot import bot from config import RUB_TO_XTR diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index 49d8fba0..e57cab33 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -63,25 +63,25 @@ async def process_callback_pay_yookassa( 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"], - ) + for i in range(0, 4, 2): + 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"], + ), + ) + + for i in range(4, len(PAYMENT_OPTIONS)): + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=PAYMENT_OPTIONS[i]["callback_data"], ) + ) key_count = await get_key_count(tg_id) @@ -107,6 +107,7 @@ async def process_callback_pay_yookassa( await callback_query.answer() + @router.callback_query(F.data == "back_to_profile") async def back_to_profile_handler( callback_query: types.CallbackQuery, state: FSMContext diff --git a/handlers/pic_invite.jpg b/handlers/pic_invite.jpg new file mode 100644 index 00000000..a6ec6b6b Binary files /dev/null and b/handlers/pic_invite.jpg differ diff --git a/handlers/profile.py b/handlers/profile.py index 913307ed..0b5377c6 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -92,23 +92,48 @@ async def invite_handler(callback_query: types.CallbackQuery): invite_message = invite_message_send(referral_link, referral_stats) + image_path = os.path.join(os.path.dirname(__file__), "pic_invite.jpg") + builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile") ) - await callback_query.message.delete() + try: + await callback_query.message.delete() + except Exception as e: + logger.error(f"Ошибка при удалении сообщения: {e}") - await bot.send_message( - chat_id=chat_id, - text=invite_message, - parse_mode="HTML", - reply_markup=builder.as_markup(), - ) + 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", + reply_markup=builder.as_markup(), + ) + except Exception as e: + await bot.send_message( + chat_id=chat_id, + text=f"❗️ Не удалось отправить сообщение. Техническая ошибка: {e}", + parse_mode="HTML", + 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): await process_callback_view_profile(callback_query, state) diff --git a/requirements.txt b/requirements.txt index 2ee02a5d..5aad9d5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ annotated-types==0.7.0 async-timeout==4.0.3 asyncpg==0.30.0 attrs==24.2.0 -certifi==2024.8.30 +certifi charset-normalizer==3.4.0 Deprecated==1.2.14 distro==1.9.0 @@ -17,8 +17,8 @@ magic-filter==1.0.12 multidict==6.1.0 netaddr==1.3.0 propcache==0.2.0 -pydantic==2.9.2 -pydantic_core==2.23.4 +pydantic +pydantic_core requests==2.32.3 typing_extensions==4.12.2 urllib3==2.2.3