Merge pull request #6 from hteppl/main

Changes from upstream
This commit is contained in:
Oleg Kraev
2025-01-15 22:05:55 +03:00
committed by GitHub
32 changed files with 5502 additions and 3606 deletions
+1 -1
View File
@@ -3,8 +3,8 @@ import subprocess
from datetime import datetime
from aiogram.types import BufferedInputFile
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER
from logger import logger
+3 -5
View File
@@ -5,14 +5,13 @@ from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import ErrorEvent
from config import API_TOKEN
from config import API_TOKEN
from logger import logger
from middlewares.admin import AdminMiddleware
from middlewares.database import DatabaseMiddleware
from middlewares.delete import DeleteMessageMiddleware
from middlewares.logging import LoggingMiddleware
from middlewares.throttling import ThrottlingMiddleware
from middlewares.user import UserMiddleware
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
@@ -30,9 +29,8 @@ dp.callback_query.middleware(UserMiddleware())
dp.message.middleware(DatabaseMiddleware())
dp.callback_query.middleware(DatabaseMiddleware())
# Add throttling middleware
dp.message.middleware(ThrottlingMiddleware(limit=1)) # 1 message per second
dp.callback_query.middleware(ThrottlingMiddleware(limit=1))
# dp.message.middleware(ThrottlingMiddleware(limit=1))
# dp.callback_query.middleware(ThrottlingMiddleware(limit=1))
dp.message.outer_middleware(DeleteMessageMiddleware())
dp.callback_query.outer_middleware(DeleteMessageMiddleware())
+10 -4
View File
@@ -1,6 +1,6 @@
import py3xui
from config import LIMIT_IP
from config import LIMIT_IP, SUPERNODE
from logger import logger
@@ -15,6 +15,7 @@ async def add_client(
enable: bool,
flow: str,
inbound_id: int,
sub_id
):
"""
Adds a client to the server via 3x-ui.
@@ -30,7 +31,7 @@ async def add_client(
expiry_time=expiry_time,
enable=enable,
tg_id=tg_id,
sub_id=email,
sub_id=sub_id,
flow=flow,
)
@@ -54,7 +55,7 @@ async def add_client(
async def extend_client_key(
xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int
xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id = str
):
"""
Функция для обновления срока действия ключа клиента по email.
@@ -78,7 +79,7 @@ async def extend_client_key(
client.id = client_id
client.expiry_time = new_expiry_time
client.flow = "xtls-rprx-vision"
client.sub_id = email
client.sub_id = sub_id
client.total_gb = total_gb
client.enable = True
client.limit_ip = LIMIT_IP
@@ -106,6 +107,11 @@ async def delete_client(
"""
await xui.login()
try:
if SUPERNODE:
await xui.client.delete(inbound_id, client_id)
logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE).")
return True
client = await xui.client.get_by_email(email)
if not client:
+1 -1
View File
@@ -3,8 +3,8 @@ from datetime import datetime
from typing import Any
import asyncpg
from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from logger import logger
+1
View File
@@ -1,5 +1,6 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
+770
View File
@@ -0,0 +1,770 @@
import asyncio
from datetime import datetime
from typing import Any
from aiogram import Bot, F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import TOTAL_GB
from database import (
delete_user_data,
get_client_id_by_email,
get_servers_from_db,
restore_trial,
update_key_expiry,
)
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
delete_key_from_cluster,
delete_key_from_db,
renew_key_in_cluster,
)
from handlers.utils import sanitize_key_name
from logger import logger
router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
waiting_for_username = State()
displaying_user_info = State()
waiting_for_new_balance = State()
waiting_for_key_name = State()
waiting_for_expiry_time = State()
waiting_for_message_text = State()
@router.callback_query(F.data == "search_by_tg_id", IsAdminFilter())
async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await callback_query.message.answer(
"🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup()
)
await state.set_state(UserEditorState.waiting_for_tg_id)
@router.callback_query(F.data == "search_by_username", IsAdminFilter())
async def prompt_username(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await callback_query.message.answer(
"🔍 Введите Username клиента:", reply_markup=builder.as_markup()
)
await state.set_state(UserEditorState.waiting_for_username)
@router.message(UserEditorState.waiting_for_username, IsAdminFilter())
async def handle_username_input(
message: types.Message, state: FSMContext, session: Any
):
username = message.text.strip().lstrip("@").replace("https://t.me/", "")
user_record = await session.fetchrow(
"SELECT tg_id FROM users WHERE username = $1", username
)
if not user_record:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"🔍 Пользователь с указанным username не найден. 🚫",
reply_markup=builder.as_markup(),
)
await state.clear()
return
tg_id = user_record["tg_id"]
username = await session.fetchval(
"SELECT username FROM users WHERE tg_id = $1", tg_id
)
balance = await session.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await session.fetchval(
"SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
)
if balance is None:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"🚫 Пользователь с указанным tg_id не найден. 🔍",
reply_markup=builder.as_markup(),
)
await state.clear()
return
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(
InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")
)
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
callback_data=f"restore_trial_{tg_id}",
)
)
builder.row(
InlineKeyboardButton(
text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
@router.callback_query(F.data.startswith("send_message_"))
async def handle_send_message(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.data.split("_")[2]
await state.update_data(target_tg_id=tg_id)
await callback_query.message.answer(
"✉️ Введите текст сообщения, которое вы хотите отправить пользователю."
)
await state.set_state(UserEditorState.waiting_for_message_text)
@router.message(UserEditorState.waiting_for_message_text, IsAdminFilter())
async def process_send_message(message: types.Message, state: FSMContext, bot: Bot):
data = await state.get_data()
target_tg_id = data.get("target_tg_id")
if not target_tg_id:
await message.answer("🚫 Ошибка: ID пользователя не найден.")
await state.clear()
return
try:
await bot.send_message(chat_id=target_tg_id, text=message.text)
await message.answer("✅ Сообщение успешно отправлено.")
except Exception as e:
await message.answer(f"❌ Не удалось отправить сообщение: {e}")
await state.clear()
@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter())
async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any):
tg_id = int(message.text)
username = await session.fetchval(
"SELECT username FROM users WHERE tg_id = $1", tg_id
)
balance = await session.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await session.fetchval(
"SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
)
if balance is None:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"❌ Пользователь с указанным tg_id не найден. 🔍",
reply_markup=builder.as_markup(),
)
await state.clear()
return
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(
InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")
)
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
)
builder.row(
InlineKeyboardButton(
text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
callback_data=f"restore_trial_{tg_id}",
)
)
builder.row(
InlineKeyboardButton(
text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
@router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter())
async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any):
tg_id = int(callback_query.data.split("_")[2])
await restore_trial(tg_id, session)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в меню администратора", callback_data="admin"
)
)
await callback_query.message.answer(
"✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()
)
@router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter())
async def process_balance_change(callback_query: CallbackQuery, state: FSMContext):
tg_id = int(callback_query.data.split("_")[2])
await state.update_data(tg_id=tg_id)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await callback_query.message.answer(
"💸 Введите новую сумму баланса:", reply_markup=builder.as_markup()
)
await state.set_state(UserEditorState.waiting_for_new_balance)
@router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter())
async def handle_new_balance_input(
message: types.Message, state: FSMContext, session: Any
):
if not message.text.isdigit() or int(message.text) < 0:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"❌ Пожалуйста, введите корректную сумму для изменения баланса.",
reply_markup=builder.as_markup(),
)
return
new_balance = int(message.text)
user_data = await state.get_data()
tg_id = user_data.get("tg_id")
await session.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2",
new_balance,
tg_id,
)
response_message = f"✅ Баланс успешно изменен на <b>{new_balance}</b>."
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в меню администратора",
callback_data="admin",
)
)
await message.answer(response_message, reply_markup=builder.as_markup())
await state.clear()
async def get_key_details(email, session):
record = await session.fetchrow(
"""
SELECT k.key, k.expiry_time, k.server_id, c.tg_id, c.balance
FROM keys k
JOIN connections c ON k.tg_id = c.tg_id
WHERE k.email = $1
""",
email,
)
if not record:
return None
servers = await get_servers_from_db()
cluster_name = "Неизвестный кластер"
for cluster_name, cluster_servers in servers.items():
if any(
server["inbound_id"] == record["server_id"] for server in cluster_servers
):
cluster_name = cluster_name
break
expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "<b>Ключ истек.</b>"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
return {
"key": record["key"],
"expiry_date": expiry_date.strftime("%d %B %Y года"),
"days_left_message": days_left_message,
"server_name": cluster_name,
"balance": record["balance"],
"tg_id": record["tg_id"],
}
@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter())
async def process_key_edit(callback_query: CallbackQuery, session: Any):
email = callback_query.data.split("_", 2)[2]
key_details = await get_key_details(email, session)
if not key_details:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await callback_query.message.answer(
"🔍 <b>Информация о ключе не найдена.</b> 🚫",
reply_markup=builder.as_markup(),
)
return
response_message = (
f"🔑 Ключ: <code>{key_details['key']}</code>\n"
f"⏰ Дата истечения: <b>{key_details['expiry_date']}</b>\n"
f"💰 Баланс пользователя: <b>{key_details['balance']}</b>\n"
f"🌐 Кластер: <b>{key_details['server_name']}</b>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="ℹ️ Получить информацию о юзере",
callback_data=f"user_info|{key_details['tg_id']}",
)
)
builder.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
)
)
builder.row(
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{email}",
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await callback_query.message.answer(
response_message, reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "search_by_key_name", IsAdminFilter())
async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await callback_query.message.answer(
"🔑 Введите имя ключа:", reply_markup=builder.as_markup()
)
await state.set_state(UserEditorState.waiting_for_key_name)
@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
async def handle_key_name_input(
message: types.Message, state: FSMContext, session: Any
):
key_name = sanitize_key_name(message.text)
key_details = await get_key_details(key_name, session)
if not key_details:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"🚫 Пользователь с указанным именем ключа не найден.",
reply_markup=builder.as_markup(),
)
await state.clear()
return
response_message = (
f"🔑 Ключ: <code>{key_details['key']}</code>\n"
f"⏰ Дата истечения: <b>{key_details['expiry_date']}</b>\n"
f"💰 Баланс пользователя: <b>{key_details['balance']}</b>\n"
f"🌐 Сервер: <b>{key_details['server_name']}</b>"
)
key_buttons = InlineKeyboardBuilder()
key_buttons.row(
InlineKeyboardButton(
text="ℹ️ Получить информацию о юзере",
callback_data=f"user_info|{key_details['tg_id']}",
)
)
key_buttons.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{key_name}",
)
)
key_buttons.row(
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{key_name}",
)
)
key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(response_message, reply_markup=key_buttons.as_markup())
await state.clear()
@router.callback_query(F.data.startswith("change_expiry|"), IsAdminFilter())
async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext):
email = callback_query.data.split("|")[1]
await callback_query.message.answer(
f"⏳ Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:"
)
await state.update_data(email=email)
await state.set_state(UserEditorState.waiting_for_expiry_time)
@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
async def handle_expiry_time_input(
message: types.Message, state: FSMContext, session: Any
):
user_data = await state.get_data()
email = user_data.get("email")
if not email:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup()
)
await state.clear()
return
try:
expiry_time_str = message.text
expiry_time = int(
datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000
)
client_id = await get_client_id_by_email(email)
if client_id is None:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")
)
await message.answer(
f"🚫 Клиент с email {email} не найден. 🔍",
reply_markup=builder.as_markup(),
)
await state.clear()
return
record = await session.fetchrow(
"SELECT server_id FROM keys WHERE client_id = $1", client_id
)
if not record:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")
)
await message.answer(
"🚫 Клиент не найден в базе данных. 🔍",
reply_markup=builder.as_markup(),
)
await state.clear()
return
clusters = await get_servers_from_db()
async def update_key_on_all_servers():
tasks = []
for cluster_name, cluster_servers in clusters.items():
for server in cluster_servers:
tasks.append(
asyncio.create_task(
renew_key_in_cluster(
cluster_name,
email,
client_id,
expiry_time,
total_gb=TOTAL_GB,
)
)
)
await asyncio.gather(*tasks)
await update_key_on_all_servers()
await update_key_expiry(client_id, expiry_time)
response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await message.answer(response_message, reply_markup=builder.as_markup())
except ValueError:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(e)
await state.clear()
@router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter())
async def process_callback_delete_key(
callback_query: types.CallbackQuery, session: Any
):
email = callback_query.data.split("|")[1]
client_id = await session.fetchval(
"SELECT client_id FROM keys WHERE email = $1", email
)
if client_id is None:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await callback_query.message.answer(
"🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup()
)
return
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"confirm_delete_admin|{client_id}",
)
)
builder.row(
types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor")
)
await callback_query.message.answer(
"<b>❓ Вы уверены, что хотите удалить ключ?</b>",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter())
async def process_callback_confirm_delete(
callback_query: types.CallbackQuery, session: Any
):
client_id = callback_query.data.split("|")[1]
record = await session.fetchrow(
"SELECT email FROM keys WHERE client_id = $1", client_id
)
if record:
email = record["email"]
response_message = "✅ Ключ успешно удален."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
clusters = await get_servers_from_db()
async def delete_key_from_servers(email, client_id):
tasks = []
for cluster_name, cluster_servers in clusters.items():
for server in cluster_servers:
tasks.append(
delete_key_from_cluster(cluster_name, email, client_id)
)
await asyncio.gather(*tasks)
await delete_key_from_servers(email, client_id)
await delete_key_from_db(client_id, session)
await callback_query.message.answer(
response_message, reply_markup=builder.as_markup()
)
else:
response_message = "🚫 Ключ не найден или уже удален."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
await callback_query.message.answer(
response_message, reply_markup=builder.as_markup()
)
@router.callback_query(F.data.startswith("user_info|"), IsAdminFilter())
async def handle_user_info(
callback_query: types.CallbackQuery, state: FSMContext, session: Any
):
tg_id = int(callback_query.data.split("|")[1])
username = await session.fetchval(
"SELECT username FROM users WHERE tg_id = $1", tg_id
)
balance = await session.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await session.fetchval(
"SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
)
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(
InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")
)
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await callback_query.message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
@router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter())
async def confirm_delete_user(
callback_query: types.CallbackQuery, state: FSMContext, session: Any
):
tg_id = int(callback_query.data.split("_")[3])
confirmation_markup = InlineKeyboardMarkup(
row_width=2,
inline_keyboard=[
[
InlineKeyboardButton(
text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}"
)
],
[InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")],
],
)
await callback_query.message.answer(
f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
reply_markup=confirmation_markup,
)
@router.callback_query(F.data.startswith("delete_user_"), IsAdminFilter())
async def delete_user(callback_query: types.CallbackQuery, session: Any):
tg_id = int(callback_query.data.split("_")[2])
key_records = await session.fetch(
"SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id
)
async def delete_keys_from_servers():
try:
tasks = []
for email, client_id in key_records:
servers = await get_servers_from_db()
for cluster_id, cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
except Exception as e:
logger.error(
f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}"
)
await delete_keys_from_servers()
try:
await delete_user_data(session, tg_id)
back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await callback_query.message.answer(
f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard
)
except Exception as e:
logger.error(
f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}"
)
await callback_query.message.answer(
f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
)
+4 -4
View File
@@ -1,6 +1,6 @@
DOWNLOAD_IOS_BUTTON = "🍏 Скачать для iOS"
DOWNLOAD_ANDROID_BUTTON = "🤖 Скачать для Android"
IMPORT_IOS = "🍏 Подключить на iOS"
IMPORT_ANDROID = "🤖 Подключить на Android"
DOWNLOAD_IOS_BUTTON = "🍏 Скачать iOS"
DOWNLOAD_ANDROID_BUTTON = "🤖 Скачать Android"
IMPORT_IOS = "🍏 Подключить"
IMPORT_ANDROID = "🤖 Подключить"
PC_BUTTON = "💻 Компьютеры"
TV_BUTTON = "📺 Андроид TV"
+2
View File
@@ -1,5 +1,7 @@
ADD_SUB = " Подписка"
MY_SUBS = "📱 Мои подписки"
BALANCE = "💰 Баланс"
BALANCE_HISTORY = "📊 История пополнения"
PAYMENT = "💳 Пополнить баланс"
INVITE = "👥 Пригласить"
GIFTS = "🎁 Подарить"
+5
View File
@@ -1,3 +1,8 @@
PAY = "Пополнить"
PAY_2 = "Оплатить"
BACK = "⬅️ Назад"
CUSTOM_SUM = "💰 Ввести свою сумму"
PROFILE = "👤 Личный кабинет"
KEY_CREATION_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
KEY_RENEWAL_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
DEFAULT_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей. Перейдите по ссылке для оплаты:"
+1 -1
View File
@@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RUB_TO_XTR
from config import RUB_TO_XTR
from logger import logger
+1 -1
View File
@@ -5,8 +5,8 @@ import asyncpg
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
from handlers.texts import (
CONNECT_TV_TEXT,
INSTRUCTION_PC,
+19 -24
View File
@@ -3,11 +3,14 @@ import uuid
from datetime import datetime, timedelta
from typing import Any
import pytz
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
@@ -19,8 +22,6 @@ from config import (
TRIAL_TIME,
USE_NEW_PAYMENT_FLOW,
)
from bot import bot
from database import (
get_balance,
get_trial,
@@ -37,6 +38,7 @@ from handlers.buttons.add_subscribe import (
TV_BUTTON,
)
from handlers.keys.key_utils import create_key_on_cluster
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import DISCOUNTS, key_message_success
from handlers.utils import generate_random_email, get_least_loaded_cluster
@@ -145,8 +147,10 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
if balance < plan_price:
required_amount = plan_price - balance
if USE_NEW_PAYMENT_FLOW:
if USE_NEW_PAYMENT_FLOW == "YOOKASSA":
await process_custom_amount_input(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "ROBOKASSA":
await handle_custom_amount_input(callback_query, session)
else:
builder = InlineKeyboardBuilder()
builder.row(
@@ -175,6 +179,9 @@ async def create_key(
message_or_query: Message | CallbackQuery | None = None,
):
"""Создаёт ключ с заданным сроком действия."""
moscow_tz = pytz.timezone("Europe/Moscow")
expiry_time = expiry_time.astimezone(moscow_tz)
while True:
key_name = generate_random_email()
logger.info(f"Generated random key name for user {tg_id}: {key_name}")
@@ -192,7 +199,7 @@ async def create_key(
client_id = str(uuid.uuid4())
email = key_name.lower()
expiry_timestamp = int(expiry_time.timestamp() * 1000)
expiry_timestamp = int(expiry_time.astimezone(moscow_tz).timestamp() * 1000)
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
try:
@@ -226,19 +233,13 @@ async def create_key(
except Exception as e:
logger.error(f"Error while creating the key for user {tg_id} on cluster: {e}")
error_message = "❌ Произошла ошибка при создании подписки. Пожалуйста, попробуйте снова."
if isinstance(message_or_query, Message):
await message_or_query.answer(
"❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова."
)
await message_or_query.answer(error_message)
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(
"❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова."
)
await message_or_query.message.answer(error_message)
else:
await bot.send_message(
chat_id=tg_id,
text="❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова.",
)
await bot.send_message(chat_id=tg_id, text=error_message)
return
builder = InlineKeyboardBuilder()
@@ -249,9 +250,7 @@ async def create_key(
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{public_link}"),
InlineKeyboardButton(
text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}"
),
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}"),
)
builder.row(
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
@@ -259,20 +258,16 @@ async def create_key(
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
remaining_time = expiry_time - datetime.utcnow()
remaining_time = expiry_time - datetime.now(moscow_tz)
days = remaining_time.days
key_message = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
if isinstance(message_or_query, Message):
await message_or_query.answer(key_message, reply_markup=builder.as_markup())
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(
key_message, reply_markup=builder.as_markup()
)
await message_or_query.message.answer(key_message, reply_markup=builder.as_markup())
else:
await bot.send_message(
chat_id=tg_id, text=key_message, reply_markup=builder.as_markup()
)
await bot.send_message(chat_id=tg_id, text=key_message, reply_markup=builder.as_markup())
if state:
await state.clear()
+51 -28
View File
@@ -1,51 +1,54 @@
import asyncio
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, TOTAL_GB
from py3xui import AsyncApi
from client import add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, SUPERNODE, TOTAL_GB
from database import get_servers_from_db
from logger import logger
async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_timestamp):
"""
Создает ключ на всех серверах указанного кластера.
:param cluster_id: ID кластера.
:param tg_id: Telegram ID пользователя.
:param client_id: Уникальный идентификатор клиента.
:param email: Email клиента.
:param expiry_timestamp: Время истечения ключа (timestamp в миллисекундах).
:param allow_existing: Игнорируется, ключи всегда продолжают выполнение.
Создает ключ на всех серверах указанного кластера с одинаковым sub_id и уникальным email при активном SUPERNODE.
"""
try:
tasks = []
servers = await get_servers_from_db()
cluster = servers.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
for server_info in cluster:
xui = AsyncApi(
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
semaphore = asyncio.Semaphore(2)
inbound_id = server_info.get("inbound_id")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_info.get('server_name', 'unknown')}. Пропуск."
async def create_client_on_server(server_info):
async with semaphore:
xui = AsyncApi(
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
continue
tasks.append(
add_client(
inbound_id = server_info.get("inbound_id")
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
return
if SUPERNODE:
unique_email = f"{email}_{server_name.lower()}"
sub_id = email
else:
unique_email = email
sub_id = unique_email
await add_client(
xui,
client_id,
email,
unique_email,
tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
@@ -53,16 +56,25 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=sub_id
)
)
await asyncio.gather(*tasks)
if SUPERNODE:
await asyncio.sleep(0.7)
if SUPERNODE:
for server_info in cluster:
await create_client_on_server(server_info)
else:
await asyncio.gather(*(create_client_on_server(server) for server in cluster))
except Exception as e:
logger.error(f"Ошибка при создании ключа: {e}")
raise e
async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb):
try:
servers = await get_servers_from_db()
@@ -80,20 +92,30 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
)
inbound_id = server_info.get("inbound_id")
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_info.get('server_name', 'unknown')}. Пропуск."
f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
continue
if SUPERNODE:
unique_email = f"{email}_{server_name.lower()}"
sub_id = email
else:
unique_email = email
sub_id = unique_email
tasks.append(
extend_client_key(
xui,
int(inbound_id),
email,
unique_email,
new_expiry_time,
client_id,
total_gb,
sub_id
)
)
@@ -190,6 +212,7 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=email
)
)
+105 -72
View File
@@ -7,21 +7,25 @@ from typing import Any
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
ENABLE_DELETE_KEY_BUTTON,
ENABLE_UPDATE_SUBSCRIPTION_BUTTON,
PUBLIC_LINK,
RENEWAL_PLANS,
TOTAL_GB,
USE_NEW_PAYMENT_FLOW,
)
from database import (
delete_key,
get_balance,
get_servers_from_db,
save_temporary_data,
store_key,
update_balance,
update_key_expiry,
@@ -40,9 +44,10 @@ from handlers.keys.key_utils import (
renew_key_in_cluster,
update_key_on_cluster,
)
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import (
DISCOUNTS,
INSUFFICIENT_FUNDS_MSG,
KEY_NOT_FOUND_MSG,
PLAN_SELECTION_MSG,
SUCCESS_RENEWAL_MSG,
@@ -113,7 +118,7 @@ def build_keys_response(records):
inline_keyboard = builder.as_markup()
response_message = (
"<b>🔑 Список ваших подписок</b>\n\n"
"<i>👇 Выберите подписку для управления или добавьте новую (например, для подключения нового устройства):</i>"
"<i>👇 Выберите подписку для управления или добавьте новую для подключения дополнительного устройства:</i>"
)
return inline_keyboard, response_message
@@ -214,14 +219,22 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
),
)
builder.row(
InlineKeyboardButton(
text="⏳ Продлить", callback_data=f"renew_key|{key_name}"
),
InlineKeyboardButton(
text="❌ Удалить", callback_data=f"delete_key|{key_name}"
),
)
# ✅ Добавлена проверка флага ENABLE_DELETE_KEY_BUTTON
if ENABLE_DELETE_KEY_BUTTON:
builder.row(
InlineKeyboardButton(
text="⏳ Продлить", callback_data=f"renew_key|{key_name}"
),
InlineKeyboardButton(
text="❌ Удалить", callback_data=f"delete_key|{key_name}"
),
)
else:
builder.row(
InlineKeyboardButton(
text="⏳ Продлить", callback_data=f"renew_key|{key_name}"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
@@ -480,14 +493,9 @@ async def process_callback_confirm_delete(
@router.callback_query(F.data.startswith("renew_plan|"))
async def process_callback_renew_plan(
callback_query: types.CallbackQuery, session: Any
):
async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
plan, client_id = (
callback_query.data.split("|")[1],
callback_query.data.split("|")[2],
)
plan, client_id = callback_query.data.split("|")[1], callback_query.data.split("|")[2]
days_to_extend = 30 * int(plan)
gb_multiplier = {"1": 1, "3": 3, "6": 6, "12": 12}
@@ -505,70 +513,95 @@ async def process_callback_renew_plan(
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
)
new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000)
else:
new_expiry_time = int(
expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000
)
new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000)
cost = RENEWAL_PLANS[plan]["price"]
balance = await get_balance(tg_id)
if balance < cost:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить баланс", callback_data="pay")
)
builder.row(
InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
required_amount = cost - balance
logger.info(f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}")
await save_temporary_data(
session,
tg_id,
"waiting_for_renewal_payment",
{
"plan": plan,
"client_id": client_id,
"cost": cost,
"required_amount": required_amount,
"new_expiry_time": new_expiry_time,
"total_gb": total_gb,
"email": email,
},
)
await callback_query.message.answer(
INSUFFICIENT_FUNDS_MSG,
reply_markup=builder.as_markup(),
)
if USE_NEW_PAYMENT_FLOW == "YOOKASSA":
logger.info(f"[RENEW] Запуск оплаты через Юкассу для пользователя {tg_id}")
await process_custom_amount_input(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "ROBOKASSA":
logger.info(f"[RENEW] Запуск оплаты через Робокассу для пользователя {tg_id}")
await handle_custom_amount_input(callback_query, session)
else:
logger.info(f"[RENEW] Отправка сообщения о доплате пользователю {tg_id}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
f"💳 Недостаточно средств. Пополните баланс на {required_amount}₽.",
reply_markup=builder.as_markup(),
)
return
response_message = SUCCESS_RENEWAL_MSG.format(
months=RENEWAL_PLANS[plan]["months"]
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
await callback_query.message.answer(
response_message, reply_markup=builder.as_markup()
)
servers = await get_servers_from_db()
async def renew_key_on_servers():
tasks = []
for cluster_id in servers:
task = asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
new_expiry_time,
total_gb,
)
)
tasks.append(task)
await asyncio.gather(*tasks)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time)
await renew_key_on_servers()
logger.info(f"[RENEW] Средств достаточно. Продление ключа для пользователя {tg_id}")
await complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_gb, cost, callback_query, plan)
else:
await callback_query.message.answer(KEY_NOT_FOUND_MSG)
logger.error(f"[RENEW] Ключ с client_id={client_id} не найден.")
except Exception as e:
logger.error(e)
logger.error(f"[RENEW] Ошибка при продлении ключа для пользователя {tg_id}: {e}")
async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_gb, cost, callback_query, plan):
response_message = SUCCESS_RENEWAL_MSG.format(months=plan)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if callback_query:
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
else:
await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup())
servers = await get_servers_from_db()
logger.info(f"[RENEW] Запуск продления ключа для пользователя {tg_id} на {plan} мес. на всех серверах.")
async def renew_key_on_servers():
tasks = []
for cluster_id in servers:
task = asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
new_expiry_time,
total_gb,
)
)
tasks.append(task)
await asyncio.gather(*tasks)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time)
logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.")
await renew_key_on_servers()
+12 -2
View File
@@ -1,11 +1,12 @@
import asyncio
import base64
from datetime import datetime
import aiohttp
import asyncpg
from aiohttp import web
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, TRANSITION_DATE_STR
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE, TRANSITION_DATE_STR
from database import get_servers_from_db
from logger import logger
@@ -13,7 +14,8 @@ from logger import logger
async def fetch_url_content(url, tg_id):
try:
logger.info(f"Получение URL: {url} для tg_id: {tg_id}")
async with aiohttp.ClientSession() as session:
timeout = aiohttp.ClientTimeout(total=5)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, ssl=False) as response:
if response.status == 200:
content = await response.text()
@@ -24,12 +26,20 @@ async def fetch_url_content(url, tg_id):
f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}"
)
return []
except asyncio.TimeoutError:
logger.error(f"Таймаут при получении {url} для tg_id: {tg_id}")
return []
except Exception as e:
logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}")
return []
async def combine_unique_lines(urls, tg_id, query_string):
if SUPERNODE:
logger.info(f"Режим SUPERNODE активен. Возвращаем первую ссылку для tg_id: {tg_id}")
urls_with_query = [f"{urls[0]}?{query_string}"] if urls else []
return await fetch_url_content(urls_with_query[0], tg_id) if urls_with_query else []
all_lines = []
logger.info(
f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}"
+13 -16
View File
@@ -4,17 +4,10 @@ from datetime import datetime, timedelta
from typing import Any
import pytz
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
LIMIT_IP,
PUBLIC_LINK,
TOTAL_GB,
TRIAL_TIME,
)
from py3xui import AsyncApi
from client import add_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME
from database import get_servers_from_db, store_key, use_trial
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
@@ -26,18 +19,16 @@ async def create_trial_key(tg_id: int, session: Any):
trial_status = await session.fetchval(
"SELECT trial FROM connections WHERE tg_id = $1", tg_id
)
if trial_status == 1:
return {"error": "Вы уже использовали пробную версию."}
except Exception as e:
logger.error(f"Ошибка при проверке триала: {e}")
client_id = str(uuid.uuid4())
email = generate_random_email()
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
base_email = generate_random_email()
public_link = f"{PUBLIC_LINK}{base_email}/{tg_id}"
instructions = INSTRUCTIONS
result = {"key": public_link, "instructions": instructions, "email": email}
result = {"key": public_link, "instructions": instructions, "email": base_email}
moscow_tz = pytz.timezone("Europe/Moscow")
current_time = datetime.now(moscow_tz)
@@ -46,7 +37,6 @@ async def create_trial_key(tg_id: int, session: Any):
clusters = await get_servers_from_db()
least_loaded_cluster = await get_least_loaded_cluster()
if least_loaded_cluster not in clusters:
raise ValueError(f"Кластер {least_loaded_cluster} не найден в базе данных.")
@@ -54,6 +44,13 @@ async def create_trial_key(tg_id: int, session: Any):
tasks = []
for server_info in servers_in_cluster:
server_name = server_info.get("server_name", "unknown")
if SUPERNODE:
email = f"{base_email}_{server_name.lower()}"
else:
email = base_email
tasks.append(
add_client(
AsyncApi(
@@ -70,6 +67,7 @@ async def create_trial_key(tg_id: int, session: Any):
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(server_info["inbound_id"]),
sub_id=base_email
)
)
@@ -78,7 +76,7 @@ async def create_trial_key(tg_id: int, session: Any):
await store_key(
tg_id,
client_id,
email,
base_email,
expiry_timestamp,
public_link,
server_id=least_loaded_cluster,
@@ -86,5 +84,4 @@ async def create_trial_key(tg_id: int, session: Any):
)
await use_trial(tg_id, session)
return result
+163 -270
View File
@@ -5,17 +5,19 @@ import asyncpg
from aiogram import Bot, Router, types
from aiogram.exceptions import TelegramForbiddenError
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
AUTO_DELETE_EXPIRED_KEYS,
AUTO_RENEW_KEYS,
DATABASE_URL,
DEV_MODE,
RENEWAL_PLANS,
TOTAL_GB,
TRIAL_TIME,
)
from py3xui import AsyncApi
from database import (
add_blocked_user,
add_notification,
@@ -32,6 +34,28 @@ from logger import logger
router = Router()
async def check_users_and_update_blocked(bot: Bot):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
users = await conn.fetch("SELECT tg_id FROM users")
for user in users:
try:
await bot.send_chat_action(user['tg_id'], "typing")
except (TelegramForbiddenError,Exception):
await conn.execute(
"INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING",
user['tg_id']
)
logger.info(f"User {user['tg_id']} added to blocked_users")
except Exception as e:
logger.error(f"Error in check_users_and_update_blocked: {e}")
finally:
if conn:
await conn.close()
async def notify_expiring_keys(bot: Bot):
conn = None
@@ -102,119 +126,60 @@ async def notify_10h_keys(
logger.info(f"Найдено {len(records)} ключей для уведомления за 10 часов.")
async def process_record(record):
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
for record in records:
await process_10h_record(record, bot, conn)
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "Ключ истек"
elif time_left.days > 0:
days_left_message = f"{time_left.days}"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"{hours_left}"
message = KEY_EXPIRY_10H.format(
email=email,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
days_left_message=days_left_message,
price=RENEWAL_PLANS["1"]["price"],
)
balance = await get_balance(tg_id)
if balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
)
await update_key_expiry(record["client_id"], new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id,
email,
record["client_id"],
new_expiry_time,
TOTAL_GB,
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
)
await conn.execute(
"""
UPDATE keys
SET notified = FALSE, notified_24h = FALSE
WHERE client_id = $1
""",
record["client_id"],
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
]
]
)
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
logger.info(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}")
else:
try:
keyboard = InlineKeyboardBuilder()
keyboard.button(
text="🔄 Продлить VPN", callback_data=f"renew_key|{email}"
)
keyboard.button(text="💳 Пополнить баланс", callback_data="pay")
keyboard.button(text="👤 Личный кабинет", callback_data="profile")
keyboard.adjust(1)
await bot.send_message(
tg_id, message, reply_markup=keyboard.as_markup()
)
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
await conn.execute(
"UPDATE keys SET notified = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(
f"Обновлено поле notified для клиента {record['client_id']}."
)
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.debug(
f"Ошибка при отправке уведомления пользователю {tg_id}: {e}"
)
await asyncio.gather(*(process_record(record) for record in records))
logger.info("Обработка всех уведомлений за 10 часов завершена.")
async def process_10h_record(record, bot, conn):
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
days_left_message = (
"Ключ истек" if time_left.total_seconds() <= 0 else f"{time_left.days}" if time_left.days > 0 else f"{time_left.seconds // 3600}"
)
message = KEY_EXPIRY_10H.format(
email=email,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
days_left_message=days_left_message,
price=RENEWAL_PLANS["1"]["price"],
)
balance = await get_balance(tg_id)
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(record["client_id"], new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
await conn.execute("UPDATE keys SET notified = TRUE WHERE client_id = $1", record["client_id"])
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
)
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.")
except Exception as e:
logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}")
else:
await send_renewal_notification(bot, tg_id, email, message, conn, record["client_id"], "notified")
async def notify_24h_keys(
bot: Bot,
conn: asyncpg.Connection,
@@ -234,127 +199,76 @@ async def notify_24h_keys(
logger.info(f"Найдено {len(records_24h)} ключей для уведомления за 24 часа.")
async def process_record(record):
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
for record in records_24h:
await process_24h_record(record, bot, conn)
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "Ключ истек"
elif time_left.days > 0:
days_left_message = f"{time_left.days}"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"{hours_left}"
message_24h = KEY_EXPIRY_24H.format(
email=email,
days_left_message=days_left_message,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
)
balance = await get_balance(tg_id)
if balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
)
await update_key_expiry(record["client_id"], new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id,
email,
record["client_id"],
new_expiry_time,
TOTAL_GB,
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
)
await conn.execute(
"""
UPDATE keys
SET notified_24h = FALSE, notified = FALSE
WHERE client_id = $1
""",
record["client_id"],
)
keyboard = InlineKeyboardBuilder()
keyboard.row(
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
)
await bot.send_message(
tg_id,
text=KEY_RENEWED,
reply_markup=keyboard.as_markup(),
)
logger.info(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}")
else:
try:
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f"renew_key|{email}",
)
)
builder.row(
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
)
builder.row(
types.InlineKeyboardButton(
text="👤 Личный кабинет",
callback_data="profile",
)
)
keyboard = builder.as_markup()
await bot.send_message(tg_id, message_24h, reply_markup=keyboard)
logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.")
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(
f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}"
)
await conn.execute(
"UPDATE keys SET notified_24h = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(f"Обновлено поле notified_24h для клиента {record['client_id']}.")
await asyncio.gather(*(process_record(record) for record in records_24h))
logger.info("Обработка всех уведомлений за 24 часа завершена.")
async def process_24h_record(record, bot, conn):
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
days_left_message = (
"Ключ истек" if time_left.total_seconds() <= 0 else f"{time_left.days}" if time_left.days > 0 else f"{time_left.seconds // 3600}"
)
message_24h = KEY_EXPIRY_24H.format(
email=email,
days_left_message=days_left_message,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
)
balance = await get_balance(tg_id)
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(record["client_id"], new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
await conn.execute("UPDATE keys SET notified_24h = TRUE WHERE client_id = $1", record["client_id"])
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
)
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.")
except Exception as e:
logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}")
else:
await send_renewal_notification(bot, tg_id, email, message_24h, conn, record["client_id"], "notified_24h")
async def send_renewal_notification(bot, tg_id, email, message, conn, client_id, flag):
try:
keyboard = InlineKeyboardBuilder()
keyboard.row(types.InlineKeyboardButton(text="🔄 Продлить VPN", callback_data=f"renew_key|{email}"))
keyboard.row(types.InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
keyboard.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await bot.send_message(tg_id, message, reply_markup=keyboard.as_markup())
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
await conn.execute(f"UPDATE keys SET {flag} = TRUE WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
logger.info("Проверка пользователей, не активировавших пробный период...")
@@ -439,9 +353,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
current_time,
)
logger.info(
f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает."
)
logger.info(f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает.")
for record in expiring_keys:
try:
@@ -477,22 +389,16 @@ async def process_key(record, bot, conn):
)
try:
if balance >= RENEWAL_PLANS["1"]["price"]:
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
)
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(client_id, new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id, email, client_id, new_expiry_time, TOTAL_GB
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
)
await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
await conn.execute(
"""
@@ -502,40 +408,27 @@ async def process_key(record, bot, conn):
""",
client_id,
)
logger.info(
f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}."
)
try:
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
logger.info(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления клиенту {tg_id}: {e}")
logger.info(f"Флаги notified сброшены для клиента {client_id}.")
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.")
else:
message_expired = "Ваша подписка истекла и была удалена. Получите новую через личный кабинет"
message_expired = "Ваша подписка истекла. Пополните баланс для продления."
await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard)
logger.info(f"Уведомление об истечении подписки отправлено пользователю {tg_id}.")
try:
await bot.send_message(
tg_id, text=message_expired, reply_markup=keyboard
)
logger.info(
f"Уведомление об истечении подписки и удалении ключа отправлено пользователю {tg_id}."
)
except Exception as e:
logger.error(
f"Ошибка при отправке уведомления об истечении подписки пользователю {tg_id}: {e}"
)
if AUTO_DELETE_EXPIRED_KEYS:
servers = await get_servers_from_db()
servers = await get_servers_from_db()
for cluster_id in servers:
await delete_key_from_cluster(cluster_id, email, client_id)
logger.info(f"Клиент {client_id} удален из кластера {cluster_id}.")
for cluster_id in servers:
await delete_key_from_cluster(cluster_id, email, client_id)
logger.info(f"Клиент {client_id} удален из кластера {cluster_id}.")
await delete_key(client_id)
logger.info(f"Ключ для клиента с ID {client_id} удален из базы данных.")
await delete_key(client_id)
logger.info(f"Ключ {client_id} удалён из базы данных.")
else:
logger.info(f"Ключ {client_id} НЕ был удалён (AUTO_DELETE_EXPIRED_KEYS=False).")
except Exception as e:
logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
+1
View File
@@ -1,6 +1,7 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CRYPTO_BOT_ENABLE,
DONATIONS_ENABLE,
+1
View File
@@ -1,6 +1,7 @@
__all__ = ("router",)
from aiogram import Router
from config import (
CRYPTO_BOT_ENABLE,
ROBOKASSA_ENABLE,
+57 -20
View File
@@ -1,26 +1,29 @@
import hashlib
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from robokassa import HashAlgorithm, Robokassa
from config import (
DATABASE_URL,
ROBOKASSA_ENABLE,
ROBOKASSA_LOGIN,
ROBOKASSA_PASSWORD1,
ROBOKASSA_PASSWORD2,
ROBOKASSA_TEST_MODE,
)
from robokassa import HashAlgorithm, Robokassa
from database import (
add_connection,
add_payment,
check_connection_exists,
get_key_count,
get_temporary_data,
update_balance,
)
from handlers.payments.utils import send_payment_success_notification
@@ -242,17 +245,31 @@ async def process_custom_amount_selection(
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input(message: types.Message, state: FSMContext):
tg_id = message.chat.id
logger.info(f"User {tg_id} entered custom amount: {message.text}")
async def handle_custom_amount_input(message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None):
if isinstance(message, types.CallbackQuery):
tg_id = message.message.chat.id
else:
tg_id = message.chat.id
logger.info(f"User {tg_id} initiated payment through ROBOKASSA")
inv_id = 0
try:
amount = int(message.text)
if amount <= 0:
raise ValueError("Сумма должна быть положительным числом.")
await state.update_data(amount=amount)
conn = await asyncpg.connect(DATABASE_URL)
user_data = await get_temporary_data(conn, tg_id)
await conn.close()
if not user_data:
await message.answer("Данные для оплаты не найдены. Попробуйте снова.")
return
state_type = user_data["state"]
amount = user_data["data"].get("required_amount", 0)
if amount <= 0:
await message.answer("Недостаточная сумма для пополнения.")
return
payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id)
@@ -260,18 +277,38 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Оплатить", url=payment_url)],
[InlineKeyboardButton(text="💳 Оплатить", url=payment_url)],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")],
]
)
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
reply_markup=confirm_keyboard,
)
await state.clear()
except ValueError as e:
logger.error(f"Некорректная сумма от пользователя {tg_id}: {e}")
await message.answer(
text="Введите корректную сумму в рублях (целое положительное число)."
)
if state_type == "waiting_for_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
elif state_type == "waiting_for_renewal_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
else:
await message.answer("Некорректное состояние данных. Попробуйте снова.")
return
if isinstance(message, types.CallbackQuery):
await message.message.answer(
text=message_text,
reply_markup=confirm_keyboard,
)
else:
await message.answer(
text=message_text,
reply_markup=confirm_keyboard,
)
if isinstance(state, FSMContext):
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}")
error_message = "Произошла ошибка при создании платежа. Попробуйте позже."
if isinstance(message, types.CallbackQuery):
await message.message.answer(error_message)
else:
await message.answer(error_message)
+1992 -1158
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+57 -4
View File
@@ -1,15 +1,18 @@
import os
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS
from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS
from database import get_balance, get_key_count, get_referral_stats, get_trial
from handlers.buttons.profile import (
ADD_SUB,
BALANCE,
BALANCE_HISTORY,
GIFTS,
INSTRUCTIONS,
INVITE,
@@ -53,7 +56,7 @@ async def process_callback_view_profile(
)
if key_count == 0:
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i></pre>"
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Подписка, чтобы настроить VPN-подключение</i></pre>"
else:
profile_message += f"\n<pre> <i>{NEWS_MESSAGE}</i></pre>"
@@ -66,8 +69,8 @@ async def process_callback_view_profile(
builder.row(
InlineKeyboardButton(
text=PAYMENT,
callback_data="pay",
text=BALANCE,
callback_data="balance",
)
)
builder.row(
@@ -112,6 +115,56 @@ async def process_callback_view_profile(
await conn.close()
@router.callback_query(F.data == "balance")
async def balance_handler(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await callback_query.message.answer(
"💰 Управление балансом:",
reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "balance_history")
async def balance_history_handler(callback_query: types.CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
query = """
SELECT amount, payment_system, status, created_at
FROM payments
WHERE tg_id = $1
ORDER BY created_at DESC
LIMIT 3
"""
records = await session.fetch(query, callback_query.from_user.id)
if records:
history_text = "📊 <b>Последние 3 операции с балансом:</b>\n\n"
for record in records:
amount = record['amount']
payment_system = record['payment_system']
status = record['status']
date = record['created_at'].strftime('%Y-%m-%d %H:%M:%S')
history_text += (
f"<b>Сумма:</b> {amount}\n"
f"<b>Способ оплаты:</b> {payment_system}\n"
f"<b>Статус:</b> {status}\n"
f"<b>Дата:</b> {date}\n\n"
)
else:
history_text = "❌ У вас пока нет операций с балансом."
await callback_query.message.answer(
history_text,
reply_markup=builder.as_markup()
)
@router.message(F.text == "/tariffs")
@router.callback_query(F.data == "view_tariffs")
async def view_tariffs_handler(callback_query: types.CallbackQuery):
+11 -6
View File
@@ -11,6 +11,7 @@ from aiogram.types import (
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CHANNEL_EXISTS,
CHANNEL_URL,
@@ -21,7 +22,6 @@ from config import (
DOWNLOAD_IOS,
SUPPORT_CHAT_URL,
)
from database import (
add_connection,
add_referral,
@@ -200,17 +200,22 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL),
)
if CHANNEL_EXISTS:
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL)
)
else:
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL)
)
if admin:
builder.row(
InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")
)
builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn"))
builder.row(InlineKeyboardButton(text="🌐 О VPN", callback_data="about_vpn"))
if os.path.isfile(image_path):
with open(image_path, "rb") as image_from_buffer:
+1 -1
View File
@@ -4,9 +4,9 @@ import re
import aiohttp
import asyncpg
from config import DATABASE_URL
from bot import bot
from config import DATABASE_URL
from database import get_servers_from_db
from logger import logger
+1
View File
@@ -3,6 +3,7 @@ from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import ADMIN_ID
+1
View File
@@ -4,6 +4,7 @@ from typing import Any
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import DATABASE_URL
+15 -38
View File
@@ -1,42 +1,19 @@
import asyncio
from collections.abc import Awaitable, Callable
from typing import Any
from aiogram import Dispatcher, types
from aiogram.dispatcher import DEFAULT_RATE_LIMIT
from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.utils.exceptions import Throttled
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
class ThrottlingMiddleware(BaseMiddleware):
def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix="antiflood_"):
self.rate_limit = limit
self.prefix = key_prefix
super(ThrottlingMiddleware, self).__init__()
class ThrottleMiddleware(BaseMiddleware):
def __init__(self, limit: int):
self.limit = limit
async def on_process_message(self, message: types.Message, data: dict):
handler = current_handler.get()
dispatcher = Dispatcher.get_current()
if handler:
limit = getattr(handler, "throttling_rate_limit", self.rate_limit)
key = getattr(handler, "throttling_key", f"{self.prefix}_{handler.__name__}")
else:
limit = self.rate_limit
key = f"{self.prefix}_message"
try:
await dispatcher.throttle(key, rate=limit)
except Throttled as t:
await self.message_throttled(message, t)
raise CancelHandler()
async def message_throttled(self, message: types.Message, throttled: Throttled):
delta = throttled.rate - throttled.delta
if throttled.exceeded_count <= 2:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.reply("🚫 Слишком много запросов! Пожалуйста, не торопитесь!", reply_markup=builder.as_markup())
await asyncio.sleep(delta)
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
#todo
return await handler(event, data)
+15 -3
View File
@@ -5,13 +5,25 @@ target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "ANN", "ASYNC", "S", "BLE", "FBT", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE"]
ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001']
exclude = [
".git",
"venv",
"main.py",
"handlers/payments",
]
[tool.ruff.format]
quote-style = "single"
indent-style = "space"
quote-style = "double"
indent-style = "tab"
[tool.darker]
src = ["."]
revision = "HEAD"
diff = false
check = false
check = false
exclude = [
".git",
"venv",
"main.py",
"handlers/payments",
]
+1 -1
View File
@@ -5,10 +5,10 @@ from datetime import datetime, timedelta
import asyncpg
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import ADMIN_ID, DATABASE_URL, PING_TIME
from ping3 import ping
from bot import bot
from config import ADMIN_ID, DATABASE_URL, PING_TIME
from database import get_servers_from_db
from logger import logger