v3. Полноценные подписки
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user