diff --git a/auth.py b/auth.py index 21e193f2..87a2fce6 100644 --- a/auth.py +++ b/auth.py @@ -1,14 +1,15 @@ import json + import requests -from config import SERVERS # Импортируем SERVERS из config.py +from config import SERVERS session = None def login_with_credentials(server_id: str, username: str, password: str): global session session = requests.Session() - api_url = SERVERS[server_id]['API_URL'] # Получаем API_URL для выбранного сервера + api_url = SERVERS[server_id]['API_URL'] auth_url = f"{api_url}/login/" data = { "username": username, @@ -22,10 +23,10 @@ def login_with_credentials(server_id: str, username: str, password: str): raise Exception(f"Ошибка авторизации: {response.status_code}, {response.text}") def get_clients(session, server_id): - api_url = SERVERS[server_id]['API_URL'] # Получаем GET_INBOUNDS_URL для выбранного сервера + api_url = SERVERS[server_id]['API_URL'] response = session.get(f'{api_url}/panel/api/inbounds/list/') if response.status_code == 200: - return response.json() # Возвращает данные по инбаундам и клиентам + return response.json() else: raise Exception(f"Ошибка при получении клиентов: {response.status_code}, {response.text}") @@ -45,12 +46,10 @@ def link(session, server_id: str, client_id: str, email: str): inbounds = response['obj'][0] settings = json.loads(inbounds['settings']) - # Найти клиентский ID в настройках stream_settings = json.loads(inbounds['streamSettings']) tcp = stream_settings.get('network', 'tcp') reality = stream_settings.get('security', 'reality') flow = stream_settings.get('flow', 'xtls-rprx-vision') - # Создание ссылки для подключения VLESS val = f"vless://{client_id}@{SERVERS[server_id]['DOMEN']}?type={tcp}&security={reality}&pbk={SERVERS[server_id]['PBK']}&fp=chrome&sni={SERVERS[server_id]['SNI']}&sid={SERVERS[server_id]['SID']}=%2F&flow={flow}#{SERVERS[server_id]['PREFIX']}-{email}" return val diff --git a/bot.py b/bot.py index c7a020b7..52abedfe 100644 --- a/bot.py +++ b/bot.py @@ -10,7 +10,6 @@ router = Router() from handlers import key_management, keys, notifications, pay, profile, start -# Регистрация обработчиков dp.include_router(start.router) dp.include_router(profile.router) dp.include_router(keys.router) diff --git a/client.py b/client.py index d847e74c..92a8f82a 100644 --- a/client.py +++ b/client.py @@ -1,8 +1,10 @@ import json -from config import SERVERS # Импортируем SERVERS из config.py + +from config import SERVERS + def add_client(session, server_id: str, client_id: str, email: str, tg_id: str, limit_ip: int, total_gb: int, expiry_time: int, enable: bool, flow: str): - api_url = SERVERS[server_id]['API_URL'] # Получаем API_URL для выбранного сервера + api_url = SERVERS[server_id]['API_URL'] url = f'{api_url}/panel/api/inbounds/addClient' email = email.lower() @@ -44,7 +46,7 @@ def add_client(session, server_id: str, client_id: str, email: str, tg_id: str, print(f"Ошибка при добавлении клиента: {response.status_code}, {response.text}") def extend_client_key(session, server_id: str, tg_id, client_id, email: str, new_expiry_time: int) -> bool: - api_url = SERVERS[server_id]['API_URL'] # Получаем API_URL для выбранного сервера + api_url = SERVERS[server_id]['API_URL'] response = session.get(f"{api_url}/panel/api/inbounds/getClientTraffics/{email}") print(f"GET {response.url} Status: {response.status_code}") print(f"GET Response: {response.text}") @@ -108,7 +110,7 @@ def extend_client_key(session, server_id: str, tg_id, client_id, email: str, new return False def delete_client(session, server_id: str, client_id: str) -> bool: - api_url = SERVERS[server_id]['API_URL'] # Получаем API_URL для выбранного сервера + api_url = SERVERS[server_id]['API_URL'] url = f"{api_url}/panel/api/inbounds/1/delClient/{client_id}" headers = { 'Accept': 'application/json' diff --git a/database.py b/database.py index dca8bd56..603089de 100644 --- a/database.py +++ b/database.py @@ -4,9 +4,10 @@ import asyncpg from config import DATABASE_URL + async def init_db(): conn = await asyncpg.connect(DATABASE_URL) - # Создаем таблицу connections, если она не существует + await conn.execute(''' CREATE TABLE IF NOT EXISTS connections ( tg_id BIGINT PRIMARY KEY NOT NULL, @@ -14,7 +15,7 @@ async def init_db(): trial INTEGER NOT NULL DEFAULT 0 ) ''') - # Создаем таблицу keys, если она не существует + await conn.execute(''' CREATE TABLE IF NOT EXISTS keys ( tg_id BIGINT NOT NULL, @@ -23,21 +24,29 @@ async def init_db(): created_at BIGINT NOT NULL, expiry_time BIGINT NOT NULL, key TEXT NOT NULL, - server_id TEXT NOT NULL DEFAULT 'server1', -- новое поле для идентификатора сервера + server_id TEXT NOT NULL DEFAULT 'server1', -- поле для идентификатора сервера + notified BOOLEAN NOT NULL DEFAULT FALSE, -- новое поле для статуса уведомления PRIMARY KEY (tg_id, client_id) ) ''') - # Добавляем поле server_id в таблицу keys, если его нет + try: await conn.execute(''' ALTER TABLE keys ADD COLUMN server_id TEXT NOT NULL DEFAULT 'server1' ''') except asyncpg.exceptions.DuplicateColumnError: - # Если поле уже существует, ничего не делаем pass - await conn.close() + + try: + await conn.execute(''' + ALTER TABLE keys + ADD COLUMN notified BOOLEAN NOT NULL DEFAULT FALSE + ''') + except asyncpg.exceptions.DuplicateColumnError: + pass + await conn.close() async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0): conn = await asyncpg.connect(DATABASE_URL) diff --git a/handlers/key_management.py b/handlers/key_management.py index 3ce76967..40679b3f 100644 --- a/handlers/key_management.py +++ b/handlers/key_management.py @@ -12,18 +12,15 @@ from aiogram.types import (CallbackQuery, InlineKeyboardButton, from auth import link, login_with_credentials from bot import dp from client import add_client -from config import (ADMIN_PASSWORD, ADMIN_USERNAME, - DATABASE_URL, SERVERS) -from database import (add_connection, get_balance, store_key, - update_balance) +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS +from database import add_connection, get_balance, store_key, update_balance from handlers.instructions import send_instructions +from handlers.notifications import send_message_to_all_clients from handlers.profile import process_callback_view_profile from handlers.start import start_command -from handlers.notifications import send_message_to_all_clients router = Router() -# Удаляем специальные символы из имени ключа def sanitize_key_name(key_name: str) -> str: return re.sub(r'[^a-z0-9@._-]', '', key_name.lower()) @@ -36,14 +33,12 @@ 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 / 100) * 100 # Заполнение в процентах + percent_full = (count / 100) * 100 server_name = f"{server['name']} ({percent_full:.1f}%)" server_buttons.append([InlineKeyboardButton(text=server_name, callback_data=f'select_server|{server_id}')]) finally: @@ -64,7 +59,6 @@ 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) - # Получаем данные о trial из базы данных conn = await asyncpg.connect(DATABASE_URL) try: existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', callback_query.from_user.id) @@ -78,7 +72,7 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext): "⚠️ У вас уже был пробный ключ.\n\n" "Новый ключ будет выдан на один месяц и стоит 100 рублей.\n\n" "Хотите продолжить?", - parse_mode="HTML", # Добавляем параметр parse_mode + parse_mode="HTML", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='✅ Да, создать новый ключ', callback_data='confirm_create_new_key')], [InlineKeyboardButton(text='↩️ Назад', callback_data='cancel_create_key')] @@ -101,7 +95,6 @@ async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContex data = await state.get_data() server_id = data.get('selected_server_id') - # Проверяем баланс перед созданием нового ключа balance = await get_balance(tg_id) if balance < 100: replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile') @@ -125,7 +118,6 @@ async def cancel_create_key(callback_query: CallbackQuery, state: FSMContext): await process_callback_view_profile(callback_query, state) await callback_query.answer() -# Обработка текстовых сообщений @dp.message() async def handle_text(message: Message, state: FSMContext): current_state = await state.get_state() @@ -170,7 +162,6 @@ async def handle_key_name_input(message: Message, state: FSMContext): current_time = datetime.utcnow() expiry_time = None - # Получаем статус пробного ключа из базы данных conn = await asyncpg.connect(DATABASE_URL) try: existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', tg_id) @@ -180,10 +171,8 @@ async def handle_key_name_input(message: Message, state: FSMContext): trial_status = existing_connection['trial'] if existing_connection else 0 if trial_status == 0: - # Создаем пробный ключ на 1 день expiry_time = current_time + timedelta(days=1, hours=3) else: - # Проверяем баланс перед созданием нового ключа balance = await get_balance(tg_id) if balance < 100: replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile') @@ -198,7 +187,6 @@ async def handle_key_name_input(message: Message, state: FSMContext): expiry_timestamp = int(expiry_time.timestamp() * 1000) try: - # Попробуем добавить клиента response = 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): @@ -225,13 +213,11 @@ async def handle_key_name_input(message: Message, state: FSMContext): 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} день" ) diff --git a/handlers/keys.py b/handlers/keys.py index f88191d8..586cdcbe 100644 --- a/handlers/keys.py +++ b/handlers/keys.py @@ -3,15 +3,14 @@ from datetime import datetime, timedelta import asyncpg from aiogram import Router, types -from auth import login_with_credentials, link +from auth import link, login_with_credentials from bot import bot -from client import delete_client, extend_client_key, add_client +from client import add_client, delete_client, extend_client_key from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS from database import get_balance, update_balance router = Router() -# Обработка запроса на просмотр ключей @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 @@ -24,26 +23,19 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): ''', tg_id) if records: - # Создаем кнопки для каждого ключа buttons = [] for record in records: key_name = record['email'] client_id = record['client_id'] - # Заменяем подчеркивание на вертикальную черту в callback_data button = types.InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f'view_key|{key_name}|{client_id}') buttons.append([button]) - # Создаем клавиатуру с кнопками inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) response_message = "Выберите устройство:" - # Редактируем сообщение с клавиатурой await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=inline_keyboard, parse_mode="HTML") else: - # Если нет ключей, добавляем кнопку "Создать ключ" и "Назад" response_message = "У вас нет ключей." - - # Кнопка "Создать ключ" create_key_button = types.InlineKeyboardButton(text='➕ Создать ключ', callback_data='create_key') back_button = types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile') # Измените на правильное значение для кнопки "Назад" @@ -62,7 +54,6 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): @router.callback_query(lambda c: c.data.startswith('view_key|')) async def process_callback_view_key(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id - # Разделяем данные по вертикальной черте key_name, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2] try: @@ -79,7 +70,6 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): expiry_time = record['expiry_time'] server_id = record['server_id'] - # Получаем название сервера по server_id server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер') expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) @@ -99,7 +89,6 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): f"{days_left_message}\n" f"🌍 Сервер: {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}') @@ -108,10 +97,10 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ - [instructions_button], # Инструкции отдельной строкой - [renew_button, delete_button], # Продлить и Удалить в одном ряду - [change_location_button], # Сменить локацию отдельной строкой - [back_button] # Назад отдельной строкой + [instructions_button], + [renew_button, delete_button], + [change_location_button], + [back_button] ] ) @@ -127,12 +116,10 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): await callback_query.answer() - -# Обработка запроса на удаление ключа @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}')], @@ -142,12 +129,10 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery): 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: conn = await asyncpg.connect(DATABASE_URL) @@ -157,9 +142,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery): if record: email = record['email'] expiry_time = record['expiry_time'] - current_time = datetime.utcnow().timestamp() * 1000 # Получаем текущее время в миллисекундах - - # Добавляем новые планы продления (6 месяцев и год) + current_time = datetime.utcnow().timestamp() * 1000 keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text='📅 1 месяц (100 руб.)', callback_data=f'renew_plan|1|{client_id}')], [types.InlineKeyboardButton(text='📅 3 месяца (285 руб.)', callback_data=f'renew_plan|3|{client_id}')], @@ -186,19 +169,16 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery): @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 - client_id = callback_query.data.split('|')[1] # Используем разделитель вертикальная черта + client_id = callback_query.data.split('|')[1] try: conn = await asyncpg.connect(DATABASE_URL) try: - # Извлекаем server_id и email из базы данных record = await conn.fetchrow('SELECT email, server_id FROM keys WHERE client_id = $1', client_id) if record: email = record['email'] - server_id = record['server_id'] # Извлекаем server_id из записи - - # Используем server_id для авторизации + server_id = record['server_id'] session = login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) success = delete_client(session, server_id, client_id) @@ -227,36 +207,33 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery): @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 - plan, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2] # '1', '3', '6' или '12' и client_id - days_to_extend = 30 * int(plan) # Умножаем на количество месяцев (1 месяц = 30 дней) + plan, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2] + days_to_extend = 30 * int(plan) try: conn = await asyncpg.connect(DATABASE_URL) try: - # Извлекаем email, expiry_time и server_id из базы данных record = await conn.fetchrow('SELECT email, expiry_time, server_id FROM keys WHERE client_id = $1', client_id) if record: email = record['email'] expiry_time = record['expiry_time'] - server_id = record['server_id'] # Извлекаем server_id из записи - current_time = datetime.utcnow().timestamp() * 1000 # Текущее время в миллисекундах + server_id = record['server_id'] + 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) else: new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000) - # Стоимость продления в зависимости от выбранного плана if plan == '1': cost = 100 elif plan == '3': cost = 285 elif plan == '6': - cost = 540 # Стоимость для 6 месяцев + cost = 540 elif plan == '12': - cost = 1000 # Стоимость для 12 месяцев + cost = 1000 balance = await get_balance(tg_id) if balance < cost: @@ -267,12 +244,10 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery): await bot.edit_message_text("Недостаточно средств для продления ключа.", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) return - # Продлеваем ключ через API, используя server_id session = login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) success = extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time) 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 = f"Ваш ключ был успешно продлен на {days_to_extend // 30} месяц(-а)." @@ -298,16 +273,13 @@ async def handle_error(tg_id, callback_query, message): @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] # Используем разделитель вертикальная черта - - # Получаем количество подключений для каждого сервера + client_id = callback_query.data.split('|')[1] 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 / 100) * 100 # Заполнение в процентах + percent_full = (count / 100) * 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: @@ -327,32 +299,23 @@ async def process_callback_select_server(callback_query: types.CallbackQuery): try: conn = await asyncpg.connect(DATABASE_URL) try: - # Извлекаем email и expiry_time из базы данных record = await conn.fetchrow('SELECT email, expiry_time, server_id FROM keys WHERE client_id = $1', client_id) if record: email = record['email'] expiry_time = record['expiry_time'] current_server_id = record['server_id'] - - # Создаем сессию для нового сервера session = login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - - # Рассчитываем новое время окончания ключа new_expiry_time = int(datetime.utcnow().timestamp() * 1000) + (expiry_time - datetime.utcnow().timestamp() * 1000) - # Создаем нового клиента на новом сервере new_client_data = add_client(session, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0, expiry_time=new_expiry_time, enable=True, flow="xtls-rprx-vision") if new_client_data: - # Генерируем новый ключ new_key = link(session, server_id, client_id, email) - # Обновляем запись в базе данных, только ключ и сервер await conn.execute('UPDATE keys SET server_id = $1, key = $2 WHERE client_id = $3', server_id, new_key, client_id) - # Удаляем клиента с текущего сервера session = login_with_credentials(current_server_id, ADMIN_USERNAME, ADMIN_PASSWORD) success_delete = delete_client(session, current_server_id, client_id) if success_delete: diff --git a/handlers/notifications.py b/handlers/notifications.py index 4a7d408c..776d4e67 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -2,15 +2,15 @@ from datetime import datetime, timedelta import asyncpg from aiogram import Bot, Router, types -from aiogram.fsm.state import State, StatesGroup -from aiogram.types import ( # Импортируем необходимые классы - InlineKeyboardButton, InlineKeyboardMarkup) from aiogram.filters import Command +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -from bot import bot -from config import DATABASE_URL, ADMIN_PASSWORD, ADMIN_USERNAME, ADMIN_ID -from client import delete_client from auth import login_with_credentials +from bot import bot +from client import delete_client +from config import ADMIN_ID, ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL +from database import get_balance router = Router() @@ -21,40 +21,50 @@ async def notify_expiring_keys(bot: Bot): try: conn = await asyncpg.connect(DATABASE_URL) try: - # Получаем все ключи, которые истекают в течение следующих 10 часов - threshold_time = (datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000 # В миллисекундах + threshold_time = (datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000 records = await conn.fetch(''' SELECT tg_id, email, expiry_time, client_id, server_id FROM keys - WHERE expiry_time <= $1 AND expiry_time > $2 + WHERE expiry_time <= $1 AND expiry_time > $2 AND notified = FALSE ''', threshold_time, datetime.utcnow().timestamp() * 1000) for record in records: tg_id = record['tg_id'] email = record['email'] expiry_time = record['expiry_time'] - server_id = record['server_id'] # Получаем server_id из записи + server_id = record['server_id'] - # Рассчитываем оставшееся время и уменьшаем его на 3 часа time_left = (expiry_time / 1000) - datetime.utcnow().timestamp() hours_left = max(0, int(time_left // 3600) - 3) expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S') + balance = await get_balance(tg_id) - # Создаем клавиатуру с выбором тарифов keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='1 месяц (100 руб.)', callback_data=f'renew_plan|1|{record["client_id"]}')], - [InlineKeyboardButton(text='3 месяца (285 руб.)', callback_data=f'renew_plan|3|{record["client_id"]}')], - [InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance')] + [ + InlineKeyboardButton(text='1 месяц (100 руб.)', callback_data=f'renew_plan|1|{record["client_id"]}'), + InlineKeyboardButton(text='3 месяца (285 руб.)', callback_data=f'renew_plan|3|{record["client_id"]}') + ], + [ + InlineKeyboardButton(text='6 месяцев (540 руб.)', callback_data=f'renew_plan|6|{record["client_id"]}'), + InlineKeyboardButton(text='1 год (1080 руб.)', callback_data=f'renew_plan|12|{record["client_id"]}') + ], + [ + InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance'), + InlineKeyboardButton(text='Назад', callback_data='back_to_main') + ] ]) - message = f"Ваш ключ {email} истечет и будет удален через {hours_left} часов ({expiry_date}). Пожалуйста, продлите его." + message = (f"Ваш ключ {email} истечет и будет удален через {hours_left} часов " + f"({expiry_date}). Пожалуйста, продлите его.\n" + f"Ваш текущий баланс: {balance:.2f} руб.") try: await bot.send_message(chat_id=tg_id, text=message, parse_mode='HTML', reply_markup=keyboard) + + await conn.execute('UPDATE keys SET notified = TRUE WHERE client_id = $1', record['client_id']) except Exception as e: print(f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя.") - # Обрабатываем истекшие ключи expired_records = await conn.fetch(''' SELECT tg_id, email, client_id, server_id FROM keys WHERE expiry_time <= $1 @@ -64,18 +74,13 @@ async def notify_expiring_keys(bot: Bot): tg_id = record['tg_id'] email = record['email'] client_id = record['client_id'] - server_id = record['server_id'] # Получаем server_id из записи + server_id = record['server_id'] - # Удаляем ключ из базы данных await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id) - # Создаем сессию с использованием учетных данных session = login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - - # Удаляем клиента из панели delete_client(session, server_id, client_id) - # Создаем клавиатуру с кнопкой "В профиль" keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='В профиль', callback_data='view_profile')] ]) @@ -95,12 +100,10 @@ async def notify_expiring_keys(bot: Bot): @router.message(Command('send_to_all')) async def send_message_to_all_clients(message: types.Message): - # Проверяем, является ли отправитель администратором - if message.from_user.id != ADMIN_ID: # Замените ADMIN_ID на ID вашего администратора + if message.from_user.id != ADMIN_ID: await message.answer("У вас нет прав для выполнения этой команды.") return - # Получаем текст сообщения text = message.get_args() if not text: await message.answer("Пожалуйста, введите текст сообщения после команды.") @@ -108,7 +111,6 @@ async def send_message_to_all_clients(message: types.Message): try: conn = await asyncpg.connect(DATABASE_URL) - # Получаем все tg_id клиентов tg_ids = await conn.fetch('SELECT tg_id FROM keys') for record in tg_ids: @@ -123,4 +125,4 @@ async def send_message_to_all_clients(message: types.Message): print(f"Ошибка при подключении к базе данных: {e}") await message.answer("Произошла ошибка при отправке сообщения.") finally: - await conn.close() \ No newline at end of file + await conn.close() diff --git a/handlers/pay.py b/handlers/pay.py index 4fa376ea..5f4900ae 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -6,7 +6,7 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiohttp import web -from yookassa import Configuration, Payment # Импортируем ЮKассу +from yookassa import Configuration, Payment from bot import bot from config import YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID @@ -18,7 +18,6 @@ router = Router() logging.basicConfig(level=logging.DEBUG) -# Настройка конфигурации ЮKассы Configuration.account_id = YOOKASSA_SHOP_ID Configuration.secret_key = YOOKASSA_SECRET_KEY @@ -51,17 +50,13 @@ async def send_message_with_deletion(chat_id, text, reply_markup=None, state=Non async def process_callback_replenish_balance(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id - # Проверяем, есть ли у пользователя ключи key_count = await get_key_count(tg_id) - # Если ключей нет, проверяем, существует ли запись с таким tg_id if key_count == 0: exists = await check_connection_exists(tg_id) - # Если записи нет, создаем нового клиента в базе данных if not exists: await add_connection(tg_id, balance=0.0, trial=0) - # Переходим сразу к выбору суммы пополнения amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='100 RUB', callback_data='amount_100'), InlineKeyboardButton(text='300 RUB', callback_data='amount_300')], [InlineKeyboardButton(text='600 RUB', callback_data='amount_600'), InlineKeyboardButton(text='1000 RUB', callback_data='amount_1000')], @@ -101,7 +96,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F customer_name = callback_query.from_user.full_name customer_id = callback_query.from_user.id - # Создаем платеж с чеком для самозанятых payment = Payment.create({ "amount": { "value": str(amount), @@ -109,7 +103,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F }, "confirmation": { "type": "redirect", - "return_url": "https://pocomacho.ru/" # Укажите реальный URL для возврата + "return_url": "https://pocomacho.ru/" }, "capture": True, "description": "Пополнение баланса", diff --git a/handlers/profile.py b/handlers/profile.py index e3643d2b..5641c6ea 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -3,7 +3,7 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -from bot import bot # Импортируем объект bot +from bot import bot from database import get_balance, get_key_count router = Router() @@ -14,25 +14,21 @@ class ReplenishBalanceState(StatesGroup): async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id - username = callback_query.from_user.full_name # Получаем имя пользователя + username = callback_query.from_user.full_name try: - # Получаем количество ключей key_count = await get_key_count(tg_id) - - # Получаем баланс balance = await get_balance(tg_id) if balance is None: - balance = 0 # По умолчанию 0, если баланс неизвестен + balance = 0 profile_message = ( f"Профиль: {username}\n\n" f"🔹 ID: {tg_id}\n" f"🔹 Баланс: {balance} RUB\n" - f"🔹 К-во устройств: {key_count}\n" # Изменили текст на "К-во устройств" + f"🔹 К-во устройств: {key_count}\n" ) - # Кнопки для действий в профиле с добавленными смайликами button_create_key = InlineKeyboardButton(text='➕ Устройство', callback_data='create_key') button_view_keys = InlineKeyboardButton(text='📱 Мои устройства', callback_data='view_keys') button_replenish_balance = InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance') @@ -49,12 +45,10 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta profile_message = f"❗️ Ошибка при получении данных профиля: {e}" keyboard = None - # Удаляем текущее сообщение await callback_query.message.delete() - # Отправляем новое сообщение с профилем через bot await bot.send_message( - chat_id=tg_id, # ID чата, куда отправляем сообщение + chat_id=tg_id, text=profile_message, parse_mode='HTML', reply_markup=keyboard diff --git a/handlers/start.py b/handlers/start.py index c1a1dad5..87f002e4 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -7,8 +7,7 @@ from aiogram.types import (BufferedInputFile, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message) from bot import bot -from config import (CHANNEL_URL, - SUPPORT_CHAT_URL) +from config import CHANNEL_URL, SUPPORT_CHAT_URL router = Router() @@ -16,22 +15,18 @@ class FeedbackState(StatesGroup): waiting_for_feedback = State() async def send_welcome_message(chat_id: int): - # Новый текст приветствия welcome_text = ( "*SoloNet — ваш доступ в свободный интернет! 🌐✨*\n\n" "Используйте надежный и быстрый VPN, который гарантирует вашу безопасность даже в самых строгих условиях. 🔒🚀\n\n" "*ver. 1.0*" ) - # Путь к изображению image_path = os.path.join(os.path.dirname(__file__), 'pic.jpg') - # Проверка существования файла if not os.path.isfile(image_path): await bot.send_message(chat_id, "Файл изображения не найден.") return - # Создаем inline-клавиатуру inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')], [InlineKeyboardButton(text='🔒 О VPN', callback_data='about_vpn')], @@ -39,14 +34,13 @@ async def send_welcome_message(chat_id: int): [InlineKeyboardButton(text='📢 Наш канал', url=CHANNEL_URL)] ]) - # Отправляем изображение с инлайн-клавиатурой with open(image_path, 'rb') as image_from_buffer: await bot.send_photo( chat_id, BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"), caption=welcome_text, parse_mode='Markdown', - reply_markup=inline_keyboard # Inline-клавиатура + reply_markup=inline_keyboard ) @router.message(Command('start')) @@ -55,9 +49,8 @@ async def start_command(message: Message): @router.callback_query(lambda c: c.data == 'about_vpn') async def handle_about_vpn(callback_query: CallbackQuery): - # Удаляем сообщение главного меню - await callback_query.message.delete() + await callback_query.message.delete() info_message = ( "*О VPN*\n\n" "Мы используем высокоскоростные серверы в разных локациях и выдаём ключ каждому индивидуально. " @@ -78,9 +71,7 @@ async def handle_about_vpn(callback_query: CallbackQuery): @router.callback_query(lambda c: c.data == 'back_to_menu') async def handle_back_to_menu(callback_query: CallbackQuery): - # Удаляем текущее сообщение + await callback_query.message.delete() - - # Отправляем приветственное сообщение await send_welcome_message(callback_query.from_user.id) await callback_query.answer()