From 9a67bf397632748818a626b27ab5cf080bb33ff3 Mon Sep 17 00:00:00 2001 From: Vladless Date: Thu, 7 Nov 2024 07:53:29 +0300 Subject: [PATCH] =?UTF-8?q?v3.=20=D0=9F=D0=BE=D0=BB=D0=BD=D0=BE=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backup.py | 3 +- bot.py | 4 +- handlers/admin/admin.py | 12 +- handlers/admin/admin_panel.py | 24 +- handlers/admin/user_editor.py | 26 +- handlers/commands.py | 11 +- handlers/instructions/instructions.py | 1 + handlers/keys/key_management.py | 161 ++++----- handlers/keys/keys.py | 464 ++++++++++++++++---------- handlers/keys/pic_view.jpg | Bin 0 -> 33288 bytes handlers/keys/subscriptions.py | 71 ++++ handlers/keys/trial_key.py | 101 +++--- handlers/notifications.py | 87 +++-- handlers/payment/freekassa.py | 5 +- handlers/payment/pay.py | 2 +- handlers/profile.py | 10 +- handlers/start.py | 44 ++- handlers/utils.py | 5 +- main.py | 4 +- 19 files changed, 661 insertions(+), 374 deletions(-) create mode 100644 handlers/keys/pic_view.jpg create mode 100644 handlers/keys/subscriptions.py 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 0000000000000000000000000000000000000000..cced6e0380b744c87689c87bec6c42e15a0a9625 GIT binary patch literal 33288 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" - 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()) diff --git a/main.py b/main.py index b5310b33..36225298 100644 --- a/main.py +++ b/main.py @@ -10,9 +10,10 @@ from backup import backup_database from bot import bot, dp, router from config import WEBAPP_HOST, WEBAPP_PORT, WEBHOOK_PATH, WEBHOOK_URL from database import init_db +from handlers.keys.subscriptions import handle_subscription from handlers.notifications import notify_expiring_keys -from handlers.payment.pay import payment_webhook from handlers.payment.freekassa import freekassa_webhook +from handlers.payment.pay import payment_webhook logging.basicConfig(level=logging.DEBUG) @@ -54,6 +55,7 @@ async def main(): app.on_shutdown.append(on_shutdown) app.router.add_post('/yookassa/webhook', payment_webhook) app.router.add_post('/freekassa/webhook', freekassa_webhook) + app.router.add_get('/solonet/sub/{email}', handle_subscription) SimpleRequestHandler(dispatcher=dp, bot=bot).register(app, path=WEBHOOK_PATH) setup_application(app, dp, bot=bot)