diff --git a/backup.py b/backup.py index a655822f..47ebd2d7 100644 --- a/backup.py +++ b/backup.py @@ -4,7 +4,8 @@ import subprocess from datetime import datetime from aiogram.types import BufferedInputFile -from config import ADMIN_ID, DB_NAME, DB_PASSWORD, DB_USER, BACK_DIR + +from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER async def backup_database(): diff --git a/bot.py b/bot.py index 89eeb659..fae3bb08 100644 --- a/bot.py +++ b/bot.py @@ -8,10 +8,10 @@ storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) 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 pay, freekassa -from handlers import (notifications, profile, start, commands) +from handlers.payment import freekassa, pay dp.include_router(admin.router) dp.include_router(admin_panel.router) diff --git a/handlers/admin/admin.py b/handlers/admin/admin.py index cc3f6485..207dfcbf 100644 --- a/handlers/admin/admin.py +++ b/handlers/admin/admin.py @@ -1,11 +1,15 @@ +from datetime import datetime + +import asyncpg from aiogram import Router, types from aiogram.filters import Command -from database import add_balance_to_client, check_connection_exists, update_key_expiry, get_client_id_by_email, get_tg_id_by_client_id -from config import ADMIN_ID, DATABASE_URL, ADMIN_PASSWORD, ADMIN_USERNAME -from datetime import datetime -import asyncpg + from auth import login_with_credentials from client import extend_client_key_admin +from config import ADMIN_ID, ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL +from database import (add_balance_to_client, check_connection_exists, + get_client_id_by_email, get_tg_id_by_client_id, + update_key_expiry) router = Router() diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 0b2d32d0..f0af4b91 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -1,16 +1,18 @@ -from aiogram import Router, types -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery -from aiogram.filters import Command -from aiogram.fsm.state import StatesGroup, State -from config import ADMIN_ID, DATABASE_URL -import asyncpg -from datetime import datetime -from bot import bot import subprocess -from backup import backup_database -from handlers.commands import send_message_to_all_clients -from aiogram.types import Message +from datetime import datetime + +import asyncpg +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 (CallbackQuery, InlineKeyboardButton, + InlineKeyboardMarkup, Message) + +from backup import backup_database +from bot import bot +from config import ADMIN_ID, DATABASE_URL +from handlers.commands import send_message_to_all_clients router = Router() diff --git a/handlers/admin/user_editor.py b/handlers/admin/user_editor.py index cdd475d0..b308d4e0 100644 --- a/handlers/admin/user_editor.py +++ b/handlers/admin/user_editor.py @@ -1,19 +1,19 @@ -from aiogram import Router, types, F -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery +from datetime import datetime + +import asyncpg +from aiogram import F, Router, types from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import StatesGroup, State -from config import DATABASE_URL, SERVERS -import asyncpg -from datetime import datetime -from bot import bot -from database import update_key_expiry, get_client_id_by_email, get_tg_id_by_client_id -from config import DATABASE_URL, ADMIN_PASSWORD, ADMIN_USERNAME -from datetime import datetime -import asyncpg +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import (CallbackQuery, InlineKeyboardButton, + InlineKeyboardMarkup) + from auth import login_with_credentials -from client import extend_client_key_admin +from bot import bot +from client import delete_client, extend_client_key_admin +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS +from database import (get_client_id_by_email, get_tg_id_by_client_id, + update_key_expiry) from handlers.admin.admin_panel import back_to_admin_menu -from client import delete_client router = Router() diff --git a/handlers/commands.py b/handlers/commands.py index 196e3910..e34a4c1e 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -1,18 +1,19 @@ +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 -import asyncpg +from aiogram.types import Message from bot import bot from config import ADMIN_ID, DATABASE_URL -from handlers.payment.pay import ReplenishBalanceState, process_custom_amount_input +from handlers.admin.admin import cmd_add_balance +from handlers.keys.key_management import handle_key_name_input +from handlers.payment.pay import (ReplenishBalanceState, + process_custom_amount_input) from handlers.profile import process_callback_view_profile from handlers.start import start_command from handlers.texts import TRIAL -from handlers.admin.admin import cmd_add_balance -from handlers.keys.key_management import handle_key_name_input -from aiogram.types import Message router = Router() diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 6a1eb570..3c073f60 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -6,6 +6,7 @@ from aiogram.types import (BufferedInputFile, InlineKeyboardButton, from handlers.texts import INSTRUCTIONS + async def send_instructions(callback_query: types.CallbackQuery): await callback_query.message.delete() diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 17fd9030..acb46fe7 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -1,7 +1,8 @@ +import asyncio +import logging import uuid from datetime import datetime, timedelta -from bot import dp, bot import asyncpg from aiogram import F, Router from aiogram.fsm.context import FSMContext @@ -9,10 +10,11 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message) -from auth import login_with_credentials, link_subscription +from auth import login_with_credentials +from bot import bot, dp from client import add_client -from config import (ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, - SERVERS, APP_URL) +from config import (ADMIN_PASSWORD, ADMIN_USERNAME, APP_URL, DATABASE_URL, + PUBLIC_LINK, SERVERS) from database import add_connection, get_balance, store_key, update_balance from handlers.instructions.instructions import send_instructions from handlers.profile import process_callback_view_profile @@ -21,6 +23,9 @@ from handlers.utils import sanitize_key_name router = Router() +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + class Form(StatesGroup): waiting_for_server_selection = State() waiting_for_key_name = State() @@ -31,36 +36,20 @@ class Form(StatesGroup): async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id - server_buttons = [] - conn = await asyncpg.connect(DATABASE_URL) try: - for server_id, server in SERVERS.items(): - count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE server_id = $1', server_id) - percent_full = (count / 60) * 100 if count <= 60 else 100 - server_name = f"{server['name']} ({percent_full:.1f}%)" - server_buttons.append([InlineKeyboardButton(text=server_name, callback_data=f'select_server|{server_id}')]) - finally: - await conn.close() + await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + except Exception: + pass - button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='view_profile') - server_buttons.append([button_back]) - - await callback_query.message.delete() - await bot.send_message( - chat_id=tg_id, - text="⚙️ Выберите сервер для создания ключа:", - parse_mode="HTML", - reply_markup=InlineKeyboardMarkup(inline_keyboard=server_buttons) - ) - - await state.set_state(Form.waiting_for_server_selection) + server_id = "все сервера" + await state.update_data(selected_server_id=server_id) + await select_server(callback_query, state) await callback_query.answer() -@dp.callback_query(F.data.startswith('select_server|')) + async def select_server(callback_query: CallbackQuery, state: FSMContext): - server_id = callback_query.data.split('|')[1] - await state.update_data(selected_server_id=server_id) + selected_server_id = (await state.get_data()).get("selected_server_id") conn = await asyncpg.connect(DATABASE_URL) try: @@ -71,24 +60,27 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext): trial_status = existing_connection['trial'] if existing_connection else 0 if trial_status == 1: - await callback_query.message.edit_text( - KEY, + await bot.send_message( + chat_id=callback_query.from_user.id, + text=KEY, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='✅ Да, создать новый ключ', callback_data='confirm_create_new_key')], + [InlineKeyboardButton(text='✅ Да, подключить новое устройство', callback_data='confirm_create_new_key')], [InlineKeyboardButton(text='↩️ Назад', callback_data='cancel_create_key')] ]) ) await state.update_data(creating_new_key=True) else: - await callback_query.message.edit_text( - KEY_TRIAL, + await bot.send_message( + chat_id=callback_query.from_user.id, + text=KEY_TRIAL, parse_mode="HTML" ) await state.set_state(Form.waiting_for_key_name) await callback_query.answer() + @dp.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 @@ -106,7 +98,7 @@ async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContex await state.clear() return - await callback_query.message.edit_text("🔑 Пожалуйста, введите имя нового ключа:") + await callback_query.message.edit_text("🔑 Пожалуйста, введите имя подключаемого устройства:") await state.set_state(Form.waiting_for_key_name) await state.update_data(creating_new_key=True) @@ -122,7 +114,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): key_name = sanitize_key_name(message.text) if not key_name: - await message.bot.send_message(tg_id, "📝 Пожалуйста, назовите ключ устройства на английском языке.") + await message.bot.send_message(tg_id, "📝 Пожалуйста, назовите устройство на английском языке.") return conn = await asyncpg.connect(DATABASE_URL) @@ -136,10 +128,6 @@ async def handle_key_name_input(message: Message, state: FSMContext): await conn.close() data = await state.get_data() - creating_new_key = data.get('creating_new_key', False) - server_id = data.get('selected_server_id') - - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) client_id = str(uuid.uuid4()) email = key_name.lower() current_time = datetime.utcnow() @@ -160,7 +148,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): if balance < 100: replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile') keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]]) - await message.bot.send_message(tg_id, "❗️ Недостаточно средств на балансе для создания нового ключа.", reply_markup=keyboard) + await message.bot.send_message(tg_id, "❗️ Недостаточно средств на балансе для создания подписки на новое устройство.", reply_markup=keyboard) await state.clear() return @@ -168,25 +156,49 @@ async def handle_key_name_input(message: Message, state: FSMContext): expiry_time = current_time + timedelta(days=30, hours=3) expiry_timestamp = int(expiry_time.timestamp() * 1000) + public_link = f"{PUBLIC_LINK}{email}" + + button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile') + button_iphone = InlineKeyboardButton( + text='🍏 Подключить', + url=f'{APP_URL}/?url=v2raytun://import/{public_link}' + ) + button_android = InlineKeyboardButton( + text='🤖 Подключить', + url=f'{APP_URL}/?url=v2raytun://import-sub?url={public_link}' + ) + + button_download_ios = InlineKeyboardButton( + text='🍏 Скачать', + url="https://apps.apple.com/ru/app/v2raytun/id6476628951" + ) + button_download_android = InlineKeyboardButton( + text='🤖 Скачать', + url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru" + ) + + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [button_download_ios, button_download_android], + [button_iphone, button_android], + [button_profile] + ]) + + remaining_time = expiry_time - current_time + days = remaining_time.days + key_message = key_message_success(public_link, f"Оставшееся время ключа: {days} день") + + await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard) try: - response = await add_client(session, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0, expiry_time=expiry_timestamp, enable=True, flow="xtls-rprx-vision") - - if not response.get("success", True): - error_msg = response.get("msg", "Неизвестная ошибка.") - if "Duplicate email" in error_msg: - await message.bot.send_message(tg_id, "❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.") - await state.set_state(Form.waiting_for_key_name) - return - else: - raise Exception(error_msg) + tasks = [] + for server_id in SERVERS: + tasks.append(asyncio.create_task(create_key_on_server(server_id, tg_id, client_id, email, expiry_timestamp))) - connection_link = await link_subscription(email, server_id) + await asyncio.gather(*tasks) conn = await asyncpg.connect(DATABASE_URL) try: 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: @@ -194,39 +206,30 @@ async def handle_key_name_input(message: Message, state: FSMContext): finally: await conn.close() - await store_key(tg_id, client_id, email, expiry_timestamp, connection_link, server_id) - - remaining_time = expiry_time - current_time - days = remaining_time.days - hours, remainder = divmod(remaining_time.seconds, 3600) - minutes, _ = divmod(remainder, 60) - - remaining_time_message = f"Оставшееся время ключа: {days} день" - - button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile') - button_iphone = InlineKeyboardButton( - text='🍏IPhone', - url=f'{APP_URL}/?url=v2raytun://import/{connection_link}' - ) - button_android = InlineKeyboardButton( - text='🤖Android', - url=f'{APP_URL}/?url=v2raytun://import-sub?url={connection_link}' - ) - - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [button_iphone, button_android], - [button_profile] - ]) - - key_message = key_message_success(connection_link, remaining_time_message) - - await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard) + await store_key(tg_id, client_id, email, expiry_timestamp, public_link, 'all_servers') except Exception as e: await message.bot.send_message(tg_id, f"❌ Ошибка при создании ключа: {e}") await state.clear() +async def create_key_on_server(server_id, tg_id, client_id, email, expiry_timestamp): + try: + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + response = await add_client( + session, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0, + expiry_time=expiry_timestamp, enable=True, flow="xtls-rprx-vision" + ) + if not response.get("success", True): + error_msg = response.get("msg", "Неизвестная ошибка.") + if "Duplicate email" in error_msg: + raise ValueError(f"Имя {email} уже занято на сервере {server_id}") + else: + raise Exception(error_msg) + except Exception as e: + logger.error(f"Ошибка на сервере {server_id}: {e}") + + @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 75b58f64..1351b2f5 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -1,22 +1,31 @@ +import asyncio import locale +import logging +import os from datetime import datetime, timedelta import asyncpg from aiogram import Router, types +from aiogram.types import BufferedInputFile -from auth import login_with_credentials, link_subscription +from auth import login_with_credentials from bot import bot from client import add_client, delete_client, extend_client_key -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS, APP_URL -from database import get_balance, update_balance -from handlers.texts import NO_KEYS -from handlers.texts import key_message, key_relocated -from handlers.texts import RENEWAL_PLANS, INSUFFICIENT_FUNDS_MSG, KEY_NOT_FOUND_MSG, SUCCESS_RENEWAL_MSG, ERROR_RENEWAL_MSG, PLAN_SELECTION_MSG +from config import (ADMIN_PASSWORD, ADMIN_USERNAME, APP_URL, DATABASE_URL, + PUBLIC_LINK, SERVERS) +from database import (delete_key, get_balance, store_key, update_balance, + update_key_expiry) +from handlers.texts import (INSUFFICIENT_FUNDS_MSG, KEY_NOT_FOUND_MSG, NO_KEYS, + PLAN_SELECTION_MSG, RENEWAL_PLANS, + SUCCESS_RENEWAL_MSG, key_message) locale.setlocale(locale.LC_TIME, 'ru_RU.UTF-8') router = Router() +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + @router.callback_query(lambda c: c.data == 'view_keys') async def process_callback_view_keys(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id @@ -42,7 +51,7 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) response_message = ( "Это ваши устройства:\n\n" - "Нажмите на имя устройства для управления его ключом." + "Нажмите на имя устройства для управления его подпиской." ) await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) @@ -83,10 +92,15 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): key_name, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2] 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 k.key, k.expiry_time, k.server_id + 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) @@ -96,8 +110,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): expiry_time = record['expiry_time'] server_id = record['server_id'] - server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер') - + server_name = SERVERS.get(server_id, {}).get('name', 'мультисервер') expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) current_date = datetime.utcnow() time_left = expiry_date - current_date @@ -111,29 +124,60 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): 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) - response_message = ( - key_message(key, formatted_expiry_date, days_left_message, server_name) - ) - - renew_button = types.InlineKeyboardButton(text='⏳ Продлить ключ', callback_data=f'renew_key|{client_id}') - instructions_button = types.InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions') - delete_button = types.InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key|{client_id}') - change_location_button = types.InlineKeyboardButton(text='🌍 Сменить локацию', callback_data=f'change_location|{client_id}') - back_button = types.InlineKeyboardButton(text='🔙 Назад в профиль', callback_data='view_profile') - - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [instructions_button], - [renew_button, delete_button], - [change_location_button], - [back_button] - ] + download_android_button = types.InlineKeyboardButton( + text='🤖 Скачать', + url='https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru' + ) + download_iphone_button = types.InlineKeyboardButton( + text='🍏 Скачать', + url='https://apps.apple.com/ru/app/v2raytun/id6476628951' ) - await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML") + connect_iphone_button = types.InlineKeyboardButton( + text='🍏 Подключить', + url=f'{APP_URL}/?url=v2raytun://import/{key}' + ) + connect_android_button = types.InlineKeyboardButton( + text='🤖 Подключить', + url=f'{APP_URL}/?url=v2raytun://import-sub?url={key}' + ) + + renew_button = types.InlineKeyboardButton(text='⏳ Продлить', callback_data=f'renew_key|{client_id}') + delete_button = types.InlineKeyboardButton(text='❌ Удалить', callback_data=f'delete_key|{client_id}') + back_button = types.InlineKeyboardButton(text='🔙 Назад в профиль', callback_data='view_profile') + + inline_keyboard = [ + [download_iphone_button, download_android_button], + [connect_iphone_button, connect_android_button], + [renew_button, delete_button], + ] + + if not key.startswith(PUBLIC_LINK): + update_subscription_button = types.InlineKeyboardButton(text='🔄 Обновить подписку', callback_data=f'update_subscription|{client_id}') + inline_keyboard.append([update_subscription_button]) + + inline_keyboard.append([back_button]) + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard) + + image_path = os.path.join(os.path.dirname(__file__), '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.edit_message_text("Информация о ключе не найдена.", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML") + await bot.send_message(chat_id=tg_id, text="Информация о подписке не найдена.", parse_mode="HTML") finally: await conn.close() @@ -143,25 +187,137 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): await callback_query.answer() +@router.callback_query(lambda c: c.data.startswith('update_subscription|')) +async def process_callback_update_subscription(callback_query: types.CallbackQuery): + tg_id = callback_query.from_user.id + client_id = 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 + FROM keys k + WHERE k.tg_id = $1 AND k.client_id = $2 + ''', tg_id, client_id) + + if record: + expiry_time = record['expiry_time'] + email = record['email'] + public_link = f"{PUBLIC_LINK}{email}" + + try: + await conn.execute(''' + DELETE FROM keys + WHERE tg_id = $1 AND client_id = $2 + ''', tg_id, client_id) + except Exception as delete_error: + await bot.send_message(tg_id, f"Ошибка при удалении старой подписки: {delete_error}") + return + + tasks = [] + for server_id in SERVERS: + tasks.append(update_key_on_server(tg_id, client_id, email, expiry_time, server_id)) + + results = await asyncio.gather(*tasks) + + await store_key(tg_id, client_id, email, expiry_time, public_link, server_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}") + + 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() + + except Exception as e: + await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}") + + await callback_query.answer() + +async def update_key_on_server(tg_id, client_id, email, expiry_time, server_id): + try: + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + response = await add_client( + session, server_id, client_id, email, tg_id, + limit_ip=1, total_gb=0, expiry_time=expiry_time, + enable=True, flow="xtls-rprx-vision" + ) + + if not response.get("success"): + logger.error(f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}") + else: + logger.info(f"Ключ успешно обновлен на сервере {server_id} для {client_id}") + + except Exception as e: + logger.error(f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}: {e}") + @router.callback_query(lambda c: c.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] + client_id = callback_query.data.split('|')[1] - confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete|{client_id}')], - [types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')] - ]) + 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=[ + [types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete|{client_id}')], + [types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')] + ]) + + await bot.send_message( + chat_id=tg_id, + 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 bot.edit_message_text("Вы уверены, что хотите удалить ключ?", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=confirmation_keyboard, parse_mode="HTML") await callback_query.answer() @router.callback_query(lambda c: c.data.startswith('renew_key|')) async def process_callback_renew_key(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id - client_id = callback_query.data.split('|')[1] + 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 + conn = await asyncpg.connect(DATABASE_URL) try: record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id) @@ -181,17 +337,20 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery): 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.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML") + 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.edit_message_text(f"Ошибка при выборе плана: {e}", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML") + await bot.send_message(chat_id=tg_id, text=f"Ошибка при выборе плана: {e}", parse_mode="HTML") await callback_query.answer() - @router.callback_query(lambda c: c.data.startswith('confirm_delete|')) async def process_callback_confirm_delete(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id @@ -200,27 +359,38 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery): try: conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow('SELECT email, server_id FROM keys WHERE client_id = $1', client_id) + record = await conn.fetchrow('SELECT email FROM keys WHERE client_id = $1', client_id) if record: email = record['email'] - server_id = record['server_id'] - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - success = await delete_client(session, server_id, client_id) + response_message = "Ключ успешно удален." + back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - if success: - await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id) - response_message = "Ключ был успешно удален." - else: - response_message = "Ошибка при удалении клиента через API." + 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) + + async def delete_key_from_servers(): + try: + tasks = [] + for server_id in SERVERS: + tasks.append(delete_key_from_server(server_id, client_id)) + + await asyncio.gather(*tasks) + + except Exception as e: + logger.error(f"Ошибка при удалении ключа {client_id}: {e}") + + asyncio.create_task(delete_key_from_servers()) + + await delete_key_from_db(client_id) else: response_message = "Ключ не найден или уже удален." + back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - 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) + 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() @@ -230,6 +400,30 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery): await callback_query.answer() + +async def delete_key_from_server(server_id, client_id): + """Удаление ключа с сервера""" + try: + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + success = await delete_client(session, server_id, client_id) + + if not success: + logger.error(f"Ошибка удаления ключа {client_id} на сервере {server_id}") + except Exception as e: + logger.error(f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}") + + +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() + + @router.callback_query(lambda c: c.data.startswith('renew_plan|')) async def process_callback_renew_plan(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id @@ -237,15 +431,19 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery): days_to_extend = 30 * int(plan) 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 email, expiry_time, server_id FROM keys WHERE client_id = $1', client_id) + 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'] - server_id = record['server_id'] - current_time = datetime.utcnow().timestamp() * 1000 + 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) @@ -260,144 +458,58 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery): back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile') keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [back_button]]) - await bot.edit_message_text(INSUFFICIENT_FUNDS_MSG, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) + await bot.send_message(tg_id, INSUFFICIENT_FUNDS_MSG, reply_markup=keyboard, parse_mode="HTML") return - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - success = await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time) + 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 server_id in SERVERS: + task = asyncio.create_task( + renew_server_key(server_id, tg_id, client_id, email, new_expiry_time) + ) + tasks.append(task) + + await asyncio.gather(*tasks) - if success: await update_balance(tg_id, -cost) - await conn.execute('UPDATE keys SET expiry_time = $1 WHERE client_id = $2', new_expiry_time, client_id) - 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.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) - else: - await bot.edit_message_text(ERROR_RENEWAL_MSG, chat_id=tg_id, message_id=callback_query.message.message_id) + await update_key_expiry(client_id, new_expiry_time) + + await renew_key_on_servers() + else: - await bot.edit_message_text(KEY_NOT_FOUND_MSG, chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.send_message(tg_id, KEY_NOT_FOUND_MSG, 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) + await bot.send_message(tg_id, f"Ошибка при продлении ключа: {e}", parse_mode="HTML") await callback_query.answer() +async def renew_server_key(server_id, tg_id, client_id, email, new_expiry_time): + try: + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time) + except Exception as e: + logger.error(f"Не удалось продлить ключ {client_id} на сервере {server_id}: {e}") + + 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) - -@router.callback_query(lambda c: c.data.startswith('change_location|')) -async def process_callback_change_location(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id - client_id = callback_query.data.split('|')[1] - - instructions_message = ( - "Перед сменой локации:\n\n" - "1. Пожалуйста, отключите ваш VPN.\n" - "2. Удалите старый ключ, чтобы избежать конфликтов.\n\n" - "Теперь выберите новый сервер для вашего ключа:" - ) - - server_buttons = [] - conn = await asyncpg.connect(DATABASE_URL) try: - for server_id, server in SERVERS.items(): - count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE server_id = $1', server_id) - percent_full = (count / 60) * 100 if count <= 60 else 100 - server_name = f"{server['name']} ({percent_full:.1f}%)" - server_buttons.append([types.InlineKeyboardButton(text=server_name, callback_data=f'select_server&{server_id}&{client_id}')]) - finally: - await conn.close() - - keyboard = types.InlineKeyboardMarkup(inline_keyboard=server_buttons) - - await bot.edit_message_text(instructions_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML") - await callback_query.answer() - -@router.callback_query(lambda c: c.data.startswith('select_server&')) -async def process_callback_select_server(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id - server_id, client_id = callback_query.data.split('&')[1], callback_query.data.split('&')[2] - - try: - conn = await asyncpg.connect(DATABASE_URL) try: - async with conn.transaction(): - record = await conn.fetchrow( - 'SELECT email, expiry_time, server_id FROM keys WHERE client_id = $1 FOR UPDATE', client_id - ) + await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + except Exception: + pass - if record: - email = record['email'] - expiry_time = record['expiry_time'] - current_server_id = record['server_id'] - - if current_server_id == server_id: - await callback_query.answer("Клиент уже на этом сервере.") - return - - 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("Ошибка при создании клиента на новом сервере.") - - new_key = await link_subscription(email, server_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 not success_delete: - raise Exception(f"Ошибка при удалении клиента с сервера {current_server_id}") - - response_message = key_relocated(new_key) - except Exception as e: - response_message = f"Ключ перемещен, но возникла ошибка при удалении клиента с текущего сервера: {e}" - - else: - response_message = "Ключ не найден или уже удален." - - back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') - iphone_button = types.InlineKeyboardButton( - text='🍏IPhone', - url=f'{APP_URL}/?url=v2raytun://import/{new_key}' - ) - android_button = types.InlineKeyboardButton( - text='🤖Android', - url=f'{APP_URL}/?url=v2raytun://import-sub?url={new_key}' - ) - - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [back_button], - [iphone_button, android_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' - ) - - finally: - await conn.close() + await bot.send_message(tg_id, message, parse_mode="HTML") 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' - ) + logger.error(f"Ошибка при обработке ошибки: {e}") - await callback_query.answer() diff --git a/handlers/keys/pic_view.jpg b/handlers/keys/pic_view.jpg new file mode 100644 index 00000000..cced6e03 Binary files /dev/null and b/handlers/keys/pic_view.jpg differ diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py new file mode 100644 index 00000000..78a0c767 --- /dev/null +++ b/handlers/keys/subscriptions.py @@ -0,0 +1,71 @@ +import base64 +import logging + +import aiohttp +from aiohttp import web + +from config import SERVERS + +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + + +async def fetch_url_content(url): + try: + logger.debug(f"Получение URL: {url}") + async with aiohttp.ClientSession() as session: + async with session.get(url, ssl=False) as response: + if response.status == 200: + content = await response.text() + logger.debug(f"Успешно получен контент с {url}") + return base64.b64decode(content).decode('utf-8').split("\n") + else: + logger.error(f"Не удалось получить {url}, статус: {response.status}") + return [] + except Exception as e: + logger.error(f"Ошибка при получении {url}: {e}") + return [] + + +async def combine_unique_lines(urls, query_string): + all_lines = [] + logger.debug(f"Начинаем объединение подписок для запроса: {query_string}") + + urls_with_query = [f"{url}?{query_string}" for url in urls] + logger.debug(f"Составлены URL-адреса: {urls_with_query}") + + for url in urls_with_query: + lines = await fetch_url_content(url) + all_lines.extend(lines) + + + all_lines = list(set(filter(None, all_lines))) + logger.debug(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов") + + return all_lines + +async def handle_subscription(request): + email = request.match_info['email'] + logger.info(f"Получен запрос на подписку для email: {email}") + + urls = [] + for server in SERVERS.values(): + server_subscription_url = f"{server['SUBSCRIPTION']}/{email}" + urls.append(server_subscription_url) + + query_string = request.query_string + logger.debug(f"Извлечен query string: {query_string}") + + combined_subscriptions = await combine_unique_lines(urls, query_string) + + base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode('utf-8')).decode('utf-8') + + headers = { + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Disposition': 'inline', + 'profile-update-interval': '7', + 'profile-title': email, + } + + logger.info(f"Возвращаем объединенные подписки для email: {email}") + return web.Response(text=base64_encoded, headers=headers) \ No newline at end of file diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index c158c84a..c5dad1c7 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -1,51 +1,76 @@ -import asyncpg +import asyncio import uuid -from config import DATABASE_URL, SERVERS, ADMIN_USERNAME, ADMIN_PASSWORD -from auth import login_with_credentials, link_subscription -from client import add_client -from database import store_key, add_connection -from handlers.texts import INSTRUCTIONS from datetime import datetime, timedelta -from handlers.utils import generate_random_email, get_least_loaded_server + +import asyncpg + +from auth import login_with_credentials +from client import add_client +from config import (ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, PUBLIC_LINK, + SERVERS) +from database import store_key +from handlers.texts import INSTRUCTIONS +from handlers.utils import generate_random_email async def create_trial_key(tg_id: int): conn = await asyncpg.connect(DATABASE_URL) try: - server_id = await get_least_loaded_server(conn) - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - current_time = datetime.utcnow() - - expiry_time = current_time + timedelta(days=1, hours=3) - expiry_timestamp = int(expiry_time.timestamp() * 1000) - client_id = str(uuid.uuid4()) email = generate_random_email() - response = await add_client( - session, server_id, client_id, email, tg_id, - limit_ip=1, total_gb=0, expiry_time=expiry_timestamp, - enable=True, flow="xtls-rprx-vision" - ) - if response.get("success"): - # Генерация ссылки подписки - connection_link = await link_subscription(email, server_id) + public_link = f"{PUBLIC_LINK}{email}" + instructions = INSTRUCTIONS + + result = { + 'key': public_link, + 'instructions': instructions + } + + asyncio.create_task(generate_and_store_keys(tg_id, client_id, email, public_link)) + + return result - 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) - - await store_key(tg_id, client_id, email, expiry_timestamp, connection_link, server_id) - - instructions = INSTRUCTIONS - return { - 'key': connection_link, - 'instructions': instructions - } - else: - return {'error': 'Не удалось добавить клиента на панель'} 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=1, hours=3) + expiry_timestamp = int(expiry_time.timestamp() * 1000) + + tasks = [] + for server_id in SERVERS: + task = create_key_on_server(server_id, client_id, email, tg_id, expiry_timestamp) + tasks.append(task) + + results = await asyncio.gather(*tasks) + + if all(result.get("success") for result in results): + await store_key(tg_id, client_id, email, expiry_timestamp, public_link, server_id="all_servers") + + + await conn.execute(''' + INSERT INTO connections (tg_id, trial) + VALUES ($1, 1) + ON CONFLICT (tg_id) + DO UPDATE SET trial = 1 + ''', tg_id) + else: + print('Не удалось создать ключ на одном или нескольких серверах.') + + finally: + await conn.close() + + +async def create_key_on_server(server_id: str, client_id: str, email: str, tg_id: int, expiry_timestamp: int): + """Асинхронно создает ключ на указанном сервере и возвращает результат.""" + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + response = await add_client( + session, server_id, client_id, email, tg_id, + limit_ip=1, total_gb=0, expiry_time=expiry_timestamp, + enable=True, flow="xtls-rprx-vision" + ) + return response diff --git a/handlers/notifications.py b/handlers/notifications.py index 9a5da7e7..8a6a7ba5 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -1,15 +1,17 @@ -from datetime import datetime, timedelta -import asyncpg import asyncio -from aiogram import Bot, Router -from aiogram.fsm.state import State, StatesGroup import logging -from config import DATABASE_URL, ADMIN_USERNAME, ADMIN_PASSWORD, SERVERS -from database import get_balance, update_key_expiry, delete_key, update_balance -from client import extend_client_key, delete_client +from datetime import datetime, timedelta + +import asyncpg +from aiogram import Bot, Router, types +from aiogram.fsm.state import State, StatesGroup + from auth import login_with_credentials -from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, KEY_RENEWAL_FAILED -from aiogram import types +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) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -165,7 +167,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: logger.info(f"Текущее время: {current_time}, Скорректированное текущее время: {adjusted_current_time}") expiring_keys = await conn.fetch(''' - SELECT tg_id, client_id, expiry_time, server_id, email FROM keys + SELECT tg_id, client_id, expiry_time, email FROM keys WHERE expiry_time <= $1 ''', adjusted_current_time) @@ -174,12 +176,9 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: for record in expiring_keys: tg_id = record['tg_id'] client_id = record['client_id'] - balance = await get_balance(tg_id) - server_id = record['server_id'] email = record['email'] + balance = await get_balance(tg_id) - logger.info(f"Проверка баланса для клиента {tg_id}: {balance}.") - expiry_time = record['expiry_time'] expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) current_date = datetime.utcnow() @@ -194,8 +193,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: hours_left = time_left.seconds // 3600 days_left_message = f"Осталось часов: {hours_left}" - message_expired = f"Ваш ключ {email} для сервера {SERVERS[server_id]['name']} истек и был удален!\n\n Перейдите в профиль для создания нового ключа" - + message_expired = f"Ваш ключ {email} истек и был удален!\n\n Перейдите в профиль для создания нового ключа" button_profile = types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile') keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[button_profile]]) @@ -205,28 +203,61 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: 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')}.") - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - success = await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time) - if success: + all_success = True + for server_id in SERVERS: + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + success = await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time) + if not success: + all_success = False + 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} успешно продлен на месяц.") + logger.info(f"Ключ для пользователя {tg_id} успешно продлен на месяц на всех серверах.") except Exception as e: - logger.error(f"Ошибка при отправке уведомления о продлении ключа пользователю {tg_id}: {e}") + if 'blocked' in str(e).lower(): + logger.warning(f"Пользователь {tg_id} заблокирован. Ключ будет удален.") + await delete_key(client_id) + for server_id in SERVERS: + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + success = await delete_client(session, server_id, client_id) + if success: + logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.") + else: + logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.") + else: + logger.error(f"Ошибка при отправке уведомления о продлении ключа пользователю {tg_id}: {e}") else: try: await bot.send_message(tg_id, KEY_RENEWAL_FAILED, reply_markup=keyboard) - logger.error(f"Не удалось продлить ключ для пользователя {tg_id}.") + logger.error(f"Не удалось продлить ключ для пользователя {tg_id} на одном или нескольких серверах.") except Exception as 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) - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - success = await delete_client(session, server_id, client_id) - logger.info(f"Ключ для клиента {tg_id} удален из базы данных.") + + for server_id in SERVERS: + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + success = await delete_client(session, server_id, 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) + if 'blocked' in str(e).lower(): + logger.warning(f"Пользователь {tg_id} заблокирован. Ключ будет удален.") + await delete_key(client_id) + for server_id in SERVERS: + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + success = await delete_client(session, server_id, client_id) + if success: + logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.") + else: + logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.") + else: + logger.error(f"Ошибка при удалении ключа для клиента {tg_id}: {e}") + + await asyncio.sleep(1) \ No newline at end of file diff --git a/handlers/payment/freekassa.py b/handlers/payment/freekassa.py index acf260fa..29295dd8 100644 --- a/handlers/payment/freekassa.py +++ b/handlers/payment/freekassa.py @@ -1,14 +1,15 @@ -import uuid import hashlib -import requests import logging import time +import uuid +import requests from aiogram import Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiohttp import web + from bot import bot from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID from database import update_balance diff --git a/handlers/payment/pay.py b/handlers/payment/pay.py index 3f0c6107..fe47bf09 100644 --- a/handlers/payment/pay.py +++ b/handlers/payment/pay.py @@ -13,7 +13,7 @@ from config import YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID 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 +from handlers.texts import PAYMENT_OPTIONS router = Router() diff --git a/handlers/profile.py b/handlers/profile.py index 6130f275..36799b33 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -1,15 +1,17 @@ +import logging import os from aiogram import Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, BufferedInputFile +from aiogram.types import (BufferedInputFile, InlineKeyboardButton, + InlineKeyboardMarkup) from bot import bot -from database import get_balance, get_key_count, get_referral_stats -from handlers.texts import profile_message_send, invite_message_send, CHANNEL_LINK, get_referral_link from config import PAYMENT_METHOD -import logging +from database import get_balance, get_key_count, get_referral_stats +from handlers.texts import (CHANNEL_LINK, get_referral_link, + invite_message_send, profile_message_send) class ReplenishBalanceState(StatesGroup): diff --git a/handlers/start.py b/handlers/start.py index 2f2e7dd9..a122dbfd 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,15 +1,18 @@ import os + +import asyncpg from aiogram import Router from aiogram.filters import Command from aiogram.fsm.state import State, StatesGroup from aiogram.types import (BufferedInputFile, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message) -from handlers.texts import ABOUT_VPN, WELCOME_TEXT + from bot import bot -from config import CHANNEL_URL, SUPPORT_CHAT_URL, APP_URL -from database import add_connection, add_referral, check_connection_exists, get_trial -from handlers.keys.trial_key import create_trial_key -from handlers.texts import INSTRUCTIONS_TRIAL +from config import APP_URL, CHANNEL_URL, DATABASE_URL, SUPPORT_CHAT_URL +from database import (add_connection, add_referral, check_connection_exists, + get_trial) +from handlers.keys.trial_key import create_trial_key +from handlers.texts import ABOUT_VPN, INSTRUCTIONS_TRIAL, WELCOME_TEXT router = Router() @@ -70,6 +73,19 @@ async def handle_connect_vpn(callback_query: CallbackQuery): if 'error' in trial_key_info: await callback_query.message.answer(trial_key_info['error']) else: + conn = await asyncpg.connect(DATABASE_URL) + try: + result = await conn.execute(''' + UPDATE connections SET trial = 1 WHERE tg_id = $1 + ''', user_id) + print(f"Rows updated: {result}") + + except Exception as e: + print(f"Ошибка при обновлении trial: {e}") + + finally: + await conn.close() + key_message = ( f"Ваш ключ доступа:\n
{trial_key_info['key']}\n\n"
f"Инструкции:\n{INSTRUCTIONS_TRIAL}"
@@ -78,15 +94,25 @@ async def handle_connect_vpn(callback_query: CallbackQuery):
button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
button_iphone = InlineKeyboardButton(
- text='🍏IPhone',
+ text='🍏 Подключить',
url=f'{APP_URL}/?url=v2raytun://import/{trial_key_info["key"]}'
)
button_android = InlineKeyboardButton(
- text='🤖Android',
+ text='🤖 Подключить',
url=f'{APP_URL}/?url=v2raytun://import-sub?url={trial_key_info["key"]}'
)
+ button_download_iphone = InlineKeyboardButton(
+ text='🍏 Скачать',
+ url='https://apps.apple.com/ru/app/v2raytun/id6476628951'
+ )
+ button_download_android = InlineKeyboardButton(
+ text='🤖 Скачать',
+ url='https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru'
+ )
+
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [button_download_iphone, button_download_android],
[button_iphone, button_android],
[button_profile]
])
@@ -102,7 +128,9 @@ async def handle_connect_vpn(callback_query: CallbackQuery):
@router.callback_query(lambda c: c.data == 'about_vpn')
async def handle_about_vpn(callback_query: CallbackQuery):
await callback_query.message.delete()
- info_message = ABOUT_VPN
+
+ bot_version = "3.0.0_beta"
+ info_message = ABOUT_VPN.format(bot_version=bot_version)
button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu')
inline_keyboard_back = InlineKeyboardMarkup(inline_keyboard=[[button_back]])
diff --git a/handlers/utils.py b/handlers/utils.py
index 06b13e24..3fd2506f 100644
--- a/handlers/utils.py
+++ b/handlers/utils.py
@@ -1,6 +1,9 @@
-import re
import random
+import re
+
from config import SERVERS
+
+
def sanitize_key_name(key_name: str) -> str:
return re.sub(r'[^a-z0-9@._-]', '', key_name.lower())