diff --git a/handlers/instructions.jpg b/handlers/instructions.jpg new file mode 100644 index 00000000..9c52d9f6 Binary files /dev/null and b/handlers/instructions.jpg differ diff --git a/handlers/instructions.py b/handlers/instructions.py index a0d5f21e..c9c2dc5e 100644 --- a/handlers/instructions.py +++ b/handlers/instructions.py @@ -1,8 +1,12 @@ +import os from aiogram import types from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup - +from aiogram.types import BufferedInputFile async def send_instructions(callback_query: types.CallbackQuery): + # Удаляем предыдущее сообщение + await callback_query.message.delete() + instructions_message = ( "*📋 Инструкции по использованию вашего ключа:*\n\n" "1. **Скачайте приложение для вашего устройства:**\n" @@ -14,8 +18,26 @@ async def send_instructions(callback_query: types.CallbackQuery): "4. **Выберите 'Вставить из буфера обмена' для добавления ключа.**\n\n" "💬 Если у вас возникнут вопросы, не стесняйтесь обращаться в [поддержку](https://t.me/solonet_sup)." ) + + # Путь к изображению + image_path = os.path.join(os.path.dirname(__file__), 'instructions.jpg') + + # Проверка существования файла изображения + if not os.path.isfile(image_path): + await callback_query.message.answer("Файл изображения не найден.") + await callback_query.answer() + return + back_button = InlineKeyboardButton(text='🔙 Назад', callback_data='back_to_main') keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - await callback_query.message.edit_text(instructions_message, parse_mode='Markdown', reply_markup=keyboard) - await callback_query.answer() \ No newline at end of file + # Отправка изображения и сообщения + with open(image_path, 'rb') as image_from_buffer: + await callback_query.message.answer_photo( + BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"), + caption=instructions_message, + parse_mode='Markdown', + reply_markup=keyboard + ) + + await callback_query.answer() diff --git a/handlers/key_management.py b/handlers/key_management.py index 808b2f78..a2bfed0b 100644 --- a/handlers/key_management.py +++ b/handlers/key_management.py @@ -271,7 +271,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): ) keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='📖 Инструкции по использованию', callback_data='instructions')], + [InlineKeyboardButton(text='📘 Инструкции по использованию', callback_data='instructions')], [InlineKeyboardButton(text='🔙 Перейти в профиль', callback_data='view_profile')] ]) diff --git a/handlers/keys.py b/handlers/keys.py index c61558dc..3e7b6cba 100644 --- a/handlers/keys.py +++ b/handlers/keys.py @@ -318,57 +318,74 @@ async def process_callback_select_server(callback_query: types.CallbackQuery): try: conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow('SELECT email, expiry_time, server_id FROM keys WHERE client_id = $1', client_id) + # Начинаем транзакцию с блокировкой строки по client_id, чтобы предотвратить параллельную миграцию + async with conn.transaction(): + record = await conn.fetchrow( + 'SELECT email, expiry_time, server_id FROM keys WHERE client_id = $1 FOR UPDATE', client_id + ) - if record: - email = record['email'] - expiry_time = record['expiry_time'] - current_server_id = record['server_id'] + if record: + email = record['email'] + expiry_time = record['expiry_time'] + current_server_id = record['server_id'] - # Авторизация на новом сервере - session_new = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - new_expiry_time = int(datetime.utcnow().timestamp() * 1000) + (expiry_time - datetime.utcnow().timestamp() * 1000) + if current_server_id == server_id: + await callback_query.answer("Клиент уже на этом сервере.") + return - # Добавляем клиента на новый сервер - new_client_data = await add_client(session_new, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0, expiry_time=new_expiry_time, enable=True, flow="xtls-rprx-vision") + # Проверка наличия клиента на новом сервере перед добавлением + session_new = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + new_client_data = await add_client( + session_new, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0, + expiry_time=int(datetime.utcnow().timestamp() * 1000) + (expiry_time - datetime.utcnow().timestamp() * 1000), + enable=True, flow="xtls-rprx-vision" + ) + + if not new_client_data: + raise Exception("Ошибка при создании клиента на новом сервере.") - if new_client_data: # Генерация нового ключа new_key = await link(session_new, server_id, client_id, email) # Обновляем запись в БД - await conn.execute('UPDATE keys SET server_id = $1, key = $2 WHERE client_id = $3', - server_id, new_key, client_id) + await conn.execute( + 'UPDATE keys SET server_id = $1, key = $2 WHERE client_id = $3', + server_id, new_key, client_id + ) - # Логируем удаление клиента со старого сервера + # Удаление клиента со старого сервера try: session_old = await login_with_credentials(current_server_id, ADMIN_USERNAME, ADMIN_PASSWORD) success_delete = await delete_client(session_old, current_server_id, client_id) - if success_delete: - response_message = (f"Ключ успешно перемещен на новый сервер.\n\n" - f"Удалите старый ключ из вашего приложения и используйте новый для подключения к новому серверу:\n" - f"
{new_key}
") - else: - response_message = "Ошибка при удалении ключа с текущего сервера. Клиент не удален." - print(f"Не удалось удалить клиента {client_id} с сервера {current_server_id}. Ответ API: {success_delete}") + if not success_delete: + raise Exception(f"Ошибка при удалении клиента с сервера {current_server_id}") + + response_message = ( + f"Ключ успешно перемещен на новый сервер.\n\n" + f"Удалите старый ключ из вашего приложения и используйте новый для подключения к новому серверу:\n" + f"
{new_key}
" + ) except Exception as e: - response_message = f"Ошибка при удалении клиента с текущего сервера: {e}" - print(f"Ошибка при авторизации на старом сервере {current_server_id}: {e}") + response_message = f"Ключ перемещен, но возникла ошибка при удалении клиента с текущего сервера: {e}" + else: - response_message = "Ошибка при создании клиента на новом сервере." - else: - response_message = "Ключ не найден или уже удален." + 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, parse_mode='HTML') + await bot.edit_message_text( + response_message, chat_id=tg_id, message_id=callback_query.message.message_id, + reply_markup=keyboard, parse_mode='HTML' + ) 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, parse_mode='HTML') + await bot.edit_message_text( + f"Ошибка при смене локации: {e}", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode='HTML' + ) await callback_query.answer() diff --git a/handlers/profile.py b/handlers/profile.py index 880ab930..56fbd69f 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -11,7 +11,6 @@ class ReplenishBalanceState(StatesGroup): waiting_for_admin_confirmation = State() router = Router() - async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id username = callback_query.from_user.full_name @@ -33,7 +32,6 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta f"Обязательно подпишитесь на канал здесь\n" ) - # Если у клиента нет ключей, добавляем сообщение if key_count == 0: profile_message += "\nНажмите ➕Устройство снизу чтобы добавить устройство в VPN" @@ -41,14 +39,14 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta button_view_keys = InlineKeyboardButton(text='📱 Мои устройства', callback_data='view_keys') button_replenish_balance = InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance') button_invite = InlineKeyboardButton(text='👥 Пригласить', callback_data='invite') + button_instructions = InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions') button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu') keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [button_create_key], - [button_view_keys], - [button_replenish_balance], - [button_invite], - [button_back] + [button_create_key, button_view_keys], # В один ряд "➕ Устройство" и "📱 Мои устройства" + [button_replenish_balance], # Отдельная кнопка для пополнения баланса + [button_invite, button_instructions], # В один ряд "👥 Пригласить" и "📘 Инструкции" + [button_back] # Отдельная кнопка "Назад" ]) except Exception as e: diff --git a/handlers/start.py b/handlers/start.py index 16bcfe49..a173d9da 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -18,14 +18,14 @@ async def send_welcome_message(chat_id: int): welcome_text = ( "🎉 SoloNet — твой доступ в свободный интернет! 🌐✨\n\n" "Наши преимущества:\n" - "
"
+        "
" "🚀 Высокая скорость\n" "🔄 Стабильность\n" "🌍 Смена локаций\n" "💬 Отзывчивая поддержка\n" "📱💻 Для телефонов, компьютеров и планшетов\n" "💰 Реферальная программа: 25% от покупки\n" - "
" + "" "\n🎁 1 день бесплатно\n\n" "Перейдите в профиль для продолжения 👇" )