Refactor and clean up code formatting across multiple files

- Consolidated and simplified code formatting by removing unnecessary line breaks and improving readability in various functions.
- Updated the handling of backup file sending in backup.py for better clarity.
- Streamlined error handling and logging messages in several handlers to enhance consistency.
- Adjusted the Makefile to exclude specific files during Ruff checks and formatting.
- Made minor adjustments to function signatures and parameter handling for improved clarity and consistency.

This commit enhances code maintainability and readability without altering functionality.
This commit is contained in:
Zakhar Izmaylov
2025-01-21 08:57:12 +03:00
parent ffc18f4056
commit acb73101c0
33 changed files with 446 additions and 1124 deletions
+35
View File
@@ -0,0 +1,35 @@
name: Code Formatting
on: [push, pull_request]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Run Ruff formatter with pyproject.toml
run: ruff format . --config pyproject.toml --exclude main.py,handlers/payments
- name: Check for changes
id: check_changes
run: |
git diff --exit-code || echo "changes_detected=true" >> $GITHUB_OUTPUT
- name: Commit and push changes
if: steps.check_changes.outputs.changes_detected == 'true'
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add .
git commit -m "Auto-format code with Ruff using pyproject.toml"
git push
+4 -3
View File
@@ -1,6 +1,7 @@
formatting:
@echo "Running Ruff..." && ruff check . --fix
@echo "Running Ruff format..." && ruff format .
@echo "Running Ruff format..." && ruff format . --config pyproject.toml --exclude main.py,handlers/payments
@echo "Running Ruff..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments --fix
lint:
@echo "Running Ruff checks..." && ruff check .
@echo "Running Ruff checks..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments
+1 -3
View File
@@ -59,9 +59,7 @@ def _create_database_backup():
async def _send_backup_to_admin(bot, backup_file_path):
try:
with open(backup_file_path, "rb") as backup_file:
backup_input_file = BufferedInputFile(
backup_file.read(), filename=os.path.basename(backup_file_path)
)
backup_input_file = BufferedInputFile(backup_file.read(), filename=os.path.basename(backup_file_path))
admin_ids: int | list[int] = ADMIN_ID
if isinstance(admin_ids, list):
for id in admin_ids:
+1 -1
View File
@@ -44,7 +44,7 @@ async def errors_handler(
filename=f"error_{event.update.update_id}.txt",
),
caption=f"{hbold(type(event.exception).__name__)}: {str(event.exception)[:1021]}...",
)
)
except TelegramBadRequest as exception:
logger.warning(f"Failed to send error details: {exception}")
except Exception as exception:
+5 -11
View File
@@ -15,7 +15,7 @@ async def add_client(
enable: bool,
flow: str,
inbound_id: int,
sub_id
sub_id,
):
"""
Adds a client to the server via 3x-ui.
@@ -45,9 +45,7 @@ async def add_client(
error_message = str(e)
if "Duplicate email" in error_message:
logger.warning(
f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}"
)
logger.warning(f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}")
return {"status": "duplicate", "email": email}
logger.error(f"Ошибка при добавлении клиента {email}: {error_message}")
@@ -55,7 +53,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, sub_id = str
xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id=str
):
"""
Функция для обновления срока действия ключа клиента по email.
@@ -72,9 +70,7 @@ async def extend_client_key(
logger.warning(f"Ошибка: клиент {email} не имеет действительного ID.")
return
logger.info(
f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}"
)
logger.info(f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}")
client.id = client_id
client.expiry_time = new_expiry_time
@@ -87,9 +83,7 @@ async def extend_client_key(
await xui.client.update(client.id, client)
await xui.client.reset_stats(inbound_id, email)
logger.info(
f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}."
)
logger.info(f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}.")
except Exception as e:
logger.error(f"Ошибка при обновлении клиента с email {email}: {e}")
+40 -121
View File
@@ -26,9 +26,7 @@ async def save_temporary_data(session, tg_id: int, state: str, data: dict):
async def get_temporary_data(session, tg_id: int) -> dict | None:
"""Извлекает временные данные пользователя."""
result = await session.fetchrow(
"SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id
)
result = await session.fetchrow("SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id)
if result:
return {"state": result["state"], "data": json.loads(result["data"])}
return None
@@ -71,18 +69,14 @@ async def check_unique_server_name(server_name: str) -> bool:
"""
conn = await asyncpg.connect(DATABASE_URL)
result = await conn.fetchrow(
"SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name
)
result = await conn.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name)
await conn.close()
return result is None
async def create_coupon(
coupon_code: str, amount: float, usage_limit: int, session: Any
):
async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any):
"""
Создает новый купон в базе данных.
@@ -142,7 +136,8 @@ async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10):
ORDER BY id
LIMIT $1 OFFSET $2
""",
per_page, offset
per_page,
offset,
)
total_count = await session.fetchval("SELECT COUNT(*) FROM coupons")
@@ -150,12 +145,7 @@ async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10):
logger.info(f"Успешно получено {len(coupons)} купонов из базы данных (страница {page})")
return {
"coupons": coupons,
"total": total_count,
"pages": total_pages,
"current_page": page
}
return {"coupons": coupons, "total": total_count, "pages": total_pages, "current_page": page}
except Exception as e:
logger.error(f"Критическая ошибка при получении списка купонов: {e}")
logger.exception("Трассировка стека ошибки получения купонов")
@@ -230,9 +220,7 @@ async def restore_trial(tg_id: int, session: Any):
logger.info(f"Триальный период успешно восстановлен для пользователя {tg_id}")
return True
except Exception as e:
logger.error(
f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}"
)
logger.error(f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}")
return False
@@ -264,9 +252,7 @@ async def use_trial(tg_id: int, session: Any):
return False
async def add_connection(
tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None
):
async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None):
"""
Добавляет новое подключение для пользователя в базу данных.
@@ -293,9 +279,7 @@ async def add_connection(
f"Успешно добавлено новое подключение для пользователя {tg_id} с балансом {balance} и статусом триала {trial}"
)
except Exception as e:
logger.error(
f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}"
)
logger.error(f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}")
raise
@@ -369,9 +353,7 @@ async def store_key(
key,
server_id,
)
logger.info(
f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}"
)
logger.info(f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}")
except Exception as e:
logger.error(f"Ошибка при сохранении ключа для пользователя {tg_id}: {e}")
raise
@@ -437,14 +419,10 @@ async def get_keys_by_server(tg_id: int, server_id: str):
tg_id,
server_id,
)
logger.info(
f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}"
)
logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}")
return records
except Exception as e:
logger.error(
f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}"
)
logger.error(f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}")
raise
finally:
if conn:
@@ -468,14 +446,10 @@ async def has_active_key(tg_id: int) -> bool:
try:
conn = await asyncpg.connect(DATABASE_URL)
count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id)
logger.info(
f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}"
)
logger.info(f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}")
return count > 0
except Exception as e:
logger.error(
f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}"
)
logger.error(f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}")
raise
finally:
if conn:
@@ -498,9 +472,7 @@ async def get_balance(tg_id: int) -> float:
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
balance = await conn.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
logger.info(f"Получен баланс для пользователя {tg_id}: {balance}")
return balance if balance is not None else 0.0
except Exception as e:
@@ -558,15 +530,11 @@ async def get_trial(tg_id: int, session: Any) -> int:
int: Статус триала (0 - не использован, 1 - использован)
"""
try:
trial = await session.fetchval(
"SELECT trial FROM connections WHERE tg_id = $1", tg_id
)
trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id)
logger.info(f"Получен статус триала для пользователя {tg_id}: {trial}")
return trial if trial is not None else 0
except Exception as e:
logger.error(
f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}"
)
logger.error(f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}")
return 0
@@ -590,9 +558,7 @@ async def get_key_count(tg_id: int) -> int:
logger.info(f"Получено количество ключей для пользователя {tg_id}: {count}")
return count if count is not None else 0
except Exception as e:
logger.error(
f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}"
)
logger.error(f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}")
return 0
finally:
if conn:
@@ -623,11 +589,8 @@ async def get_all_users(conn):
async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any):
try:
if referred_tg_id == referrer_tg_id:
logger.warning(
f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку."
)
logger.warning(f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку.")
return
await session.execute(
@@ -638,9 +601,7 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any):
referred_tg_id,
referrer_tg_id,
)
logger.info(
f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}"
)
logger.info(f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}")
except Exception as e:
logger.error(f"Ошибка при добавлении реферала: {e}")
raise
@@ -673,9 +634,7 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
for level in range(1, MAX_REFERRAL_LEVELS + 1):
if current_tg_id in visited_tg_ids:
logger.warning(
f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение."
)
logger.warning(f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение.")
break
visited_tg_ids.add(current_tg_id)
@@ -714,15 +673,11 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
bonus = round(amount * bonus_percent, 2)
if bonus > 0:
logger.info(
f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}."
)
logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}.")
await update_balance(referrer_tg_id, bonus)
except Exception as e:
logger.error(
f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}"
)
logger.error(f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}")
finally:
if conn:
await conn.close()
@@ -821,9 +776,7 @@ async def get_referral_stats(referrer_tg_id: int):
)
total_referral_bonus = total_referral_bonus or 0
logger.debug(
f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}"
)
logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}")
return {
"total_referrals": total_referrals,
@@ -833,9 +786,7 @@ async def get_referral_stats(referrer_tg_id: int):
}
except Exception as e:
logger.error(
f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}"
)
logger.error(f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}")
raise
finally:
if conn:
@@ -857,9 +808,7 @@ async def update_key_expiry(client_id: str, new_expiry_time: int):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info(
f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}"
)
logger.info(f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}")
await conn.execute(
"""
@@ -873,9 +822,7 @@ async def update_key_expiry(client_id: str, new_expiry_time: int):
logger.info(f"Успешно обновлено время истечения ключа для клиента {client_id}")
except Exception as e:
logger.error(
f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}"
)
logger.error(f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}")
raise
finally:
if conn:
@@ -896,9 +843,7 @@ async def delete_key(client_id: str):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info(
f"Установлено подключение к базе данных для удаления ключа клиента {client_id}"
)
logger.info(f"Установлено подключение к базе данных для удаления ключа клиента {client_id}")
await conn.execute(
"""
@@ -932,9 +877,7 @@ async def add_balance_to_client(client_id: str, amount: float):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info(
f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}"
)
logger.info(f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}")
await conn.execute(
"""
@@ -972,9 +915,7 @@ async def get_client_id_by_email(email: str):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info(
f"Установлено подключение к базе данных для поиска client_id по email: {email}"
)
logger.info(f"Установлено подключение к базе данных для поиска client_id по email: {email}")
client_id = await conn.fetchval(
"""
@@ -1015,13 +956,9 @@ async def get_tg_id_by_client_id(client_id: str):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info(
f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}"
)
logger.info(f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}")
result = await conn.fetchrow(
"SELECT tg_id FROM keys WHERE client_id = $1", client_id
)
result = await conn.fetchrow("SELECT tg_id FROM keys WHERE client_id = $1", client_id)
if result:
logger.info(f"Найден Telegram ID для client_id: {client_id}")
@@ -1064,9 +1001,7 @@ async def upsert_user(
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info(
f"Установлено подключение к базе данных для обновления пользователя {tg_id}"
)
logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}")
await conn.execute(
"""
@@ -1113,9 +1048,7 @@ async def add_payment(tg_id: int, amount: float, payment_system: str):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info(
f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}"
)
logger.info(f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}")
await conn.execute(
"""
@@ -1126,9 +1059,7 @@ async def add_payment(tg_id: int, amount: float, payment_system: str):
amount,
payment_system,
)
logger.info(
f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}"
)
logger.info(f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}")
except Exception as e:
logger.error(f"Ошибка при добавлении платежа для пользователя {tg_id}: {e}")
raise
@@ -1161,19 +1092,13 @@ async def add_notification(tg_id: int, notification_type: str, session: Any):
tg_id,
notification_type,
)
logger.info(
f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}"
)
logger.info(f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}")
except Exception as e:
logger.error(
f"Ошибка при добавлении notification для пользователя {tg_id}: {e}"
)
logger.error(f"Ошибка при добавлении notification для пользователя {tg_id}: {e}")
raise
async def check_notification_time(
tg_id: int, notification_type: str, hours: int = 12, session: Any = None
) -> bool:
async def check_notification_time(tg_id: int, notification_type: str, hours: int = 12, session: Any = None) -> bool:
"""
Проверяет, прошло ли указанное количество часов с момента последнего уведомления.
@@ -1218,9 +1143,7 @@ async def check_notification_time(
return can_notify
except Exception as e:
logger.error(
f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}"
)
logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}")
return False
finally:
@@ -1259,11 +1182,8 @@ async def get_servers_from_db():
async def delete_user_data(session: Any, tg_id: int):
try:
await session.execute(
"DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id
)
await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id)
except Exception as e:
logger.warning(f"У Вас версия без подарков для {tg_id}: {e}")
await session.execute("DELETE FROM payments WHERE tg_id = $1", tg_id)
@@ -1322,7 +1242,6 @@ async def store_gift_link(
logger.error(f"Не удалось добавить подарок с ID {gift_id} в базу данных.")
return False
except Exception as e:
logger.error(f"Ошибка при сохранении подарка с ID {gift_id} в базе данных: {e}")
return False
-1
View File
@@ -10,6 +10,5 @@ class IsAdminFilter(BaseFilter):
admin_ids: int | list[int] = ADMIN_ID
if isinstance(admin_ids, list):
return message.from_user.id in admin_ids
return message.from_user.id == admin_ids
except Exception:
return False
+1 -1
View File
@@ -3,6 +3,7 @@ __all__ = ("router",)
from aiogram import Router
from .admin import router as admin_router
from .captcha import router as captcha_router
from .coupons import router as coupons_router
from .donate import router as donate_router
from .instructions import router as instructions_router
@@ -13,7 +14,6 @@ from .payments import router as payments_router
from .profile import router as profile_router
from .start import router as start_router
from .user import router as user_router
from .captcha import router as captcha_router
router = Router(name="handlers_main_router")
+12 -19
View File
@@ -19,25 +19,19 @@ router = Router()
@router.callback_query(F.data == "coupons_editor", IsAdminFilter())
async def show_coupon_management_menu(
callback_query: types.CallbackQuery, state: FSMContext
):
async def show_coupon_management_menu(callback_query: types.CallbackQuery, state: FSMContext):
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text=" Создать купон", callback_data="create_coupon")
)
builder.row(InlineKeyboardButton(text=" Создать купон", callback_data="create_coupon"))
builder.row(InlineKeyboardButton(text="🎟️ Купоны", callback_data="coupons"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(
"🛠 Меню управления купонами:", reply_markup=builder.as_markup()
)
await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("coupons"), IsAdminFilter())
async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
try:
page = int(callback_query.data.split(':')[1]) if ':' in callback_query.data else 1
page = int(callback_query.data.split(":")[1]) if ":" in callback_query.data else 1
per_page = 10
result = await get_all_coupons(session, page, per_page)
coupons = result["coupons"]
@@ -63,15 +57,16 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
f"🔢 <b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n\n"
)
builder.row(InlineKeyboardButton(
text=f"❌ Удалить {coupon['code']}",
callback_data=f"delete_coupon_{coupon['code']}"
))
builder.row(
InlineKeyboardButton(
text=f"❌ Удалить {coupon['code']}", callback_data=f"delete_coupon_{coupon['code']}"
)
)
if current_page > 1:
builder.row(InlineKeyboardButton(text="⬅️ Предыдущая", callback_data=f"coupons:{current_page-1}"))
builder.row(InlineKeyboardButton(text="⬅️ Предыдущая", callback_data=f"coupons:{current_page - 1}"))
if current_page < total_pages:
builder.row(InlineKeyboardButton(text="➡️ Следующая", callback_data=f"coupons:{current_page+1}"))
builder.row(InlineKeyboardButton(text="➡️ Следующая", callback_data=f"coupons:{current_page + 1}"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
await callback_query.message.answer(coupon_list, reply_markup=builder.as_markup())
@@ -154,9 +149,7 @@ async def process_coupon_data(message: types.Message, state: FSMContext, session
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
await message.answer(result_message, reply_markup=builder.as_markup())
await state.clear()
+47 -161
View File
@@ -40,48 +40,23 @@ async def handle_admin_message(message: types.Message, state: FSMContext):
BOT_VERSION = "4.0.0-preAlpha(14)"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📊 Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(
text="👥 Управление пользователями", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(
text="🖥️ Управление серверами", callback_data="servers_editor"
)
)
builder.row(
InlineKeyboardButton(
text="🎟️ Управление купонами", callback_data="coupons_editor"
)
)
builder.row(
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to")
)
builder.row(
InlineKeyboardButton(text="🤖 Управление Ботом", callback_data="bot_management")
)
builder.row(InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="user_stats"))
builder.row(InlineKeyboardButton(text="👥 Управление пользователями", callback_data="user_editor"))
builder.row(InlineKeyboardButton(text="🖥️ Управление серверами", callback_data="servers_editor"))
builder.row(InlineKeyboardButton(text="🎟️ Управление купонами", callback_data="coupons_editor"))
builder.row(InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to"))
builder.row(InlineKeyboardButton(text="🤖 Управление Ботом", callback_data="bot_management"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
f"🤖 Панель администратора\n\nВерсия бота: <b>{BOT_VERSION}</b>",
reply_markup=builder.as_markup()
f"🤖 Панель администратора\n\nВерсия бота: <b>{BOT_VERSION}</b>", reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "bot_management")
async def handle_bot_management(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")
)
builder.row(
InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")
)
builder.row(InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups"))
builder.row(InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot"))
builder.row(InlineKeyboardButton(text="🚫 Баны", callback_data="ban_user"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin"))
await callback_query.message.answer(
@@ -106,13 +81,9 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any):
total_payments_month = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
total_payments_all_time = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments"
)
total_payments_all_time = await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments")
registrations_today = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE"
)
registrations_today = await session.fetchval("SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE")
registrations_week = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('week', CURRENT_DATE)"
)
@@ -120,9 +91,7 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any):
"SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
users_updated_today = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE"
)
users_updated_today = await session.fetchval("SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE")
active_keys = await session.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
@@ -153,27 +122,17 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any):
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats")
)
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats"))
builder.row(
InlineKeyboardButton(
text="📥 Выгрузить пользователей в CSV",
callback_data="export_users_csv",
)
)
builder.row(
InlineKeyboardButton(
text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv"
)
)
builder.row(
InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")
)
builder.row(InlineKeyboardButton(text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv"))
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
await callback_query.message.answer(
stats_message, reply_markup=builder.as_markup()
)
await callback_query.message.answer(stats_message, reply_markup=builder.as_markup())
except Exception as e:
logger.error(f"Error in user_stats_menu: {e}")
@@ -200,14 +159,10 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any):
)
if not users:
await callback_query.message.answer(
"📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup()
)
await callback_query.message.answer("📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup())
return
csv_data = (
"tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n"
)
csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n"
for user in users:
csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n"
@@ -253,9 +208,7 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any):
)
if not payments:
await callback_query.message.answer(
"📭 Нет платежей для экспорта.", reply_markup=builder.as_markup()
)
await callback_query.message.answer("📭 Нет платежей для экспорта.", reply_markup=builder.as_markup())
return
csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV
@@ -283,24 +236,10 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any):
@router.callback_query(F.data == "send_to", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all")
)
builder.row(
InlineKeyboardButton(
text="📢 Отправить с подпиской", callback_data="send_to_subscribed"
)
)
builder.row(
InlineKeyboardButton(
text="📢 Отправить без подписки", callback_data="send_to_unsubscribed"
)
)
builder.row(
InlineKeyboardButton(
text="📢 Рассылка по кластеру", callback_data="send_to_cluster"
)
)
builder.row(InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all"))
builder.row(InlineKeyboardButton(text="📢 Отправить с подпиской", callback_data="send_to_subscribed"))
builder.row(InlineKeyboardButton(text="📢 Отправить без подписки", callback_data="send_to_unsubscribed"))
builder.row(InlineKeyboardButton(text="📢 Рассылка по кластеру", callback_data="send_to_cluster"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(
"✍️ Выберите группу пользователей и введите текст сообщения для рассылки:",
@@ -311,34 +250,26 @@ async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
@router.callback_query(F.data == "send_to_all", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="all")
await callback_query.message.answer(
"✍️ Введите текст сообщения для рассылки всем пользователям:"
)
await callback_query.message.answer("✍️ Введите текст сообщения для рассылки всем пользователям:")
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_subscribed", IsAdminFilter())
async def handle_send_to_subscribed(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="subscribed")
await callback_query.message.answer(
"✍️ Введите текст сообщения для рассылки пользователям с активной подпиской:"
)
await callback_query.message.answer("✍️ Введите текст сообщения для рассылки пользователям с активной подпиской:")
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_unsubscribed", IsAdminFilter())
async def handle_send_to_unsubscribed(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="unsubscribed")
await callback_query.message.answer(
"✍️ Введите текст сообщения для рассылки пользователям без активной подписки:"
)
await callback_query.message.answer("✍️ Введите текст сообщения для рассылки пользователям без активной подписки:")
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_cluster", IsAdminFilter())
async def handle_send_to_cluster(
callback_query: CallbackQuery, state: FSMContext, session: Any
):
async def handle_send_to_cluster(callback_query: CallbackQuery, state: FSMContext, session: Any):
clusters = await session.fetch("SELECT DISTINCT cluster_name FROM servers")
builder = InlineKeyboardBuilder()
@@ -368,9 +299,7 @@ async def handle_send_cluster(callback_query: CallbackQuery, state: FSMContext):
@router.message(UserEditorState.waiting_for_message, IsAdminFilter())
async def process_message_to_all(
message: types.Message, state: FSMContext, session: Any
):
async def process_message_to_all(message: types.Message, state: FSMContext, session: Any):
text_message = message.text
try:
@@ -424,9 +353,7 @@ async def process_message_to_all(
success_count += 1
except Exception as e:
error_count += 1
logger.error(
f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}")
await message.answer(
f"📤 Рассылка завершена:\n"
@@ -442,13 +369,9 @@ async def process_message_to_all(
@router.callback_query(F.data == "backups", IsAdminFilter())
async def handle_backup(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.answer(
"💾 Инициализация резервного копирования базы данных..."
)
await callback_query.message.answer("💾 Инициализация резервного копирования базы данных...")
await backup_database()
await callback_query.message.answer(
"✅ Резервная копия успешно создана и отправлена администратору."
)
await callback_query.message.answer("✅ Резервная копия успешно создана и отправлена администратору.")
@router.callback_query(F.data == "restart_bot", IsAdminFilter())
@@ -456,9 +379,7 @@ async def handle_restart(callback_query: CallbackQuery, state: FSMContext):
await state.set_state(UserEditorState.waiting_for_restart_confirmation)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Да, перезапустить", callback_data="confirm_restart"
),
InlineKeyboardButton(text="✅ Да, перезапустить", callback_data="confirm_restart"),
InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"),
)
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
@@ -484,13 +405,9 @@ async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext):
text=True,
)
await state.clear()
await callback_query.message.answer(
"🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()
)
await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup())
except subprocess.CalledProcessError:
await callback_query.message.answer(
"🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()
)
await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup())
except Exception as e:
await callback_query.message.answer(
f"⚠️ Ошибка при перезагрузке бота: {e.stderr}",
@@ -507,37 +424,20 @@ async def user_editor_menu(callback_query: CallbackQuery):
callback_data="search_by_key_name",
)
)
builder.row(
InlineKeyboardButton(
text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id"
)
)
builder.row(
InlineKeyboardButton(
text="🌐 Поиск по Username", callback_data="search_by_username"
)
)
builder.row(InlineKeyboardButton(text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id"))
builder.row(InlineKeyboardButton(text="🌐 Поиск по Username", callback_data="search_by_username"))
builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin"))
await callback_query.message.answer(
"👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup()
)
await callback_query.message.answer("👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup())
@router.callback_query(F.data == "ban_user")
async def handle_ban_user(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="📄 Выгрузить в CSV", callback_data="export_to_csv")
)
builder.row(
InlineKeyboardButton(
text="🗑️ Удалить из БД", callback_data="delete_banned_users"
)
)
builder.row(InlineKeyboardButton(text="📄 Выгрузить в CSV", callback_data="export_to_csv"))
builder.row(InlineKeyboardButton(text="🗑️ Удалить из БД", callback_data="delete_banned_users"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management"))
await callback_query.message.answer(
"🚫 Заблокировавшие бота\n\n"
"Здесь можно просматривать и удалять пользователей, которые забанили вашего бота!",
"🚫 Заблокировавшие бота\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!",
reply_markup=builder.as_markup(),
)
@@ -559,14 +459,10 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery):
csv_output.seek(0)
document = BufferedInputFile(
file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv"
)
document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management"))
await callback_query.message.answer_document(
document=document,
@@ -575,9 +471,7 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery):
)
except Exception as e:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management"))
await callback_query.message.answer(
text=f"Ошибка при выгрузке CSV: {e}",
reply_markup=builder.as_markup(),
@@ -594,31 +488,23 @@ async def delete_banned_users(callback_query: types.CallbackQuery):
blocked_ids = [record["tg_id"] for record in blocked_users]
if not blocked_ids:
await callback_query.message.answer(
"📂 Нет заблокировавших пользователей для удаления."
)
await callback_query.message.answer("📂 Нет заблокировавших пользователей для удаления.")
return
for tg_id in blocked_ids:
await delete_user_data(conn, tg_id)
await conn.execute(
"DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids
)
await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management"))
await callback_query.message.answer(
text=f"🗑️ Удалено данные о {len(blocked_ids)} пользователях и связанных записях.",
reply_markup=builder.as_markup(),
)
except Exception as e:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management"))
await callback_query.message.answer(
text=f"Ошибка при удалении записей: {e}",
reply_markup=builder.as_markup(),
+31 -117
View File
@@ -9,7 +9,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SUPERNODE
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers_from_db
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import create_key_on_cluster
@@ -33,15 +33,9 @@ async def handle_servers_editor(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
for cluster_name, cluster_servers in servers.items():
builder.row(
InlineKeyboardButton(
text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}"
)
)
builder.row(InlineKeyboardButton(text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}"))
builder.row(
InlineKeyboardButton(text=" Добавить кластер", callback_data="add_cluster")
)
builder.row(InlineKeyboardButton(text=" Добавить кластер", callback_data="add_cluster"))
builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin"))
await callback_query.message.answer(
@@ -72,11 +66,7 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext):
if cluster_name == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor"))
await message.answer(
"Процесс создания кластера отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
@@ -90,9 +80,7 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext):
await state.update_data(cluster_name=cluster_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor"))
await message.answer(
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
@@ -110,11 +98,7 @@ async def handle_server_name_input(message: types.Message, state: FSMContext):
if server_name == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor"))
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
@@ -127,9 +111,7 @@ async def handle_server_name_input(message: types.Message, state: FSMContext):
server_unique = await check_unique_server_name(server_name)
if not server_unique:
await message.answer(
"❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя."
)
await message.answer("❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.")
return
user_data = await state.get_data()
@@ -137,9 +119,7 @@ async def handle_server_name_input(message: types.Message, state: FSMContext):
await state.update_data(server_name=server_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor"))
await message.answer(
f"<b>Введите API URL для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
@@ -158,11 +138,7 @@ async def handle_api_url_input(message: types.Message, state: FSMContext):
if api_url == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor"))
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
@@ -183,9 +159,7 @@ async def handle_api_url_input(message: types.Message, state: FSMContext):
await state.update_data(api_url=api_url)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor"))
await message.answer(
f"<b>Введите subscription_url для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
@@ -205,11 +179,7 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex
if subscription_url == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor"))
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
@@ -230,9 +200,7 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex
await state.update_data(subscription_url=subscription_url)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor"))
await message.answer(
f"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
@@ -247,9 +215,7 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext):
inbound_id = message.text.strip()
if not inbound_id.isdigit():
await message.answer(
"❌ inbound_id должен быть числовым значением. Попробуйте снова."
)
await message.answer("❌ inbound_id должен быть числовым значением. Попробуйте снова.")
return
user_data = await state.get_data()
@@ -273,11 +239,7 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext):
await conn.close()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад к кластерам", callback_data="servers_editor"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад к кластерам", callback_data="servers_editor"))
await message.answer(
f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!",
@@ -304,11 +266,7 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC
)
)
builder.row(
InlineKeyboardButton(
text=" Добавить сервер", callback_data=f"add_server|{cluster_name}"
)
)
builder.row(InlineKeyboardButton(text=" Добавить сервер", callback_data=f"add_server|{cluster_name}"))
builder.row(
InlineKeyboardButton(
@@ -331,11 +289,7 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад в управление кластерами", callback_data="servers_editor"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor"))
await callback_query.message.answer(
f"🔧 Управление серверами для кластера {cluster_name}",
@@ -361,11 +315,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery):
await callback_query.message.answer(
f"❌ Нет ключей для синхронизации в кластере {cluster_name}.",
reply_markup=InlineKeyboardBuilder()
.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data="servers_editor"
)
)
.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor"))
.as_markup(),
)
return
@@ -421,36 +371,24 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery):
"Это может занять до 1 минуты, пожалуйста, подождите..."
)
availability_message = (
f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
)
availability_message = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
for server in cluster_servers:
xui = AsyncApi(
server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD
)
xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD)
try:
await xui.login()
online_users = len(await xui.client.online())
availability_message += (
f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
)
availability_message += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
except Exception as e:
availability_message += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"))
await in_progress_message.edit_text(
availability_message, reply_markup=builder.as_markup()
)
await in_progress_message.edit_text(availability_message, reply_markup=builder.as_markup())
await callback_query.answer()
@@ -464,9 +402,7 @@ async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMCo
server = None
cluster_name = None
for cluster, cluster_servers in servers.items():
server = next(
(s for s in cluster_servers if s["server_name"] == server_name), None
)
server = next((s for s in cluster_servers if s["server_name"] == server_name), None)
if server:
cluster_name = cluster
break
@@ -477,16 +413,8 @@ async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMCo
inbound_id = server["inbound_id"]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🗑️ Удалить", callback_data=f"delete_server|{server_name}"
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"
)
)
builder.row(InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"delete_server|{server_name}"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"))
await callback_query.message.answer(
f"<b>🔧 Информация о сервере {server_name}:</b>\n\n"
@@ -505,12 +433,8 @@ async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMCo
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Да", callback_data=f"confirm_delete_server|{server_name}"
),
InlineKeyboardButton(
text="❌ Нет", callback_data=f"manage_server|{server_name}"
),
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_server|{server_name}"),
InlineKeyboardButton(text="❌ Нет", callback_data=f"manage_server|{server_name}"),
)
await callback_query.message.answer(
@@ -520,9 +444,7 @@ async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMCo
@router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter())
async def handle_confirm_delete_server(
callback_query: types.CallbackQuery, state: FSMContext
):
async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext):
server_name = callback_query.data.split("|")[1]
conn = await asyncpg.connect(DATABASE_URL)
@@ -535,15 +457,9 @@ async def handle_confirm_delete_server(
await conn.close()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в управление кластерами", callback_data="servers_editor"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor"))
await callback_query.message.answer(
f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup()
)
await callback_query.message.answer(f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("add_server|"), IsAdminFilter())
@@ -553,9 +469,7 @@ async def handle_add_server(callback_query: types.CallbackQuery, state: FSMConte
await state.update_data(cluster_name=cluster_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor"))
await callback_query.message.answer(
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
+59 -196
View File
@@ -43,9 +43,7 @@ class UserEditorState(StatesGroup):
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 callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_tg_id)
@@ -53,20 +51,14 @@ async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
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 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
):
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
)
user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username)
if not user_record:
builder = InlineKeyboardBuilder()
@@ -79,16 +71,10 @@ async def handle_username_input(
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
)
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
)
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
if balance is None:
builder = InlineKeyboardBuilder()
@@ -103,9 +89,7 @@ async def handle_username_input(
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(
InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")
)
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
builder.row(
InlineKeyboardButton(
@@ -120,21 +104,9 @@ async def handle_username_input(
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=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 = (
@@ -153,9 +125,7 @@ async def handle_username_input(
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 callback_query.message.answer("✉️ Введите текст сообщения, которое вы хотите отправить пользователю.")
await state.set_state(UserEditorState.waiting_for_message_text)
@@ -181,16 +151,10 @@ async def process_send_message(message: types.Message, state: FSMContext, bot: B
@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
)
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
)
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
if balance is None:
builder = InlineKeyboardBuilder()
@@ -205,9 +169,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session:
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(
InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")
)
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
builder.row(
InlineKeyboardButton(
@@ -215,27 +177,15 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session:
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"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=f"send_message_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
@@ -258,15 +208,9 @@ async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any
await restore_trial(tg_id, session)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в меню администратора", callback_data="admin"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin"))
await callback_query.message.answer(
"✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()
)
await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter())
@@ -275,16 +219,12 @@ async def process_balance_change(callback_query: CallbackQuery, state: FSMContex
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 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
):
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"))
@@ -318,7 +258,6 @@ async def handle_new_balance_input(
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
@@ -399,25 +338,19 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any):
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await callback_query.message.answer(
response_message, reply_markup=builder.as_markup()
)
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 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
):
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)
@@ -474,18 +407,14 @@ async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext)
@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
async def handle_expiry_time_input(
message: types.Message, state: FSMContext, session: Any
):
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 message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup())
await state.clear()
return
@@ -499,9 +428,7 @@ async def handle_expiry_time_input(
client_id = await get_client_id_by_email(email)
if client_id is None:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
f"🚫 Клиент с email {email} не найден. 🔍",
reply_markup=builder.as_markup(),
@@ -509,14 +436,10 @@ async def handle_expiry_time_input(
await state.clear()
return
record = await session.fetchrow(
"SELECT server_id FROM keys WHERE client_id = $1", client_id
)
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")
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"🚫 Клиент не найден в базе данных. 🔍",
reply_markup=builder.as_markup(),
@@ -547,7 +470,9 @@ async def handle_expiry_time_input(
await update_key_expiry(client_id, expiry_time)
response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
response_message = (
f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
@@ -565,20 +490,14 @@ async def handle_expiry_time_input(
@router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter())
async def process_callback_delete_key(
callback_query: types.CallbackQuery, session: Any
):
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
)
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()
)
await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup())
return
builder = InlineKeyboardBuilder()
@@ -588,9 +507,7 @@ async def process_callback_delete_key(
callback_data=f"confirm_delete_admin|{client_id}",
)
)
builder.row(
types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor")
)
builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor"))
await callback_query.message.answer(
"<b>❓ Вы уверены, что хотите удалить ключ?</b>",
reply_markup=builder.as_markup(),
@@ -598,13 +515,9 @@ async def process_callback_delete_key(
@router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter())
async def process_callback_confirm_delete(
callback_query: types.CallbackQuery, session: Any
):
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
)
record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id)
if record:
email = record["email"]
@@ -618,74 +531,38 @@ async def process_callback_confirm_delete(
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)
)
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()
)
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()
)
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
):
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
)
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
)
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=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=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 = (
@@ -702,19 +579,13 @@ async def handle_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
):
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=f"delete_user_{tg_id}")],
[InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")],
],
)
@@ -729,9 +600,7 @@ async def confirm_delete_user(
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
)
key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id)
async def delete_keys_from_servers():
try:
@@ -742,9 +611,7 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any):
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}"
)
logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
await delete_keys_from_servers()
@@ -754,13 +621,9 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any):
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
)
await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard)
except Exception as e:
logger.error(
f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}"
)
logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
await callback_query.message.answer(
f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
)
+6 -2
View File
@@ -3,6 +3,10 @@ PAY_2 = "Оплатить"
BACK = "⬅️ Назад"
CUSTOM_SUM = "💰 Ввести свою сумму"
PROFILE = "👤 Личный кабинет"
KEY_CREATION_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
KEY_RENEWAL_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
KEY_CREATION_PAYMENT_MESSAGE = (
"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
)
KEY_RENEWAL_PAYMENT_MESSAGE = (
"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
)
DEFAULT_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей. Перейдите по ссылке для оплаты:"
+16 -22
View File
@@ -3,7 +3,7 @@ from typing import Any
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from aiogram.types import CallbackQuery, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CAPTCHA_EMOJIS
@@ -13,7 +13,7 @@ from logger import logger
router = Router()
async def generate_captcha(state: FSMContext):
async def generate_captcha(message: Message, state: FSMContext):
"""Генерирует новую капчу и сохраняет правильный ответ в состоянии
Пример словаря CAPTCHA_EMOJIS:
@@ -28,48 +28,42 @@ async def generate_captcha(state: FSMContext):
"""
# Выбираем случайный эмодзи и его описание из конфига
correct_emoji, correct_text = random.choice(list(CAPTCHA_EMOJIS.items()))
# Получаем 3 случайных неправильных эмодзи
wrong_emojis = random.sample(
[e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3
)
wrong_emojis = random.sample([e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3)
# Создаем список всех эмодзи и перемешиваем их
all_emojis = [correct_emoji] + wrong_emojis
random.shuffle(all_emojis)
# Сохраняем правильный ответ в состоянии
await state.update_data(correct_emoji=correct_emoji)
await state.update_data(message=message)
# Создаем клавиатуру
builder = InlineKeyboardBuilder()
for emoji in all_emojis:
builder.button(text=emoji, callback_data=f"captcha_{emoji}")
builder.adjust(2, 2)
return {
"text": f"🔒 Для подтверждения что вы не робот,\nвыберите кнопку с {correct_text}",
"markup": builder.as_markup()
"markup": builder.as_markup(),
}
@router.callback_query(F.data.startswith("captcha_"))
async def check_captcha(
callback: CallbackQuery, state: FSMContext, session: Any, admin: bool
):
async def check_captcha(callback: CallbackQuery, state: FSMContext, session: Any, admin: bool):
"""Проверяет ответ пользователя на капчу"""
selected_emoji = callback.data.split("captcha_")[1]
state_data = await state.get_data()
correct_emoji = state_data.get("correct_emoji")
message = state_data.get("message", callback.message)
if selected_emoji == correct_emoji:
logger.info(f"Пользователь {callback.message.chat.id} успешно прошел капчу")
await start_command(callback.message, state, session, admin)
await start_command(message, state, session, admin, False)
else:
logger.warning(
f"Пользователь {callback.message.chat.id} неверно ответил на капчу"
)
captcha = await generate_captcha(state)
await callback.message.answer(
text=captcha["text"], reply_markup=captcha["markup"]
)
logger.warning(f"Пользователь {callback.message.chat.id} неверно ответил на капчу")
captcha = await generate_captcha(message, state)
await callback.message.answer(text=captcha["text"], reply_markup=captcha["markup"])
+2 -6
View File
@@ -18,9 +18,7 @@ router = Router()
@router.callback_query(F.data == "activate_coupon")
async def handle_activate_coupon(
callback_query: types.CallbackQuery, state: FSMContext
):
async def handle_activate_coupon(callback_query: types.CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
@@ -40,9 +38,7 @@ async def process_coupon_code(message: types.Message, state: FSMContext, session
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
activation_result, reply_markup=builder.as_markup()
)
await message.answer(activation_result, reply_markup=builder.as_markup())
await state.clear()
+5 -17
View File
@@ -22,11 +22,7 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext)
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"
)
)
builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"))
builder.row(
InlineKeyboardButton(
text="💰 Ввести сумму доната",
@@ -44,14 +40,10 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext)
@router.callback_query(F.data == "enter_custom_donate_amount")
async def process_enter_donate_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
async def process_enter_donate_amount(callback_query: types.CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate"))
await callback_query.message.answer(
"💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup()
)
await callback_query.message.answer("💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup())
await state.set_state(DonateState.entering_donate_amount)
@@ -60,9 +52,7 @@ async def process_donate_amount_input(message: types.Message, state: FSMContext)
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_XTR <= 0:
await message.answer(
f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:"
)
await message.answer(f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:")
return
await state.update_data(amount=amount)
@@ -99,9 +89,7 @@ async def on_successful_donate(message: types.Message, state: FSMContext):
try:
amount = float(message.successful_payment.invoice_payload.split("_")[0])
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖",
reply_markup=builder.as_markup(),
+7 -27
View File
@@ -29,9 +29,7 @@ async def send_instructions(
if not os.path.isfile(image_path):
if isinstance(callback_query_or_message, types.CallbackQuery):
await callback_query_or_message.message.answer(
"Файл изображения не найден."
)
await callback_query_or_message.message.answer("Файл изображения не найден.")
else:
await callback_query_or_message.answer("Файл изображения не найден.")
return
@@ -71,9 +69,7 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
)
if not record:
await callback_query.message.answer(
"❌ <b>Ключ не найден. Проверьте имя ключа.</b> 🔍"
)
await callback_query.message.answer("❌ <b>Ключ не найден. Проверьте имя ключа.</b> 🔍")
return
key = record["key"]
@@ -81,14 +77,8 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
instruction_message = f"{key_message}{INSTRUCTION_PC}"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"
)
)
builder.row(
InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_MACOS}{key}")
)
builder.row(InlineKeyboardButton(text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"))
builder.row(InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_MACOS}{key}"))
builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
@@ -104,11 +94,7 @@ async def process_connect_tv(callback_query: types.CallbackQuery):
key_name = callback_query.data.split("|")[1]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="▶ Продолжить", callback_data=f"continue_tv|{key_name}"
)
)
builder.row(InlineKeyboardButton(text="▶ Продолжить", callback_data=f"continue_tv|{key_name}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
@@ -146,13 +132,7 @@ async def process_continue_tv(callback_query: types.CallbackQuery):
message_text = SUBSCRIPTION_DETAILS_TEXT.format(subscription_link=subscription_link)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html"
)
)
builder.row(InlineKeyboardButton(text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
text=message_text, reply_markup=builder.as_markup()
)
await callback_query.message.answer(text=message_text, reply_markup=builder.as_markup())
+7 -21
View File
@@ -55,16 +55,12 @@ class Form(StatesGroup):
@router.callback_query(F.data == "create_key")
async def confirm_create_new_key(
callback_query: CallbackQuery, state: FSMContext, session: Any
):
async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} confirmed creation of a new key.")
logger.info(
f"Balance for user {tg_id} is sufficient. Proceeding with key creation."
)
logger.info(f"Balance for user {tg_id} is sufficient. Proceeding with key creation.")
await handle_key_creation(tg_id, state, session, callback_query)
@@ -83,9 +79,7 @@ async def handle_key_creation(
expiry_time = current_time + timedelta(days=TRIAL_TIME)
logger.info(f"Assigned 1-day trial to user {tg_id}.")
await session.execute(
"UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id
)
await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id)
await create_key(tg_id, expiry_time, state, session, message_or_query)
else:
builder = InlineKeyboardBuilder()
@@ -107,9 +101,7 @@ async def handle_key_creation(
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message_or_query.message.answer(
"💳 Выберите тарифный план для создания нового ключа:",
@@ -153,12 +145,8 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
await handle_custom_amount_input(callback_query, session)
else:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay")
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
f"💳 Недостаточно средств. Для продолжения необходимо пополнить баланс на {required_amount}₽.",
@@ -193,9 +181,7 @@ async def create_key(
)
if not existing_key:
break
logger.warning(
f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one."
)
logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.")
client_id = str(uuid.uuid4())
email = key_name.lower()
+9 -31
View File
@@ -33,9 +33,7 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.")
return
if SUPERNODE:
@@ -56,7 +54,7 @@ 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
sub_id=sub_id,
)
if SUPERNODE:
@@ -73,8 +71,6 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time
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()
@@ -95,9 +91,7 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.")
continue
if SUPERNODE:
@@ -108,23 +102,13 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
sub_id = unique_email
tasks.append(
extend_client_key(
xui,
int(inbound_id),
unique_email,
new_expiry_time,
client_id,
total_gb,
sub_id
)
extend_client_key(xui, int(inbound_id), unique_email, new_expiry_time, client_id, total_gb, sub_id)
)
await asyncio.gather(*tasks)
except Exception as e:
logger.error(
f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}"
)
logger.error(f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}")
raise e
@@ -171,9 +155,7 @@ async def delete_key_from_cluster(cluster_id, email, client_id):
await asyncio.gather(*tasks)
except Exception as e:
logger.error(
f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}"
)
logger.error(f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}")
raise e
@@ -212,18 +194,14 @@ 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
sub_id=email,
)
)
await asyncio.gather(*tasks)
logger.info(
f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}"
)
logger.info(f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}")
except Exception as e:
logger.error(
f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}"
)
logger.error(f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}")
raise e
+32 -88
View File
@@ -86,9 +86,7 @@ async def process_callback_or_message_view_keys(
inline_keyboard, response_message = build_keys_response(records)
image_path = os.path.join("img", "pic_keys.jpg")
await send_with_optional_image(
send_message, send_photo, image_path, response_message, inline_keyboard
)
await send_with_optional_image(send_message, send_photo, image_path, response_message, inline_keyboard)
except Exception as e:
error_message = f"Ошибка при получении ключей: {e}"
await send_message(text=error_message)
@@ -105,15 +103,10 @@ def build_keys_response(records):
key_name = record["email"]
expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000).strftime("%d.%m.%Y")
builder.row(
InlineKeyboardButton(
text=f"🔑 {key_name} (до {expiry_date})",
callback_data=f"view_key|{key_name}"
)
InlineKeyboardButton(text=f"🔑 {key_name} (до {expiry_date})", callback_data=f"view_key|{key_name}")
)
builder.row(
InlineKeyboardButton(text=" Добавить подписку", callback_data="create_key")
)
builder.row(InlineKeyboardButton(text=" Добавить подписку", callback_data="create_key"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
@@ -125,18 +118,14 @@ def build_keys_response(records):
return inline_keyboard, response_message
async def send_with_optional_image(
send_message, send_photo, image_path, text, keyboard
):
async def send_with_optional_image(send_message, send_photo, image_path, text, keyboard):
"""
Отправляет сообщение с изображением, если файл существует. В противном случае отправляет только текст.
"""
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await send_photo(
photo=BufferedInputFile(
image_file.read(), filename=os.path.basename(image_path)
),
photo=BufferedInputFile(image_file.read(), filename=os.path.basename(image_path)),
caption=text,
reply_markup=keyboard,
)
@@ -184,9 +173,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
)
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)
builder = InlineKeyboardBuilder()
@@ -200,51 +187,29 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
builder.row(
InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS),
InlineKeyboardButton(
text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID
),
InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{key}"),
InlineKeyboardButton(
text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key}"
),
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key}"),
)
builder.row(
InlineKeyboardButton(
text=PC_BUTTON, callback_data=f"connect_pc|{key_name}"
),
InlineKeyboardButton(
text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"
),
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{key_name}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{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}"
),
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="view_keys"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
builder.row(InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
keyboard = builder.as_markup()
@@ -273,9 +238,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
@router.callback_query(F.data.startswith("update_subscription|"))
async def process_callback_update_subscription(
callback_query: types.CallbackQuery, session: Any
):
async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
email = callback_query.data.split("|")[1]
try:
@@ -335,9 +298,7 @@ async def process_callback_update_subscription(
else:
await callback_query.message.answer("<b>Ключ не найден в базе данных.</b>")
except Exception as e:
await handle_error(
tg_id, callback_query, f"Ошибка при обновлении подписки: {e}"
)
await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}")
@router.callback_query(F.data.startswith("delete_key|"))
@@ -352,11 +313,7 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery):
callback_data=f"confirm_delete|{client_id}",
)
],
[
types.InlineKeyboardButton(
text="❌ Нет, отменить", callback_data="view_keys"
)
],
[types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys")],
]
)
@@ -391,43 +348,39 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
builder.row(
InlineKeyboardButton(
text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)',
text=f"📅 1 месяц ({RENEWAL_PLANS['1']['price']} руб.)",
callback_data=f"renew_plan|1|{client_id}",
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.) {DISCOUNTS["3"]}% скидка',
text=f"📅 3 месяца ({RENEWAL_PLANS['3']['price']} руб.) {DISCOUNTS['3']}% скидка",
callback_data=f"renew_plan|3|{client_id}",
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.) {DISCOUNTS["6"]}% скидка',
text=f"📅 6 месяцев ({RENEWAL_PLANS['6']['price']} руб.) {DISCOUNTS['6']}% скидка",
callback_data=f"renew_plan|6|{client_id}",
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.) ({DISCOUNTS["12"]}% 🔥)',
text=f"📅 12 месяцев ({RENEWAL_PLANS['12']['price']} руб.) ({DISCOUNTS['12']}% 🔥)",
callback_data=f"renew_plan|12|{client_id}",
)
)
back_button = InlineKeyboardButton(
text="🔙 Назад", callback_data="view_keys"
)
back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys")
builder.row(back_button)
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"
),
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
)
await callback_query.message.answer(
@@ -441,21 +394,15 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
@router.callback_query(F.data.startswith("confirm_delete|"))
async def process_callback_confirm_delete(
callback_query: types.CallbackQuery, session: Any
):
async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any):
email = callback_query.data.split("|")[1]
try:
record = await session.fetchrow(
"SELECT client_id FROM keys WHERE email = $1", email
)
record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email)
if record:
client_id = record["client_id"]
response_message = "Ключ успешно удален."
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_keys"
)
back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await delete_key(client_id)
@@ -470,9 +417,7 @@ async def process_callback_confirm_delete(
try:
tasks = []
for cluster_id, cluster in servers.items():
tasks.append(
delete_key_from_cluster(cluster_id, email, client_id)
)
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
@@ -485,9 +430,7 @@ async def process_callback_confirm_delete(
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_keys"
)
back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await callback_query.message.answer(
@@ -529,7 +472,9 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi
if balance < cost:
required_amount = cost - balance
logger.info(f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}")
logger.info(
f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}"
)
await save_temporary_data(
session,
@@ -610,4 +555,3 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.")
await renew_key_on_servers()
+19 -40
View File
@@ -13,6 +13,7 @@ from logger import logger
# Глобальная переменная для пула соединений
db_pool = None
async def init_db_pool():
"""
Инициализация пула соединений, если он ещё не создан.
@@ -21,6 +22,7 @@ async def init_db_pool():
if not db_pool:
db_pool = await asyncpg.create_pool(dsn=DATABASE_URL, min_size=5, max_size=20)
async def fetch_url_content(url, tg_id):
try:
logger.info(f"Получение URL: {url} для tg_id: {tg_id}")
@@ -32,9 +34,7 @@ async def fetch_url_content(url, tg_id):
logger.info(f"Успешно получен контент с {url} для tg_id: {tg_id}")
return base64.b64decode(content).decode("utf-8").split("\n")
else:
logger.error(
f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}"
)
logger.error(f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}")
return []
except asyncio.TimeoutError:
logger.error(f"Таймаут при получении {url} для tg_id: {tg_id}")
@@ -43,6 +43,7 @@ async def fetch_url_content(url, tg_id):
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}")
@@ -51,9 +52,7 @@ async def combine_unique_lines(urls, tg_id, query_string):
url_with_query = f"{urls[0]}?{query_string}" if query_string else urls[0]
return await fetch_url_content(url_with_query, tg_id)
logger.info(
f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}"
)
logger.info(f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}")
urls_with_query = [f"{url}?{query_string}" if query_string else url for url in urls]
logger.info(f"Составлены URL-адреса: {urls_with_query}")
@@ -65,19 +64,17 @@ async def combine_unique_lines(urls, tg_id, query_string):
for lines in results:
all_lines.update(filter(None, lines))
logger.info(
f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}"
)
logger.info(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}")
return list(all_lines)
transition_date = datetime.strptime(TRANSITION_DATE_STR, "%Y-%m-%d %H:%M:%S")
transition_timestamp_ms = int(transition_date.timestamp() * 1000)
transition_timestamp_ms_adjusted = transition_timestamp_ms - (3 * 60 * 60 * 1000)
logger.info(
f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}"
)
logger.info(f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}")
async def handle_old_subscription(request):
email = request.match_info.get("email")
@@ -95,9 +92,7 @@ async def handle_old_subscription(request):
await init_db_pool()
async with db_pool.acquire() as conn:
key_info = await conn.fetchrow(
"SELECT created_at, server_id FROM keys WHERE email = $1", email
)
key_info = await conn.fetchrow("SELECT created_at, server_id FROM keys WHERE email = $1", email)
if not key_info:
logger.warning(f"Клиент с email {email} не найден в базе.")
@@ -115,14 +110,10 @@ async def handle_old_subscription(request):
status=400,
)
logger.info(
f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}"
)
logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}")
created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000)
logger.info(
f"Время создания клиента в формате datetime (UTC): {created_at_datetime}"
)
logger.info(f"Время создания клиента в формате datetime (UTC): {created_at_datetime}")
if created_at_ms >= transition_timestamp_ms_adjusted:
logger.info(f"Клиент с email {email} является новым.")
@@ -135,23 +126,18 @@ async def handle_old_subscription(request):
cluster_servers = servers.get(cluster_name, [])
logger.info(f"Сервера в кластере: {cluster_servers}")
urls = [
f"{server['subscription_url']}/{email}" for server in cluster_servers
]
urls = [f"{server['subscription_url']}/{email}" for server in cluster_servers]
combined_subscriptions = await combine_unique_lines(urls, email, "")
base64_encoded = base64.b64encode(
"\n".join(combined_subscriptions).encode("utf-8")
).decode("utf-8")
base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8")
encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}"
headers = {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
"profile-title": "base64:"
+ base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
@@ -175,9 +161,7 @@ async def handle_new_subscription(request):
await init_db_pool()
async with db_pool.acquire() as conn:
client_data = await conn.fetchrow(
"SELECT tg_id, server_id FROM keys WHERE email = $1", email
)
client_data = await conn.fetchrow("SELECT tg_id, server_id FROM keys WHERE email = $1", email)
if not client_data:
logger.warning(f"Клиент с email {email} не найден в базе.")
@@ -199,18 +183,14 @@ async def handle_new_subscription(request):
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
urls = [
f"{server['subscription_url']}/{email}" for server in cluster_servers
]
urls = [f"{server['subscription_url']}/{email}" for server in cluster_servers]
query_string = request.query_string
logger.info(f"Извлечен query string: {query_string}")
combined_subscriptions = await combine_unique_lines(urls, tg_id, query_string)
base64_encoded = base64.b64encode(
"\n".join(combined_subscriptions).encode("utf-8")
).decode("utf-8")
base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8")
encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}"
@@ -218,8 +198,7 @@ async def handle_new_subscription(request):
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
"profile-title": "base64:"
+ base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
+2 -4
View File
@@ -16,9 +16,7 @@ from logger import logger
async def create_trial_key(tg_id: int, session: Any):
try:
trial_status = await session.fetchval(
"SELECT trial FROM connections WHERE tg_id = $1", tg_id
)
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:
@@ -67,7 +65,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
sub_id=base_email,
)
)
+21 -42
View File
@@ -36,6 +36,7 @@ from logger import logger
router = Router()
async def periodic_expired_keys_check(bot: Bot):
"""Периодическая проверка истекших ключей с кастомным интервалом."""
while True:
@@ -54,7 +55,6 @@ async def periodic_expired_keys_check(bot: Bot):
await asyncio.sleep(EXPIRED_KEYS_CHECK_INTERVAL)
async def notify_expiring_keys(bot: Bot):
conn = None
try:
@@ -84,21 +84,16 @@ async def notify_expiring_keys(bot: Bot):
logger.info("Соединение с базой данных закрыто.")
async def is_bot_blocked(bot: Bot, chat_id: int) -> bool:
if DEV_MODE:
return False
try:
member = await bot.get_chat_member(chat_id, bot.id)
blocked = member.status == "left"
logger.info(
f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}"
)
logger.info(f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}")
return blocked
except Exception as e:
logger.warning(
f"Не удалось проверить статус бота для пользователя {chat_id}: {e}"
)
logger.warning(f"Не удалось проверить статус бота для пользователя {chat_id}: {e}")
return False
@@ -137,7 +132,11 @@ async def process_10h_record(record, bot, conn):
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}"
"Ключ истек"
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(
@@ -200,7 +199,6 @@ async def notify_24h_keys(
logger.info("Обработка всех уведомлений за 24 часа завершена.")
async def process_24h_record(record, bot, conn):
tg_id = record["tg_id"]
email = record["email"]
@@ -213,7 +211,11 @@ async def process_24h_record(record, bot, conn):
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}"
"Ключ истек"
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(
@@ -287,9 +289,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
username = user.get("username", "Пользователь")
try:
can_notify = await check_notification_time(
tg_id, "inactive_trial", hours=24, session=conn
)
can_notify = await check_notification_time(tg_id, "inactive_trial", hours=24, session=conn)
if can_notify:
builder = InlineKeyboardBuilder()
@@ -299,11 +299,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
callback_data="create_key",
)
)
builder.row(
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
)
builder.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
keyboard = builder.as_markup()
message = (
@@ -315,20 +311,14 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
try:
await bot.send_message(tg_id, message, reply_markup=keyboard)
logger.info(
f"Отправлено уведомление неактивному пользователю {tg_id}."
)
logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.")
await add_notification(tg_id, "inactive_trial", session=conn)
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users."
)
logger.warning(f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users.")
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(
f"Ошибка при отправке уведомления пользователю {tg_id}: {e}"
)
logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
except Exception as e:
logger.error(f"Ошибка при обработке пользователя {tg_id}: {e}")
@@ -378,13 +368,7 @@ async def process_key(record, bot, conn):
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
]
]
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
)
try:
@@ -446,15 +430,12 @@ async def process_key(record, bot, conn):
logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
async def check_online_users():
servers = await get_servers_from_db()
for cluster_id, cluster in servers.items():
for server_id, server in enumerate(cluster):
xui = AsyncApi(
server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD
)
xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD)
await xui.login()
try:
online_users = len(await xui.client.online())
@@ -462,6 +443,4 @@ async def check_online_users():
f"Сервер '{server['server_name']}' доступен, текущее количество активных пользователей: {online_users}."
)
except Exception as e:
logger.error(
f"Не удалось проверить пользователей на сервере {server_id}: {e}"
)
logger.error(f"Не удалось проверить пользователей на сервере {server_id}: {e}")
+2 -8
View File
@@ -54,15 +54,9 @@ async def handle_pay(callback_query: CallbackQuery):
)
)
builder.row(
InlineKeyboardButton(
text="🎟️ Активировать купон", callback_data="activate_coupon"
)
)
builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon"))
if DONATIONS_ENABLE:
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
)
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
+19 -28
View File
@@ -52,9 +52,7 @@ if ROBOKASSA_ENABLE:
def generate_payment_link(amount, inv_id, description, tg_id):
"""Генерация ссылки на оплату."""
logger.debug(
f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}"
)
logger.debug(f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}")
payment_link = robokassa._payment.link.generate_by_script(
out_sum=amount,
inv_id=inv_id,
@@ -66,9 +64,7 @@ def generate_payment_link(amount, inv_id, description, tg_id):
@router.callback_query(F.data == "pay_robokassa")
async def process_callback_pay_robokassa(
callback_query: types.CallbackQuery, state: FSMContext, session: Any
):
async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated Robokassa payment.")
@@ -78,18 +74,18 @@ async def process_callback_pay_robokassa(
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i + 1]['callback_data']}",
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
)
)
builder.row(
@@ -117,9 +113,7 @@ async def process_callback_pay_robokassa(
@router.callback_query(F.data.startswith("robokassa_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
logger.info(f"Получены данные callback_data: {callback_query.data}")
data = callback_query.data.split("|")
@@ -173,9 +167,7 @@ async def robokassa_webhook(request):
shp_id = params.get("shp_id")
signature_value = params.get("SignatureValue")
logger.info(
f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}"
)
logger.info(f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}")
if not check_payment_signature(params):
logger.error("Неверная подпись или данные запроса.")
@@ -214,9 +206,7 @@ def check_payment_signature(params):
logger.info(f"Signature string before hashing: {signature_string}")
expected_signature = (
hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
)
expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
logger.info(f"Expected signature: {expected_signature}")
logger.info(f"Received signature: {signature_value}")
@@ -225,9 +215,7 @@ def check_payment_signature(params):
@router.callback_query(F.data == "enter_custom_amount_robokassa")
async def process_custom_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} chose to enter a custom amount.")
@@ -239,13 +227,13 @@ async def process_custom_amount_selection(
reply_markup=builder.as_markup(),
)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_robokassa
)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input(message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None):
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:
@@ -255,7 +243,6 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer
inv_id = 0
try:
conn = await asyncpg.connect(DATABASE_URL)
user_data = await get_temporary_data(conn, tg_id)
await conn.close()
@@ -283,9 +270,13 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer
)
if state_type == "waiting_for_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
message_text = (
f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
)
elif state_type == "waiting_for_renewal_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
message_text = (
f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
)
else:
await message.answer("Некорректное состояние данных. Попробуйте снова.")
return
+9 -19
View File
@@ -51,9 +51,7 @@ async def process_callback_view_profile(
try:
trial_status = await get_trial(chat_id, conn)
profile_message = profile_message_send(
username, chat_id, int(balance), key_count
)
profile_message = profile_message_send(username, chat_id, int(balance), key_count)
if key_count == 0:
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Подписка, чтобы настроить VPN-подключение</i></pre>"
@@ -81,9 +79,7 @@ async def process_callback_view_profile(
InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"),
)
if admin:
builder.row(
InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")
)
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start"))
if os.path.isfile(image_path):
@@ -122,10 +118,8 @@ async def balance_handler(callback_query: types.CallbackQuery):
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()
)
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):
@@ -145,10 +139,10 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session:
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')
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"
@@ -158,11 +152,7 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session:
else:
history_text = "❌ У вас пока нет операций с балансом."
await callback_query.message.answer(
history_text,
reply_markup=builder.as_markup()
)
await callback_query.message.answer(history_text, reply_markup=builder.as_markup())
@router.message(F.text == "/tariffs")
+32 -76
View File
@@ -13,6 +13,7 @@ from aiogram.types import (
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CAPTCHA_ENABLE,
CHANNEL_EXISTS,
CHANNEL_URL,
CONNECT_ANDROID,
@@ -21,7 +22,6 @@ from config import (
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
SUPPORT_CHAT_URL,
CAPTCHA_ENABLE,
)
from database import (
add_connection,
@@ -38,32 +38,32 @@ from handlers.buttons.add_subscribe import (
PC_BUTTON,
TV_BUTTON,
)
from handlers.captcha import generate_captcha
from handlers.keys.key_management import create_key
from handlers.keys.trial_key import create_trial_key
from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn
from logger import logger
from handlers.captcha import generate_captcha
router = Router()
@router.callback_query(F.data == "start")
async def handle_start_callback_query(
callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool
callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool, captcha: bool = False
):
await start_command(callback_query.message, state, session, admin)
await start_command(callback_query.message, state, session, admin, captcha)
@router.message(Command("start"))
async def start_command(message: Message, state: FSMContext, session: Any, admin: bool):
async def start_command(message: Message, state: FSMContext, session: Any, admin: bool, captcha: bool = True):
"""Обрабатывает команду /start, включает логику рефералов и подарков."""
logger.info(f"Вызвана функция start_command для пользователя {message.chat.id}")
await state.clear()
# Проверка капчи, если включена
if CAPTCHA_ENABLE:
captcha = await generate_captcha(state)
if CAPTCHA_ENABLE and captcha:
captcha = await generate_captcha(message, state)
await message.answer(text=captcha["text"], reply_markup=captcha["markup"])
return
@@ -74,9 +74,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
if not connection_exists:
await add_connection(tg_id=message.chat.id, session=session)
logger.info(
f"Пользователь {message.chat.id} успешно добавлен в базу данных."
)
logger.info(f"Пользователь {message.chat.id} успешно добавлен в базу данных.")
if "gift_" in message.text:
logger.info(f"Обнаружена ссылка на подарок: {message.text}")
@@ -91,36 +89,24 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
)
if gift_info is None:
logger.warning(
f"Подарок с ID {gift_id} уже был использован или не существует."
)
await message.answer(
"Этот подарок уже был использован или не существует."
)
logger.warning(f"Подарок с ID {gift_id} уже был использован или не существует.")
await message.answer("Этот подарок уже был использован или не существует.")
return await show_start_menu(message, admin, session)
if gift_info["sender_tg_id"] == recipient_tg_id:
logger.warning(
f"Пользователь {recipient_tg_id} попытался активировать подарок, который был отправлен им самим."
)
await message.answer(
"❌ Вы не можете получить подарок от самого себя."
)
await message.answer("❌ Вы не можете получить подарок от самого себя.")
return await show_start_menu(message, admin, session)
selected_months = gift_info["selected_months"]
expiry_time = gift_info["expiry_time"]
expiry_time_naive = expiry_time.replace(tzinfo=None)
logger.info(
f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}."
)
logger.info(f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}.")
await create_key(
recipient_tg_id, expiry_time_naive, state, session, message
)
logger.info(
f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев."
)
await create_key(recipient_tg_id, expiry_time_naive, state, session, message)
logger.info(f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев.")
await session.execute(
"UPDATE gifts SET is_used = TRUE, recipient_tg_id = $1 WHERE gift_id = $2",
@@ -131,9 +117,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
await message.answer(
f"🎉 Ваш подарок на {selected_months} {'месяц' if selected_months == 1 else 'месяца' if selected_months in [2, 3, 4] else 'месяцев'} активирован!"
)
logger.info(
f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}."
)
logger.info(f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}.")
return
elif "referral_" in message.text:
@@ -141,21 +125,13 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
referrer_tg_id = int(message.text.split("referral_")[1])
if connection_exists:
logger.info(
f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом."
)
await message.answer(
"❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку."
)
logger.info(f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом.")
await message.answer("❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку.")
return await show_start_menu(message, admin, session)
if referrer_tg_id == message.chat.id:
logger.warning(
f"Пользователь {message.chat.id} попытался стать рефералом самого себя."
)
await message.answer(
"❌ Вы не можете быть рефералом самого себя."
)
logger.warning(f"Пользователь {message.chat.id} попытался стать рефералом самого себя.")
await message.answer("❌ Вы не можете быть рефералом самого себя.")
return await show_start_menu(message, admin, session)
existing_referral = await session.fetchrow(
@@ -168,9 +144,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
return await show_start_menu(message, admin, session)
await add_referral(message.chat.id, referrer_tg_id, session)
logger.info(
f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}"
)
logger.info(f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}")
return await show_start_menu(message, admin, session)
except (ValueError, IndexError) as e:
@@ -178,16 +152,12 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
return
else:
logger.info(
f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка."
)
logger.info(f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка.")
await show_start_menu(message, admin, session)
except (ValueError, IndexError) as e:
logger.error(
f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}"
)
logger.error(f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}")
await message.answer("❌ Произошла ошибка. Пожалуйста, попробуйте снова.")
else:
await show_start_menu(message, admin, session)
@@ -202,26 +172,20 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
builder = InlineKeyboardBuilder()
if trial_status == 0:
builder.row(
InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")
)
builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if CHANNEL_EXISTS:
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL)
InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL),
)
else:
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL)
)
builder.row(InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL))
if admin:
builder.row(
InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")
)
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text="🌐 О VPN", callback_data="about_vpn"))
@@ -267,24 +231,20 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any):
builder.row(
InlineKeyboardButton(
text=IMPORT_IOS,
url=f'{CONNECT_IOS}{trial_key_info["key"]}',
url=f"{CONNECT_IOS}{trial_key_info['key']}",
),
InlineKeyboardButton(
text=IMPORT_ANDROID,
url=f'{CONNECT_ANDROID}{trial_key_info["key"]}',
url=f"{CONNECT_ANDROID}{trial_key_info['key']}",
),
)
builder.row(
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"),
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
key_message, reply_markup=builder.as_markup()
)
await callback_query.message.answer(key_message, reply_markup=builder.as_markup())
@router.callback_query(F.data == "about_vpn")
@@ -292,9 +252,7 @@ async def handle_about_vpn(callback_query: CallbackQuery):
builder = InlineKeyboardBuilder()
if DONATIONS_ENABLE:
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
)
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
@@ -305,6 +263,4 @@ async def handle_about_vpn(callback_query: CallbackQuery):
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start"))
await callback_query.message.answer(
get_about_vpn("3.2.3-minor"), reply_markup=builder.as_markup()
)
await callback_query.message.answer(get_about_vpn("3.2.3-minor"), reply_markup=builder.as_markup())
+3 -1
View File
@@ -12,7 +12,9 @@ router = Router()
@router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=KICKED))
async def user_blocked_bot(event: ChatMemberUpdated, session: Any):
logger.info(f"User {event.from_user.id} blocked the bot.")
await session.execute("INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", event.from_user.id)
await session.execute(
"INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", event.from_user.id
)
@router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=MEMBER))
+3 -9
View File
@@ -14,9 +14,7 @@ from logger import logger
async def get_usd_rate():
try:
async with aiohttp.ClientSession() as session:
async with session.get(
"https://www.cbr-xml-daily.ru/daily_json.js"
) as response:
async with session.get("https://www.cbr-xml-daily.ru/daily_json.js") as response:
if response.status == 200:
data = await response.text()
usd = float(json.loads(data)["Valute"]["USD"]["Value"])
@@ -86,9 +84,7 @@ async def get_least_loaded_cluster() -> str:
return least_loaded_cluster
async def handle_error(
tg_id: int, callback_query: object | None = None, message: str = ""
) -> None:
async def handle_error(tg_id: int, callback_query: object | None = None, message: str = "") -> None:
"""
Обрабатывает ошибку, отправляя сообщение пользователю.
@@ -100,9 +96,7 @@ async def handle_error(
try:
if callback_query and hasattr(callback_query, "message"):
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception as delete_error:
logger.warning(f"Не удалось удалить сообщение: {delete_error}")
+1 -3
View File
@@ -16,9 +16,7 @@ class DeleteMessageMiddleware(BaseMiddleware):
if isinstance(event, Message):
if not event.text or not event.text.startswith("/start"):
try:
await event.bot.delete_message(
event.chat.id, event.message_id - 1
)
await event.bot.delete_message(event.chat.id, event.message_id - 1)
except Exception:
pass
await event.delete()
+1 -3
View File
@@ -59,8 +59,6 @@ class ThrottlingMiddleware(BaseMiddleware):
)
self.caches[key][user.id] = None
else:
logger.debug(
f"No throttling key provided for user {user.id}, proceeding without throttle."
)
logger.debug(f"No throttling key provided for user {user.id}, proceeding without throttle.")
return await handler(event, data)
+1 -1
View File
@@ -14,7 +14,7 @@ exclude = [
[tool.ruff.format]
quote-style = "double"
indent-style = "tab"
indent-style = "space"
[tool.darker]
src = ["."]
+13 -42
View File
@@ -16,9 +16,7 @@ try:
from config import CLUSTERS
except ImportError:
CLUSTERS = None
logger.warning(
"Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!"
)
logger.warning("Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!")
async def sync_servers_with_db():
@@ -27,9 +25,7 @@ async def sync_servers_with_db():
Если CLUSTERS не найден, синхронизация не будет выполнена.
"""
if CLUSTERS is None:
logger.info(
"Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена."
)
logger.info("Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена.")
return
try:
@@ -59,13 +55,9 @@ async def sync_servers_with_db():
server_info["SUBSCRIPTION"],
server_info["INBOUND_ID"],
)
logger.info(
f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных."
)
logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных.")
else:
logger.info(
f"Сервер {server_info['name']} из кластера {cluster_name} уже существует."
)
logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} уже существует.")
except Exception as e:
logger.error(f"Ошибка при синхронизации серверов: {e}")
@@ -104,25 +96,14 @@ async def notify_admin(server_name: str):
current_time = datetime.now()
last_notification_time = last_notification_times.get(server_name)
if (
last_notification_time
and current_time - last_notification_time < timedelta(minutes=3)
):
logger.info(
f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут."
)
if last_notification_time and current_time - last_notification_time < timedelta(minutes=3):
logger.info(f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут.")
return
logger.info(
f"Отправка уведомлений администратору о недоступности сервера {server_name}..."
)
logger.info(f"Отправка уведомлений администратору о недоступности сервера {server_name}...")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Управление сервером", callback_data=f"manage_server|{server_name}"
)
)
builder.row(InlineKeyboardButton(text="Управление сервером", callback_data=f"manage_server|{server_name}"))
for admin_id in ADMIN_ID:
await bot.send_message(
@@ -134,9 +115,7 @@ async def notify_admin(server_name: str):
),
reply_markup=builder.as_markup(),
)
logger.info(
f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}."
)
logger.info(f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}.")
last_notification_times[server_name] = current_time
except Exception as e:
@@ -160,9 +139,7 @@ async def check_servers():
server_name = server["server_name"]
server_host = extract_host(original_api_url)
logger.debug(
f"Проверка доступности сервера '{server_name}' с хостом {server_host}"
)
logger.debug(f"Проверка доступности сервера '{server_name}' с хостом {server_host}")
is_online = await ping_server(server_host)
@@ -170,18 +147,12 @@ async def check_servers():
last_ping_times[server_name] = current_time
else:
last_ping_time = last_ping_times.get(server_name)
if last_ping_time and current_time - last_ping_time > timedelta(
minutes=3
):
logger.warning(
f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление."
)
if last_ping_time and current_time - last_ping_time > timedelta(minutes=3):
logger.warning(f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление.")
await notify_admin(server_name)
elif not last_ping_time:
last_ping_times[server_name] = current_time
logger.info(
f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован."
)
logger.info(f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован.")
logger.info("Завершена проверка всех серверов.")
await asyncio.sleep(PING_TIME)