v3. Полноценные подписки

This commit is contained in:
Vladless
2024-11-07 07:53:29 +03:00
parent 7b5cbb8629
commit 9a67bf3976
19 changed files with 661 additions and 374 deletions
+2 -1
View File
@@ -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():
+2 -2
View File
@@ -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)
+8 -4
View File
@@ -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()
+13 -11
View File
@@ -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()
+13 -13
View File
@@ -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()
+6 -5
View File
@@ -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()
+1
View File
@@ -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()
+82 -79
View File
@@ -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="<b>⚙️ Выберите сервер для создания ключа:</b>",
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)
+288 -176
View File
@@ -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 = (
"<b>Это ваши устройства:</b>\n\n"
"<i>Нажмите на имя устройства для управления его ключом.</i>"
"<i>Нажмите на имя устройства для управления его подпиской.</i>"
)
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"Осталось часов: <b>{hours_left}</b>"
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("<b>Информация о ключе не найдена.</b>", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML")
await bot.send_message(chat_id=tg_id, text="<b>Информация о подписке не найдена.</b>", 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,
"<b>Ключ не найден в базе данных.</b>",
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="<b>Вы уверены, что хотите удалить ключ?</b>",
reply_markup=confirmation_keyboard,
parse_mode="HTML"
)
except Exception as e:
await bot.send_message(
chat_id=tg_id,
text=f"<b>Ошибка при удалении ключа:</b> {e}",
parse_mode="HTML"
)
await bot.edit_message_text("<b>Вы уверены, что хотите удалить ключ?</b>", 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 = "<b>Ключ не найден.</b>"
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"<b>Ошибка при выборе плана:</b> {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"<b>Ошибка при выборе плана:</b> {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 = (
"<b>Перед сменой локации:</b>\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()
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+71
View File
@@ -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)
+63 -38
View File
@@ -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
+59 -28
View File
@@ -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"Осталось часов: <b>{hours_left}</b>"
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)
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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()
+6 -4
View File
@@ -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):
+36 -8
View File
@@ -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"<b>Ваш ключ доступа:</b>\n<pre>{trial_key_info['key']}</pre>\n\n"
f"<b>Инструкции:</b>\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]])
+4 -1
View File
@@ -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())
+3 -1
View File
@@ -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)