Merge pull request #139 from izzzzzi/main

Fix
This commit is contained in:
Vladislav Lisitsyn
2025-01-31 00:09:44 +03:00
committed by GitHub
54 changed files with 2255 additions and 2258 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
+3 -2
View File
@@ -54,5 +54,6 @@ scripts
models.py
Dockerfile
.csv
setup.py
/logs
setup.py
.ruff_cache
+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
+12 -10
View File
@@ -29,8 +29,12 @@ async def backup_database() -> Exception | None:
return e
def _create_database_backup() -> (str | None, Exception | None):
def _create_database_backup() -> tuple[str | None, Exception | None]:
date_formatted = datetime.now().strftime("%Y-%m-%d-%H%M%S")
if not os.path.exists(BACK_DIR):
os.makedirs(BACK_DIR)
filename = os.path.join(BACK_DIR, f"{DB_NAME}-backup-{date_formatted}.sql")
try:
@@ -93,18 +97,16 @@ async def create_backup_and_send_to_admins(xui) -> None:
async def _send_backup_to_admins(backup_file_path: str) -> None:
try:
import aiofiles
from bot import bot
with open(backup_file_path, "rb") as backup_file:
backup_input_file = BufferedInputFile(
file=backup_file.read(),
filename=os.path.basename(backup_file_path)
)
async with aiofiles.open(backup_file_path, "rb") as backup_file:
backup_data = await backup_file.read()
backup_input_file = BufferedInputFile(file=backup_data, filename=os.path.basename(backup_file_path))
admin_ids = ADMIN_ID if isinstance(ADMIN_ID, list) else [ADMIN_ID]
for admin_id in admin_ids:
await bot.send_document(
chat_id=admin_id,
document=backup_input_file
)
await bot.send_document(chat_id=admin_id, document=backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {admin_id}")
except Exception as e:
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
+36 -31
View File
@@ -3,16 +3,16 @@ import traceback
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from aiogram.filters import ExceptionTypeFilter
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import ErrorEvent
from aiogram.types import BufferedInputFile, ErrorEvent
from aiogram.utils.markdown import hbold
from config import API_TOKEN
from config import ADMIN_ID, API_TOKEN
from filters.private import IsPrivateFilter
from logger import logger
from middlewares.admin import AdminMiddleware
from middlewares.database import DatabaseMiddleware
from middlewares.delete import DeleteMessageMiddleware
from middlewares.logging import LoggingMiddleware
from middlewares.user import UserMiddleware
from middlewares import register_middleware
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
storage = MemoryStorage()
@@ -20,30 +20,35 @@ dp = Dispatcher(bot=bot, storage=storage)
version = "4.0.0-preAlpha(14-dev)"
dp.message.middleware(LoggingMiddleware())
dp.callback_query.middleware(LoggingMiddleware())
register_middleware(dp)
dp.message.middleware(AdminMiddleware())
dp.callback_query.middleware(AdminMiddleware())
dp.message.middleware(UserMiddleware())
dp.callback_query.middleware(UserMiddleware())
dp.message.middleware(DatabaseMiddleware())
dp.callback_query.middleware(DatabaseMiddleware())
# dp.message.middleware(ThrottlingMiddleware(limit=1))
# dp.callback_query.middleware(ThrottlingMiddleware(limit=1))
dp.message.outer_middleware(DeleteMessageMiddleware())
dp.callback_query.outer_middleware(DeleteMessageMiddleware())
dp.message.filter(IsPrivateFilter())
dp.callback_query.filter(IsPrivateFilter())
@dp.error()
async def error_handler(event: ErrorEvent):
logger.error(
"Ошибка в боте:\n"
f"Исключение: {event.exception}\n"
f"Тип: {type(event.exception)}\n"
f"Update: {event.update}\n"
f"Трассировка:\n{traceback.format_exc()}"
)
@dp.errors(ExceptionTypeFilter(Exception))
async def errors_handler(
event: ErrorEvent,
bot: Bot,
) -> bool:
if isinstance(event.exception, TelegramForbiddenError):
logger.info(f"User {event.update.message.from_user.id} blocked the bot.")
return True
logger.exception(f"Update: {event.update}\nException: {event.exception}")
if not ADMIN_ID:
return True
try:
for admin_id in ADMIN_ID:
await bot.send_document(
chat_id=admin_id,
document=BufferedInputFile(
traceback.format_exc().encode(),
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:
logger.error(f"Unexpected error in error handler: {exception}")
return True
+83 -55
View File
@@ -1,80 +1,101 @@
from dataclasses import dataclass
from typing import Any
import py3xui
from config import LIMIT_IP, SUPERNODE
from logger import logger
async def add_client(
xui,
client_id: str,
email: str,
tg_id: str,
limit_ip: int,
total_gb: int,
expiry_time: int,
enable: bool,
flow: str,
inbound_id: int,
sub_id
):
@dataclass
class ClientConfig:
"""Конфигурация клиента для добавления/обновления."""
client_id: str
email: str
tg_id: str
limit_ip: int
total_gb: int
expiry_time: int
enable: bool
flow: str
inbound_id: int
sub_id: str
async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, Any]:
"""
Adds a client to the server via 3x-ui.
Добавляет клиента на сервер через 3x-ui.
Args:
xui: Экземпляр API клиента
config: Конфигурация клиента
Returns:
Dict[str, Any]: Результат операции в формате
{'status': 'success'|'failed'|'duplicate', 'error': str, 'email': str}
"""
try:
await xui.login()
client = py3xui.Client(
id=client_id,
email=email.lower(),
limit_ip=limit_ip,
total_gb=total_gb,
expiry_time=expiry_time,
enable=enable,
tg_id=tg_id,
sub_id=sub_id,
flow=flow,
id=config.client_id,
email=config.email.lower(),
limit_ip=config.limit_ip,
total_gb=config.total_gb,
expiry_time=config.expiry_time,
enable=config.enable,
tg_id=config.tg_id,
sub_id=config.sub_id,
flow=config.flow,
)
response = await xui.client.add(inbound_id, [client])
logger.info(f"Клиент {email} успешно добавлен с ID {client_id}.")
response = await xui.client.add(config.inbound_id, [client])
logger.info(f"Клиент {config.email} успешно добавлен с ID {config.client_id}")
return response if response else {"status": "failed"}
except Exception as e:
error_message = str(e)
if "Duplicate email" in error_message:
logger.warning(
f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}"
)
return {"status": "duplicate", "email": email}
logger.warning(f"Дублированный email: {config.email}. Пропуск. Сообщение: {error_message}")
return {"status": "duplicate", "email": config.email}
logger.error(f"Ошибка при добавлении клиента {email}: {error_message}")
logger.error(f"Ошибка при добавлении клиента {config.email}: {error_message}")
return {"status": "failed", "error": error_message}
async def extend_client_key(
xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id = str
):
xui: py3xui.AsyncApi, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str
) -> bool | None:
"""
Функция для обновления срока действия ключа клиента по email.
Обновляет срок действия ключа клиента.
Args:
xui: Экземпляр API клиента
inbound_id: ID входящего соединения
email: Email клиента
new_expiry_time: Новое время истечения
client_id: ID клиента
total_gb: Общий объем трафика
sub_id: ID подписки
Returns:
Optional[bool]: True если успешно, False если ошибка, None если клиент не найден
"""
await xui.login()
try:
await xui.login()
client = await xui.client.get_by_email(email)
if not client:
logger.warning(f"Клиент с email {email} не найден.")
return
logger.warning(f"Клиент с email {email} не найден")
return None
if not client.id:
logger.warning(f"Ошибка: клиент {email} не имеет действительного ID.")
return
logger.warning(f"Ошибка: клиент {email} не имеет действительного ID")
return None
logger.info(
f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}"
)
logger.info(f"Обновление ключа клиента {email} с ID {client.id} до {new_expiry_time}")
client.id = client_id
client.expiry_time = new_expiry_time
@@ -87,41 +108,48 @@ 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"Ключ клиента {email} успешно продлён до {new_expiry_time}")
return True
except Exception as e:
logger.error(f"Ошибка при обновлении клиента с email {email}: {e}")
return False
async def delete_client(
xui,
xui: py3xui.AsyncApi,
inbound_id: int,
email: str,
client_id: str,
) -> bool:
"""
Функция для удаления клиента с сервера 3x-ui.
Возвращает True при успешном удалении, иначе False.
Удаляет клиента с сервера 3x-ui.
Args:
xui: Экземпляр API клиента
inbound_id: ID входящего соединения
email: Email клиента
client_id: ID клиента
Returns:
bool: True если удаление успешно, False в противном случае
"""
await xui.login()
try:
await xui.login()
if SUPERNODE:
await xui.client.delete(inbound_id, client_id)
logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE).")
logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE)")
return True
client = await xui.client.get_by_email(email)
if not client:
logger.warning(f"Клиент с email {email} и ID {client_id} не найден.")
logger.warning(f"Клиент с email {email} и ID {client_id} не найден")
return False
client.id = client_id
await xui.client.delete(inbound_id, client.id)
logger.info(f"Клиент с ID {client_id} был удален успешно.")
logger.info(f"Клиент с ID {client_id} был удален успешно")
return True
except Exception as e:
+568 -312
View File
File diff suppressed because it is too large Load Diff
-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
+8
View File
@@ -0,0 +1,8 @@
from aiogram.enums import ChatType
from aiogram.filters import BaseFilter
from aiogram.types import Chat, TelegramObject
class IsPrivateFilter(BaseFilter):
async def __call__(self, event: TelegramObject, event_chat: Chat) -> bool:
return event_chat.type == ChatType.PRIVATE
+4
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
@@ -12,11 +13,13 @@ from .pay import router as pay_router
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
router = Router(name="handlers_main_router")
router.include_routers(
start_router,
captcha_router,
profile_router,
pay_router,
donate_router,
@@ -26,4 +29,5 @@ router.include_routers(
keys_router,
instructions_router,
admin_router,
user_router,
)
+3 -9
View File
@@ -12,14 +12,11 @@ router = Router()
AdminPanelCallback.filter(F.action == "backups"),
IsAdminFilter(),
)
async def handle_backups(
callback_query: CallbackQuery
):
async def handle_backups(callback_query: CallbackQuery):
kb = build_admin_back_kb("management")
await callback_query.message.edit_text(
text="💾 Инициализация резервного копирования базы данных...",
reply_markup=kb
text="💾 Инициализация резервного копирования базы данных...", reply_markup=kb
)
exception = await backup_database()
@@ -29,7 +26,4 @@ async def handle_backups(
else:
text = "✅ Резервная копия успешно создана и отправлена администраторам."
await callback_query.message.edit_text(
text=text,
reply_markup=kb
)
await callback_query.message.edit_text(text=text, reply_markup=kb)
+7 -22
View File
@@ -1,7 +1,7 @@
from typing import Any
from aiogram import F, Router
from aiogram.types import CallbackQuery, BufferedInputFile
from aiogram.types import BufferedInputFile, CallbackQuery
from database import delete_user_data
from filters.admin import IsAdminFilter
@@ -15,13 +15,8 @@ router = Router()
AdminPanelCallback.filter(F.action == "bans"),
IsAdminFilter(),
)
async def handle_bans(
callback_query: CallbackQuery
):
text = (
"🚫 Заблокировавшие бота"
"\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!"
)
async def handle_bans(callback_query: CallbackQuery):
text = "🚫 Заблокировавшие бота\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!"
await callback_query.message.edit_text(
text=text,
@@ -33,10 +28,7 @@ async def handle_bans(
AdminPanelCallback.filter(F.action == "bans_export"),
IsAdminFilter(),
)
async def handle_bans_export(
callback_query: CallbackQuery,
session: Any
):
async def handle_bans_export(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("management")
try:
@@ -53,9 +45,7 @@ async def handle_bans_export(
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")
await callback_query.message.answer_document(
document=document,
@@ -72,10 +62,7 @@ async def handle_bans_export(
AdminPanelCallback.filter(F.action == "bans_delete_banned"),
IsAdminFilter(),
)
async def handle_bans_delete_banned(
callback_query: CallbackQuery,
session: Any
):
async def handle_bans_delete_banned(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("bans")
try:
@@ -92,9 +79,7 @@ async def handle_bans_delete_banned(
for tg_id in blocked_ids:
await delete_user_data(session, tg_id)
await session.execute(
"DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids
)
await session.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids)
await callback_query.message.answer(
text=f"🗑️ Удалены данные о {len(blocked_ids)} пользователях и связанных записях.",
+20 -41
View File
@@ -4,9 +4,9 @@ from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from database import delete_coupon_from_db, get_all_coupons, create_coupon
from database import create_coupon, delete_coupon, get_all_coupons
from filters.admin import IsAdminFilter
from keyboards.admin.coupons_kb import build_coupons_kb, build_coupons_list_kb, AdminCouponDeleteCallback
from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from logger import logger
@@ -22,22 +22,16 @@ class AdminCouponsState(StatesGroup):
IsAdminFilter(),
)
async def handle_coupons(
callback_query: types.CallbackQuery,
callback_query: types.CallbackQuery,
):
await callback_query.message.edit_text(
text="🛠 Меню управления купонами:",
reply_markup=build_coupons_kb()
)
await callback_query.message.edit_text(text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb())
@router.callback_query(
AdminPanelCallback.filter(F.action == "coupons_create"),
IsAdminFilter(),
)
async def handle_coupons_create(
callback_query: types.CallbackQuery,
state: FSMContext
):
async def handle_coupons_create(callback_query: types.CallbackQuery, state: FSMContext):
text = (
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\n\n"
@@ -51,15 +45,8 @@ async def handle_coupons_create(
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
@router.message(
AdminCouponsState.waiting_for_coupon_data,
IsAdminFilter()
)
async def handle_coupon_data_input(
message: types.Message,
state: FSMContext,
session: Any
):
@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter())
async def handle_coupon_data_input(message: types.Message, state: FSMContext, session: Any):
text = message.text.strip()
parts = text.split()
@@ -83,10 +70,7 @@ async def handle_coupon_data_input(
coupon_amount = float(parts[1])
usage_limit = int(parts[2])
except ValueError:
text = (
"⚠️ <b>Проверьте правильность введенных данных!</b>\n"
"💱 Сумма должна быть числом, а лимит — целым числом."
)
text = "⚠️ <b>Проверьте правильность введенных данных!</b>\n💱 Сумма должна быть числом, а лимит — целым числом."
await message.answer(
text=text,
@@ -103,10 +87,7 @@ async def handle_coupon_data_input(
f"🔢 Лимит использования: <b>{usage_limit} раз</b>"
)
await message.answer(
text=text,
reply_markup=kb
)
await message.answer(text=text, reply_markup=kb)
await state.clear()
except Exception as e:
@@ -117,12 +98,14 @@ async def handle_coupon_data_input(
AdminPanelCallback.filter(F.action == "coupons_list"),
IsAdminFilter(),
)
async def handle_coupons_list(
callback_query: types.CallbackQuery,
session: Any
):
async def handle_coupons_list(callback_query: types.CallbackQuery, session: Any):
try:
coupons = await get_all_coupons(session)
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"]
total_pages = result["pages"]
current_page = result["current_page"]
if not coupons:
await callback_query.message.edit_text(
@@ -142,13 +125,11 @@ async def handle_coupons_list(
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n\n"
)
await callback_query.message.edit_text(
text=coupon_list,
reply_markup=kb
)
await callback_query.message.edit_text(text=coupon_list, reply_markup=kb)
except Exception as e:
logger.error(f"Ошибка при получении списка купонов: {e}")
await callback_query.message.answer("Произошла ошибка при получении списка купонов.")
@router.callback_query(
@@ -156,14 +137,12 @@ async def handle_coupons_list(
IsAdminFilter(),
)
async def handle_coupon_delete(
callback_query: types.CallbackQuery,
callback_data: AdminCouponDeleteCallback,
session: Any
callback_query: types.CallbackQuery, callback_data: AdminCouponDeleteCallback, session: Any
):
coupon_code = callback_data.coupon_code
try:
result = await delete_coupon_from_db(coupon_code, session)
result = await delete_coupon(coupon_code, session)
if result:
await handle_coupons_list(callback_query, session)
+9 -33
View File
@@ -5,57 +5,33 @@ from aiogram.types import CallbackQuery
from bot import version
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import build_panel_kb, AdminPanelCallback, build_management_kb
from keyboards.admin.panel_kb import AdminPanelCallback, build_management_kb, build_panel_kb
router = Router()
@router.callback_query(
AdminPanelCallback.filter(F.action == "admin"),
IsAdminFilter()
)
@router.callback_query(AdminPanelCallback.filter(F.action == "admin"), IsAdminFilter())
async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext):
text = (
"🤖 Панель администратора"
f"\n📌 Версия бота: {version}"
)
text = f"🤖 Панель администратора\n📌 Версия бота: {version}"
await state.clear()
await callback_query.message.edit_text(
text=text,
reply_markup=build_panel_kb()
)
await callback_query.message.edit_text(text=text, reply_markup=build_panel_kb())
@router.callback_query(
F.data == "admin",
IsAdminFilter()
)
@router.callback_query(F.data == "admin", IsAdminFilter())
async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext):
await handle_admin_message(callback_query.message, state)
@router.message(
Command("admin"),
IsAdminFilter()
)
@router.message(Command("admin"), IsAdminFilter())
async def handle_admin_message(message: types.Message, state: FSMContext):
text = (
"🤖 Панель администратора"
f"\n📌 Версия бота: {version}"
)
text = f"🤖 Панель администратора\n📌 Версия бота: {version}"
await state.clear()
await message.answer(
text=text,
reply_markup=build_panel_kb()
)
await message.answer(text=text, reply_markup=build_panel_kb())
@router.callback_query(
AdminPanelCallback.filter(F.action == "management"),
IsAdminFilter()
)
@router.callback_query(AdminPanelCallback.filter(F.action == "management"), IsAdminFilter())
async def handle_management(callback_query: CallbackQuery):
await callback_query.message.edit_text(
text="🤖 Управление ботом",
+4 -13
View File
@@ -4,7 +4,7 @@ from aiogram import F, Router
from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import build_restart_kb, AdminPanelCallback, build_admin_back_kb
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb, build_restart_kb
router = Router()
@@ -33,17 +33,8 @@ async def handle_restart_confirm(callback_query: CallbackQuery):
capture_output=True,
text=True,
)
await callback_query.message.edit_text(
text="🔄 Бот успешно перезагружен!",
reply_markup=kb
)
await callback_query.message.edit_text(text="🔄 Бот успешно перезагружен!", reply_markup=kb)
except subprocess.CalledProcessError:
await callback_query.message.edit_text(
text="🔄 Бот успешно перезагружен!",
reply_markup=kb
)
await callback_query.message.edit_text(text="🔄 Бот успешно перезагружен!", reply_markup=kb)
except Exception as e:
await callback_query.message.edit_text(
text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}",
reply_markup=kb
)
await callback_query.message.edit_text(text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", reply_markup=kb)
+18 -28
View File
@@ -8,7 +8,7 @@ from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.sender_kb import build_sender_kb, AdminSenderCallback
from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb
from logger import logger
router = Router()
@@ -22,9 +22,7 @@ class AdminSender(StatesGroup):
AdminPanelCallback.filter(F.action == "sender"),
IsAdminFilter(),
)
async def handle_sender(
callback_query: CallbackQuery
):
async def handle_sender(callback_query: CallbackQuery):
await callback_query.message.edit_text(
text="✍️ Выберите группу пользователей для рассылки:",
reply_markup=build_sender_kb(),
@@ -35,11 +33,7 @@ async def handle_sender(
AdminSenderCallback.filter(),
IsAdminFilter(),
)
async def handle_sender_callback(
callback_query: CallbackQuery,
callback_data: AdminSenderCallback,
state: FSMContext
):
async def handle_sender_callback(callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext):
await callback_query.message.edit_text(
text="✍️ Введите текст сообщения для рассылки:",
reply_markup=build_admin_back_kb("sender"),
@@ -52,11 +46,7 @@ async def handle_sender_callback(
AdminSender.waiting_for_message,
IsAdminFilter(),
)
async def handle_message_input(
message: types.Message,
state: FSMContext,
session: Any
):
async def handle_message_input(message: types.Message, state: FSMContext, session: Any):
text_message = message.text
try:
@@ -64,20 +54,26 @@ async def handle_message_input(
send_to = state_data.get("type", "all")
if send_to == "subscribed":
tg_ids = await session.fetch("""
tg_ids = await session.fetch(
"""
SELECT DISTINCT c.tg_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.expiry_time > $1
""", int(datetime.utcnow().timestamp() * 1000))
""",
int(datetime.utcnow().timestamp() * 1000),
)
elif send_to == "unsubscribed":
tg_ids = await session.fetch("""
tg_ids = await session.fetch(
"""
SELECT c.tg_id
FROM connections c
LEFT JOIN keys k ON c.tg_id = k.tg_id
GROUP BY c.tg_id
HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1
""", int(datetime.utcnow().timestamp() * 1000))
""",
int(datetime.utcnow().timestamp() * 1000),
)
else:
tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections")
@@ -87,13 +83,10 @@ async def handle_message_input(
for record in tg_ids:
tg_id = record["tg_id"]
try:
await message.bot.send_message(
chat_id=tg_id,
text=text_message
)
await message.bot.send_message(chat_id=tg_id, text=text_message)
success_count += 1
except Exception:
pass
except Exception as e:
logger.error(e)
text = (
f"📤 Рассылка завершена!"
@@ -102,10 +95,7 @@ async def handle_message_input(
f"\n❌ Не доставлено: {total_users - success_count}"
)
await message.answer(
text=text,
reply_markup=build_admin_back_kb("stats")
)
await message.answer(text=text, reply_markup=build_admin_back_kb("stats"))
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
+58 -150
View File
@@ -1,3 +1,5 @@
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
@@ -6,12 +8,16 @@ from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers_from_db
from database import check_unique_server_name, delete_server, get_servers
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.servers_kb import build_manage_server_kb, \
build_delete_server_kb, \
build_manage_cluster_kb, build_clusters_editor_kb, AdminServerEditorCallback
from keyboards.admin.servers_kb import (
AdminServerEditorCallback,
build_clusters_editor_kb,
build_delete_server_kb,
build_manage_cluster_kb,
build_manage_server_kb,
)
router = Router()
@@ -28,10 +34,8 @@ class AdminServersEditor(StatesGroup):
AdminPanelCallback.filter(F.action == "servers"),
IsAdminFilter(),
)
async def handle_servers(
callback_query: types.CallbackQuery
):
servers = await get_servers_from_db()
async def handle_servers(callback_query: types.CallbackQuery):
servers = await get_servers()
text = (
"<b>🔧 Управление кластерами</b>\n\n"
@@ -51,36 +55,23 @@ async def handle_servers(
AdminPanelCallback.filter(F.action == "clusters_add"),
IsAdminFilter(),
)
async def handle_clusters_add(
callback_query: types.CallbackQuery,
state: FSMContext
):
async def handle_clusters_add(callback_query: types.CallbackQuery, state: FSMContext):
text = (
"🔧 <b>Введите имя нового кластера:</b>\n\n"
"<b>Имя кластера должно быть уникальным!</b>\n"
"<i>Пример:</i> <code>cluster1</code> или <code>us_east_1</code>"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_admin_back_kb("servers")
)
await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb("servers"))
await state.set_state(AdminServersEditor.waiting_for_cluster_name)
@router.message(
AdminServersEditor.waiting_for_cluster_name,
IsAdminFilter()
)
async def handle_cluster_name_input(
message: types.Message,
state: FSMContext
):
@router.message(AdminServersEditor.waiting_for_cluster_name, IsAdminFilter())
async def handle_cluster_name_input(message: types.Message, state: FSMContext):
if not message.text:
await message.answer(
text="❌ Имя кластера не может быть пустым. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
text="❌ Имя кластера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers")
)
return
@@ -101,18 +92,11 @@ async def handle_cluster_name_input(
await state.set_state(AdminServersEditor.waiting_for_server_name)
@router.message(
AdminServersEditor.waiting_for_server_name,
IsAdminFilter()
)
async def handle_server_name_input(
message: types.Message,
state: FSMContext
):
@router.message(AdminServersEditor.waiting_for_server_name, IsAdminFilter())
async def handle_server_name_input(message: types.Message, state: FSMContext):
if not message.text:
await message.answer(
text="❌ Имя сервера не может быть пустым. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
text="❌ Имя сервера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers")
)
return
@@ -121,7 +105,7 @@ async def handle_server_name_input(
if not await check_unique_server_name(server_name):
await message.answer(
text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.",
reply_markup=build_admin_back_kb("servers")
reply_markup=build_admin_back_kb("servers"),
)
return
@@ -144,18 +128,12 @@ async def handle_server_name_input(
await state.set_state(AdminServersEditor.waiting_for_api_url)
@router.message(
AdminServersEditor.waiting_for_api_url,
IsAdminFilter()
)
async def handle_api_url_input(
message: types.Message,
state: FSMContext
):
@router.message(AdminServersEditor.waiting_for_api_url, IsAdminFilter())
async def handle_api_url_input(message: types.Message, state: FSMContext):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
text="❌ API URL должен начинаться с <code>https://</code>. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
reply_markup=build_admin_back_kb("servers"),
)
return
@@ -182,18 +160,12 @@ async def handle_api_url_input(
await state.set_state(AdminServersEditor.waiting_for_subscription_url)
@router.message(
AdminServersEditor.waiting_for_subscription_url,
IsAdminFilter()
)
async def handle_subscription_url_input(
message: types.Message,
state: FSMContext
):
@router.message(AdminServersEditor.waiting_for_subscription_url, IsAdminFilter())
async def handle_subscription_url_input(message: types.Message, state: FSMContext):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
text="❌ subscription_url должен начинаться с <code>https://</code>. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
reply_markup=build_admin_back_kb("servers"),
)
return
@@ -216,20 +188,14 @@ async def handle_subscription_url_input(
await state.set_state(AdminServersEditor.waiting_for_inbound_id)
@router.message(
AdminServersEditor.waiting_for_inbound_id,
IsAdminFilter()
)
async def handle_inbound_id_input(
message: types.Message,
state: FSMContext
):
@router.message(AdminServersEditor.waiting_for_inbound_id, IsAdminFilter())
async def handle_inbound_id_input(message: types.Message, state: FSMContext):
inbound_id = message.text.strip()
if not inbound_id.isdigit():
await message.answer(
text="❌ inbound_id должен быть числовым значением. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
reply_markup=build_admin_back_kb("servers"),
)
return
@@ -261,17 +227,13 @@ async def handle_inbound_id_input(
await state.clear()
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "clusters_manage"),
IsAdminFilter()
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter())
async def handle_clusters_manage(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
await callback_query.message.edit_text(
@@ -280,23 +242,17 @@ async def handle_clusters_manage(
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_availability"),
IsAdminFilter()
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter())
async def handle_servers_availability(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
if not cluster_servers:
await callback_query.message.answer(
text=f"Кластер '{cluster_name}' не содержит серверов."
)
await callback_query.message.answer(text=f"Кластер '{cluster_name}' не содержит серверов.")
return
text = (
@@ -304,51 +260,32 @@ async def handle_servers_availability(
"Это может занять до 1 минуты, пожалуйста, подождите..."
)
in_progress_message = await callback_query.message.answer(
text=text
)
in_progress_message = await callback_query.message.answer(text=text)
text = (
f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
)
text = 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())
text += (
f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
)
text += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
except Exception as e:
text += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
await in_progress_message.edit_text(
text=text,
reply_markup=build_admin_back_kb("servers")
)
await in_progress_message.edit_text(text=text, reply_markup=build_admin_back_kb("servers"))
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_manage"),
IsAdminFilter()
)
async def handle_servers_manage(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_manage"), IsAdminFilter())
async def handle_servers_manage(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback):
server_name = callback_data.data
servers = await get_servers_from_db()
servers = await get_servers()
cluster_name, server = next(
((c, s) for c, cs in servers.items()
for s in cs if s["server_name"] == server_name),
(None, None)
((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None)
)
if server:
@@ -368,19 +305,11 @@ async def handle_servers_manage(
reply_markup=build_manage_server_kb(server_name, cluster_name),
)
else:
await callback_query.message.edit_text(
text="❌ Сервер не найден."
)
await callback_query.message.edit_text(text="❌ Сервер не найден.")
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_delete"),
IsAdminFilter()
)
async def handle_servers_delete(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete"), IsAdminFilter())
async def handle_servers_delete(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback):
server_name = callback_data.data
await callback_query.message.edit_text(
@@ -389,39 +318,22 @@ async def handle_servers_delete(
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"),
IsAdminFilter()
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter())
async def handle_servers_delete_confirm(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
server_name = callback_data.data
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute(
"""
DELETE FROM servers WHERE server_name = $1
""",
server_name,
)
await conn.close()
await delete_server(server_name, session)
await callback_query.message.edit_text(
text=f"🗑️ Сервер {server_name} успешно удален.",
reply_markup=build_admin_back_kb("servers")
text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("servers")
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_add"),
IsAdminFilter()
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter())
async def handle_servers_add(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
state: FSMContext
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext
):
cluster_name = callback_data.data
@@ -441,17 +353,13 @@ async def handle_servers_add(
await state.set_state(AdminServersEditor.waiting_for_server_name)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "clusters_backup"),
IsAdminFilter()
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter())
async def handle_clusters_backup(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
for server in cluster_servers:
+11 -41
View File
@@ -17,10 +17,7 @@ router = Router()
AdminPanelCallback.filter(F.action == "stats"),
IsAdminFilter(),
)
async def handle_stats(
callback_query: CallbackQuery,
session: Any
):
async def handle_stats(callback_query: CallbackQuery, session: Any):
try:
total_users = await session.fetchval("SELECT COUNT(*) FROM users")
total_keys = await session.fetchval("SELECT COUNT(*) FROM keys")
@@ -35,13 +32,9 @@ async def handle_stats(
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)"
)
@@ -49,9 +42,7 @@ async def handle_stats(
"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",
@@ -81,10 +72,7 @@ async def handle_stats(
f" 🏦 За все время: <b>{total_payments_all_time} ₽</b>\n"
)
await callback_query.message.edit_text(
text=stats_message,
reply_markup=build_stats_kb()
)
await callback_query.message.edit_text(text=stats_message, reply_markup=build_stats_kb())
except Exception as e:
logger.error(f"Error in user_stats_menu: {e}")
@@ -93,46 +81,28 @@ async def handle_stats(
AdminPanelCallback.filter(F.action == "stats_export_users_csv"),
IsAdminFilter(),
)
async def handle_export_users_csv(
callback_query: CallbackQuery,
session: Any
):
async def handle_export_users_csv(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("stats")
try:
export = await export_users_csv(session)
await callback_query.message.answer_document(
document=export,
caption="📥 Экспорт пользователей в CSV"
)
await callback_query.message.answer_document(document=export, caption="📥 Экспорт пользователей в CSV")
except Exception as e:
logger.error(f"Ошибка при экспорте пользователей в CSV: {e}")
await callback_query.message.edit_text(
text=f"❗ Произошла ошибка при экспорте: {e}",
reply_markup=kb
)
await callback_query.message.edit_text(text=f"❗ Произошла ошибка при экспорте: {e}", reply_markup=kb)
@router.callback_query(
AdminPanelCallback.filter(F.action == "stats_export_payments_csv"),
IsAdminFilter(),
)
async def handle_export_payments_csv(
callback_query: CallbackQuery,
session: Any
):
async def handle_export_payments_csv(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("stats")
try:
export = await export_payments_csv(session)
await callback_query.message.answer_document(
document=export,
caption="📥 Экспорт платежей в CSV"
)
await callback_query.message.answer_document(document=export, caption="📥 Экспорт платежей в CSV")
except Exception as e:
logger.error(f"Ошибка при экспорте платежей в CSV: {e}")
await callback_query.message.edit_text(
text=f"❗ Произошла ошибка при экспорте: {e}",
reply_markup=kb
)
await callback_query.message.edit_text(text=f"❗ Произошла ошибка при экспорте: {e}", reply_markup=kb)
+592
View File
@@ -0,0 +1,592 @@
import asyncio
from datetime import datetime
from typing import Any
import pytz
from aiogram import Bot, F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import TOTAL_GB
from database import (
delete_key,
delete_user_data,
get_client_id_by_email,
get_key_details,
get_keys,
get_servers,
update_key_expiry,
update_trial,
)
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
delete_key_from_cluster,
renew_key_in_cluster,
)
from handlers.utils import sanitize_key_name
from logger import logger
router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
waiting_for_username = State()
displaying_user_info = State()
waiting_for_new_balance = State()
waiting_for_key_name = State()
waiting_for_expiry_time = State()
waiting_for_message_text = State()
@router.callback_query(F.data == "search_by_tg_id", IsAdminFilter())
async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_tg_id)
@router.callback_query(F.data == "search_by_username", IsAdminFilter())
async def prompt_username(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await callback_query.message.answer("🔍 Введите Username клиента:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_username)
@router.message(UserEditorState.waiting_for_username, IsAdminFilter())
async def handle_username_input(message: types.Message, state: FSMContext, session: Any):
username = message.text.strip().lstrip("@").replace("https://t.me/", "")
user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username)
if not user_record:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(
"🔍 Пользователь с указанным username не найден. 🚫",
reply_markup=builder.as_markup(),
)
await state.clear()
return
tg_id = user_record["tg_id"]
username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
key_records = await get_keys(tg_id, session)
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
if balance is None:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(
"🚫 Пользователь с указанным tg_id не найден. 🔍",
reply_markup=builder.as_markup(),
)
await state.clear()
return
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
callback_data=f"restore_trial_{tg_id}",
)
)
builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
@router.callback_query(F.data.startswith("send_message_"))
async def handle_send_message(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.data.split("_")[2]
await state.update_data(target_tg_id=tg_id)
await callback_query.message.answer("✉️ Введите текст сообщения, которое вы хотите отправить пользователю.")
await state.set_state(UserEditorState.waiting_for_message_text)
@router.message(UserEditorState.waiting_for_message_text, IsAdminFilter())
async def process_send_message(message: types.Message, state: FSMContext, bot: Bot):
data = await state.get_data()
target_tg_id = data.get("target_tg_id")
if not target_tg_id:
await message.answer("🚫 Ошибка: ID пользователя не найден.")
await state.clear()
return
try:
await bot.send_message(chat_id=target_tg_id, text=message.text)
await message.answer("✅ Сообщение успешно отправлено.")
except Exception as e:
await message.answer(f"❌ Не удалось отправить сообщение: {e}")
await state.clear()
@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter())
async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any):
tg_id = int(message.text)
username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
key_records = await get_keys(tg_id, session)
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
if balance is None:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(
"❌ Пользователь с указанным tg_id не найден. 🔍",
reply_markup=builder.as_markup(),
)
await state.clear()
return
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
)
builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
callback_data=f"restore_trial_{tg_id}",
)
)
builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
@router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter())
async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any):
tg_id = int(callback_query.data.split("_")[2])
await update_trial(tg_id, 0, session)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад в меню администратора", callback_data="admin"))
await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter())
async def process_balance_change(callback_query: CallbackQuery, state: FSMContext):
tg_id = int(callback_query.data.split("_")[2])
await state.update_data(tg_id=tg_id)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await callback_query.message.answer("💸 Введите новую сумму баланса:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_new_balance)
@router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter())
async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any):
if not message.text.isdigit() or int(message.text) < 0:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(
"❌ Пожалуйста, введите корректную сумму для изменения баланса.",
reply_markup=builder.as_markup(),
)
return
new_balance = int(message.text)
user_data = await state.get_data()
tg_id = user_data.get("tg_id")
await session.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2",
new_balance,
tg_id,
)
response_message = f"✅ Баланс успешно изменен на <b>{new_balance}</b>."
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="⬅️ Назад в меню администратора",
callback_data="admin",
)
)
await message.answer(response_message, reply_markup=builder.as_markup())
await state.clear()
@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter())
async def process_key_edit(callback_query: CallbackQuery, session: Any):
email = callback_query.data.split("_", 2)[2]
key_details = await get_key_details(email, session)
if not key_details:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await callback_query.message.answer(
"🔍 <b>Информация о ключе не найдена.</b> 🚫",
reply_markup=builder.as_markup(),
)
return
response_message = (
f"🔑 Ключ: <code>{key_details['key']}</code>\n"
f"⏰ Дата истечения: <b>{key_details['expiry_date']}</b>\n"
f"💰 Баланс пользователя: <b>{key_details['balance']}</b>\n"
f"🌐 Кластер: <b>{key_details['server_name']}</b>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="ℹ️ Получить информацию о юзере",
callback_data=f"user_info|{key_details['tg_id']}",
)
)
builder.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
)
)
builder.row(
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{email}",
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
@router.callback_query(F.data == "search_by_key_name", IsAdminFilter())
async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await callback_query.message.answer("🔑 Введите имя ключа:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_key_name)
@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any):
key_name = sanitize_key_name(message.text)
key_details = await get_key_details(key_name, session)
if not key_details:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(
"🚫 Пользователь с указанным именем ключа не найден.",
reply_markup=builder.as_markup(),
)
await state.clear()
return
response_message = (
f"🔑 Ключ: <code>{key_details['key']}</code>\n"
f"⏰ Дата истечения: <b>{key_details['expiry_date']}</b>\n"
f"💰 Баланс пользователя: <b>{key_details['balance']}</b>\n"
f"🌐 Сервер: <b>{key_details['server_name']}</b>"
)
key_buttons = InlineKeyboardBuilder()
key_buttons.row(
InlineKeyboardButton(
text="ℹ️ Получить информацию о юзере",
callback_data=f"user_info|{key_details['tg_id']}",
)
)
key_buttons.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{key_name}",
)
)
key_buttons.row(
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{key_name}",
)
)
key_buttons.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(response_message, reply_markup=key_buttons.as_markup())
await state.clear()
@router.callback_query(F.data.startswith("change_expiry|"), IsAdminFilter())
async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext):
email = callback_query.data.split("|")[1]
await callback_query.message.answer(
f"⏳ Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:"
)
await state.update_data(email=email)
await state.set_state(UserEditorState.waiting_for_expiry_time)
@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any):
user_data = await state.get_data()
email = user_data.get("email")
if not email:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup())
await state.clear()
return
try:
expiry_time_str = message.text
moscow_tz = pytz.timezone("Europe/Moscow")
expiry_time = int(
datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=moscow_tz).timestamp() * 1000
)
client_id = await get_client_id_by_email(email)
if client_id is None:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(
f"🚫 Клиент с email {email} не найден. 🔍",
reply_markup=builder.as_markup(),
)
await state.clear()
return
record = await get_key_details(email, session)
if not record:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(
"🚫 Клиент не найден в базе данных. 🔍",
reply_markup=builder.as_markup(),
)
await state.clear()
return
clusters = await get_servers(session)
async def update_key_on_all_servers():
tasks = []
for cluster_name, cluster_servers in clusters.items():
for _server in cluster_servers:
tasks.append(
asyncio.create_task(
renew_key_in_cluster(
cluster_name,
email,
client_id,
expiry_time,
total_gb=TOTAL_GB,
)
)
)
await asyncio.gather(*tasks)
await update_key_on_all_servers()
await update_key_expiry(client_id, expiry_time, session)
response_message = (
f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin"))
await message.answer(response_message, reply_markup=builder.as_markup())
except ValueError:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await message.answer(
"❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(e)
await state.clear()
@router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter())
async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any):
email = callback_query.data.split("|")[1]
key_details = await get_key_details(email, session)
if key_details is None:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup())
return
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"confirm_delete_admin|{key_details['client_id']}",
)
)
builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor"))
await callback_query.message.answer(
"<b>❓ Вы уверены, что хотите удалить ключ?</b>",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter())
async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any):
client_id = callback_query.data.split("|")[1]
record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id)
if record:
email = record["email"]
response_message = "✅ Ключ успешно удален."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
clusters = await get_servers(session)
async def delete_key_from_servers(email, client_id):
tasks = []
for cluster_name, cluster_servers in clusters.items():
for _server in cluster_servers:
tasks.append(delete_key_from_cluster(cluster_name, email, client_id))
await asyncio.gather(*tasks)
await delete_key_from_servers(email, client_id)
await delete_key(client_id, session)
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
else:
response_message = "🚫 Ключ не найден или уже удален."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("user_info|"), IsAdminFilter())
async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
tg_id = int(callback_query.data.split("|")[1])
username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
key_records = await get_keys(tg_id, session)
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
builder.row(InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}"))
builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor"))
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await callback_query.message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
@router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter())
async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
tg_id = int(callback_query.data.split("_")[3])
confirmation_markup = InlineKeyboardMarkup(
row_width=2,
inline_keyboard=[
[InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}")],
[InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")],
],
)
await callback_query.message.answer(
f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
reply_markup=confirmation_markup,
)
@router.callback_query(F.data.startswith("delete_user_"), IsAdminFilter())
async def delete_user(callback_query: types.CallbackQuery, session: Any):
tg_id = int(callback_query.data.split("_")[2])
key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id)
async def delete_keys_from_servers():
try:
tasks = []
for email, client_id in key_records:
servers = await get_servers(session)
for cluster_id, _cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
await delete_keys_from_servers()
try:
await delete_user_data(session, tg_id)
back_button = InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard)
except Exception as e:
logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
await callback_query.message.answer(
f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
)
+126 -339
View File
@@ -10,19 +10,29 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery
from config import TOTAL_GB
from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry
from database import delete_key, delete_user_data, get_client_id_by_email, get_servers, update_key_expiry, update_trial
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
delete_key_from_cluster,
delete_key_from_db,
renew_key_in_cluster, update_subscription,
renew_key_in_cluster,
update_subscription,
)
from handlers.utils import sanitize_key_name
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.users_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \
build_user_delete_kb, AdminUserEditorCallback, build_editor_kb, build_users_balance_kb, \
build_users_balance_change_kb, build_user_key_kb, build_users_key_expiry_kb, AdminUserKeyEditorCallback, \
build_users_key_show_kb
from keyboards.admin.users_kb import (
AdminUserEditorCallback,
AdminUserKeyEditorCallback,
build_editor_kb,
build_key_delete_kb,
build_key_edit_kb,
build_user_delete_kb,
build_user_edit_kb,
build_user_key_kb,
build_users_balance_change_kb,
build_users_balance_kb,
build_users_key_expiry_kb,
build_users_key_show_kb,
)
from logger import logger
router = Router()
@@ -42,10 +52,7 @@ class UserEditorState(StatesGroup):
AdminPanelCallback.filter(F.action == "search_user"),
IsAdminFilter(),
)
async def handle_search_user(
callback_query: CallbackQuery,
state: FSMContext
):
async def handle_search_user(callback_query: CallbackQuery, state: FSMContext):
text = (
"<b>🔍 Поиск пользователя</b>"
"\n\n📌 Введите ID, Username или перешлите сообщение пользователя."
@@ -55,36 +62,20 @@ async def handle_search_user(
)
await state.set_state(UserEditorState.waiting_for_user_data)
await callback_query.message.edit_text(
text=text,
reply_markup=build_admin_back_kb()
)
await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb())
@router.callback_query(
AdminPanelCallback.filter(F.action == "search_key"),
IsAdminFilter(),
)
async def handle_search_key(
callback_query: CallbackQuery,
state: FSMContext
):
async def handle_search_key(callback_query: CallbackQuery, state: FSMContext):
await state.set_state(UserEditorState.waiting_for_key_name)
await callback_query.message.edit_text(
text="🔑 Введите имя ключа для поиска:",
reply_markup=build_admin_back_kb()
)
await callback_query.message.edit_text(text="🔑 Введите имя ключа для поиска:", reply_markup=build_admin_back_kb())
@router.message(
UserEditorState.waiting_for_user_data,
IsAdminFilter()
)
async def handle_user_data_input(
message: types.Message,
state: FSMContext,
session: Any
):
@router.message(UserEditorState.waiting_for_user_data, IsAdminFilter())
async def handle_user_data_input(message: types.Message, state: FSMContext, session: Any):
kb = build_admin_back_kb()
if message.forward_from:
@@ -93,23 +84,18 @@ async def handle_user_data_input(
return
if not message.text:
await message.answer(
text="🚫 Пожалуйста, отправьте текстовое сообщение.",
reply_markup=kb
)
await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb)
return
if message.text.isdigit():
tg_id = int(message.text)
else:
# Удаление '@' символа в начале сообщения
username = message.text.strip().lstrip('@')
username = message.text.strip().lstrip("@")
# Удаление начала ссылки на профиль
username = username.replace('https://t.me/', '')
username = username.replace("https://t.me/", "")
user = await session.fetchrow(
"SELECT tg_id FROM users WHERE username = $1", username
)
user = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username)
if not user:
await message.answer(
@@ -123,32 +109,19 @@ async def handle_user_data_input(
await process_user_search(message, state, session, tg_id)
@router.message(
UserEditorState.waiting_for_key_name,
IsAdminFilter()
)
async def handle_key_name_input(
message: types.Message,
state: FSMContext,
session: Any
):
@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any):
kb = build_admin_back_kb()
if not message.text:
await message.answer(
text="🚫 Пожалуйста, отправьте текстовое сообщение.",
reply_markup=kb
)
await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb)
return
key_name = sanitize_key_name(message.text)
key_details = await get_key_details(key_name, session)
if not key_details:
await message.answer(
text="🚫 Пользователь с указанным именем ключа не найден.",
reply_markup=kb
)
await message.answer(text="🚫 Пользователь с указанным именем ключа не найден.", reply_markup=kb)
return
await process_user_search(message, state, session, key_details["tg_id"])
@@ -159,46 +132,28 @@ async def handle_key_name_input(
IsAdminFilter(),
)
async def handle_send_message(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext
):
tg_id = callback_data.tg_id
await callback_query.message.edit_text(
text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:",
reply_markup=build_editor_kb(tg_id)
text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", reply_markup=build_editor_kb(tg_id)
)
await state.update_data(tg_id=tg_id)
await state.set_state(UserEditorState.waiting_for_message_text)
@router.message(
UserEditorState.waiting_for_message_text,
IsAdminFilter()
)
async def handle_message_text_input(
message: types.Message,
state: FSMContext
):
@router.message(UserEditorState.waiting_for_message_text, IsAdminFilter())
async def handle_message_text_input(message: types.Message, state: FSMContext):
data = await state.get_data()
tg_id = data.get("tg_id")
try:
await message.bot.send_message(
chat_id=tg_id,
text=message.text
)
await message.answer(
text="✅ Сообщение успешно отправлено.",
reply_markup=build_editor_kb(tg_id)
)
await message.bot.send_message(chat_id=tg_id, text=message.text)
await message.answer(text="✅ Сообщение успешно отправлено.", reply_markup=build_editor_kb(tg_id))
except Exception as e:
await message.answer(
text=f"❌ Не удалось отправить сообщение: {e}",
reply_markup=build_editor_kb(tg_id)
)
await message.answer(text=f"❌ Не удалось отправить сообщение: {e}", reply_markup=build_editor_kb(tg_id))
await state.clear()
@@ -208,37 +163,28 @@ async def handle_message_text_input(
IsAdminFilter(),
)
async def handle_trial_restore(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
tg_id = callback_data.tg_id
await restore_trial(tg_id, session)
await callback_query.message.edit_text(
text="✅ Триал успешно восстановлен!",
reply_markup=build_editor_kb(tg_id)
)
await update_trial(tg_id, 0, session)
await callback_query.message.edit_text(text="✅ Триал успешно восстановлен!", reply_markup=build_editor_kb(tg_id))
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_balance_edit"),
IsAdminFilter()
)
async def handle_balance_change(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_edit"), IsAdminFilter())
async def handle_balance_change(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
tg_id = callback_data.tg_id
records = await session.fetch("""
records = await session.fetch(
"""
SELECT amount, payment_system, status, created_at
FROM payments
WHERE tg_id = $1
ORDER BY created_at DESC
LIMIT 5
""", tg_id)
""",
tg_id,
)
balance = await get_user_balance(tg_id, session)
@@ -263,21 +209,12 @@ async def handle_balance_change(
else:
text += "\n <i>🚫 Отсутствуют</i>"
await callback_query.message.edit_text(
text=text,
reply_markup=build_users_balance_kb(tg_id)
)
await callback_query.message.edit_text(text=text, reply_markup=build_users_balance_kb(tg_id))
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_balance_add"),
IsAdminFilter()
)
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_add"), IsAdminFilter())
async def handle_balance_add(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext,
session: Any
callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any
):
tg_id = callback_data.tg_id
amount = callback_data.data
@@ -292,19 +229,12 @@ async def handle_balance_add(
await callback_query.message.edit_text(
text="✍️ Введите сумму, которую хотите добавить на баланс пользователя:",
reply_markup=build_users_balance_change_kb(tg_id)
reply_markup=build_users_balance_change_kb(tg_id),
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_balance_take"),
IsAdminFilter()
)
async def handle_balance_take(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext
):
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_take"), IsAdminFilter())
async def handle_balance_take(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext):
tg_id = callback_data.tg_id
await state.update_data(tg_id=tg_id, op_type="take")
@@ -312,19 +242,12 @@ async def handle_balance_take(
await callback_query.message.edit_text(
text="✍️ Введите сумму, которую хотите вычесть из баланса пользователя:",
reply_markup=build_users_balance_change_kb(tg_id)
reply_markup=build_users_balance_change_kb(tg_id),
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_balance_set"),
IsAdminFilter()
)
async def handle_balance_set(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext
):
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_set"), IsAdminFilter())
async def handle_balance_set(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext):
tg_id = callback_data.tg_id
await state.update_data(tg_id=tg_id, op_type="set")
@@ -332,27 +255,19 @@ async def handle_balance_set(
await callback_query.message.edit_text(
text="✍️ Введите баланс, который хотите установить пользователю:",
reply_markup=build_users_balance_change_kb(tg_id)
reply_markup=build_users_balance_change_kb(tg_id),
)
@router.message(
UserEditorState.waiting_for_balance,
IsAdminFilter()
)
async def handle_balance_input(
message: types.Message,
state: FSMContext,
session: Any
):
@router.message(UserEditorState.waiting_for_balance, IsAdminFilter())
async def handle_balance_input(message: types.Message, state: FSMContext, session: Any):
data = await state.get_data()
tg_id = data.get("tg_id")
op_type = data.get("op_type")
if not message.text.isdigit() or int(message.text) < 0:
await message.answer(
text="🚫 Пожалуйста, введите корректную сумму!",
reply_markup=build_users_balance_change_kb(tg_id)
text="🚫 Пожалуйста, введите корректную сумму!", reply_markup=build_users_balance_change_kb(tg_id)
)
return
@@ -368,21 +283,12 @@ async def handle_balance_input(
text = f"✅ Баланс пользователя изменен на <b>{amount}Р</b>"
await set_user_balance(tg_id, amount, session)
await message.answer(
text=text,
reply_markup=build_users_balance_change_kb(tg_id)
)
await message.answer(text=text, reply_markup=build_users_balance_change_kb(tg_id))
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_key_edit"),
IsAdminFilter()
)
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter())
async def handle_key_edit(
callback_query: CallbackQuery,
callback_data: CallbackData,
session: Any,
update: bool = False
callback_query: CallbackQuery, callback_data: CallbackData, session: Any, update: bool = False
):
email = callback_data.data
key_details = await get_key_details(email, session)
@@ -403,42 +309,24 @@ async def handle_key_edit(
)
if not update or not callback_data.edit:
await callback_query.message.edit_text(
text=text,
reply_markup=build_key_edit_kb(key_details, email)
)
await callback_query.message.edit_text(text=text, reply_markup=build_key_edit_kb(key_details, email))
else:
await callback_query.message.edit_text(
text=text,
reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email)
text=text, reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_expiry_edit"),
IsAdminFilter()
)
async def handle_change_expiry(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback
):
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_expiry_edit"), IsAdminFilter())
async def handle_change_expiry(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback):
tg_id = callback_data.tg_id
email = callback_data.data
await callback_query.message.edit_reply_markup(
reply_markup=build_users_key_expiry_kb(tg_id, email)
)
await callback_query.message.edit_reply_markup(reply_markup=build_users_key_expiry_kb(tg_id, email))
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "add"),
IsAdminFilter()
)
@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter())
async def handle_expiry_add(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext,
session: Any
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -463,18 +351,13 @@ async def handle_expiry_add(
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите добавить к времени действия ключа:",
reply_markup=build_users_key_show_kb(tg_id, email)
reply_markup=build_users_key_show_kb(tg_id, email),
)
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "take"),
IsAdminFilter()
)
@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "take"), IsAdminFilter())
async def handle_expiry_take(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -484,18 +367,13 @@ async def handle_expiry_take(
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите вычесть из времени действия ключа:",
reply_markup=build_users_key_show_kb(tg_id, email)
reply_markup=build_users_key_show_kb(tg_id, email),
)
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "set"),
IsAdminFilter()
)
@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "set"), IsAdminFilter())
async def handle_expiry_set(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -509,21 +387,11 @@ async def handle_expiry_set(
"\n Пример: 2025-02-09 09:01"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_users_key_show_kb(tg_id, email)
)
await callback_query.message.edit_text(text=text, reply_markup=build_users_key_show_kb(tg_id, email))
@router.message(
UserEditorState.waiting_for_expiry_time,
IsAdminFilter()
)
async def handle_expiry_time_input(
message: types.Message,
state: FSMContext,
session: Any
):
@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any):
data = await state.get_data()
tg_id = data.get("tg_id")
email = data.get("email")
@@ -532,7 +400,7 @@ async def handle_expiry_time_input(
if op_type != "set" and (not message.text.isdigit() or int(message.text) < 0):
await message.answer(
text="🚫 Пожалуйста, введите корректное количество дней!",
reply_markup=build_users_key_show_kb(tg_id, email)
reply_markup=build_users_key_show_kb(tg_id, email),
)
return
@@ -555,31 +423,19 @@ async def handle_expiry_time_input(
await change_expiry_time(key_details["expiry_time"] - days * 24 * 3600 * 1000, email, session)
else:
try:
expiry_time = int(
datetime.strptime(message.text, "%Y-%m-%d %H:%M").timestamp() * 1000
)
expiry_time = int(datetime.strptime(message.text, "%Y-%m-%d %H:%M").timestamp() * 1000)
text = f"✅ Время действия ключа изменено на <b>{message.text}</b>"
await change_expiry_time(expiry_time, email, session)
except ValueError:
text = f"🚫 Пожалуйста, используйте корректный формат даты!"
text = "🚫 Пожалуйста, используйте корректный формат даты!"
except Exception as e:
text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}"
await message.answer(
text=text,
reply_markup=build_users_key_show_kb(tg_id, email)
)
await message.answer(text=text, reply_markup=build_users_key_show_kb(tg_id, email))
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_update_key"),
IsAdminFilter()
)
async def handle_update_key(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_update_key"), IsAdminFilter())
async def handle_update_key(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -591,104 +447,65 @@ async def handle_update_key(
except Exception as e:
logger.error(f"Ошибка при обновлении ключа {email} администратором: {e}")
await callback_query.message.answer(
text=f"❗ Произошла ошибка при обновлении ключа: {e}",
reply_markup=build_user_key_kb(tg_id, email)
text=f"❗ Произошла ошибка при обновлении ключа: {e}", reply_markup=build_user_key_kb(tg_id, email)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_key"),
IsAdminFilter()
)
async def handle_delete_key(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key"), IsAdminFilter())
async def handle_delete_key(callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
email = callback_data.data
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:
await callback_query.message.edit_text(
text="🚫 Ключ не найден!",
reply_markup=build_editor_kb(callback_data.tg_id)
text="🚫 Ключ не найден!", reply_markup=build_editor_kb(callback_data.tg_id)
)
return
await callback_query.message.edit_text(
text="❓ Вы уверены, что хотите удалить ключ?",
reply_markup=build_key_delete_kb(callback_data.tg_id, email)
text="❓ Вы уверены, что хотите удалить ключ?", reply_markup=build_key_delete_kb(callback_data.tg_id, email)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"),
IsAdminFilter()
)
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), IsAdminFilter())
async def handle_delete_key_confirm(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
email = callback_data.data
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)
kb = build_editor_kb(callback_data.tg_id)
if record:
client_id = record["client_id"]
clusters = await get_servers_from_db()
clusters = await get_servers()
async def delete_key_from_servers():
tasks = []
for cluster_name, cluster_servers in clusters.items():
for _ 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()
await delete_key_from_db(client_id, session)
await delete_key(client_id, session)
await callback_query.message.edit_text(
text="✅ Ключ успешно удален.",
reply_markup=kb
)
await callback_query.message.edit_text(text="✅ Ключ успешно удален.", reply_markup=kb)
else:
await callback_query.message.edit_text(
text="🚫 Ключ не найден или уже удален.",
reply_markup=kb
)
await callback_query.message.edit_text(text="🚫 Ключ не найден или уже удален.", reply_markup=kb)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_user"),
IsAdminFilter()
)
async def handle_delete_user(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback
):
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user"), IsAdminFilter())
async def handle_delete_user(callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback):
tg_id = callback_data.tg_id
await callback_query.message.edit_text(
text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
reply_markup=build_user_delete_kb(tg_id)
text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?", reply_markup=build_user_delete_kb(tg_id)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"),
IsAdminFilter()
)
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), IsAdminFilter())
async def handle_delete_user_confirm(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
tg_id = callback_data.tg_id
key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id)
@@ -697,8 +514,8 @@ async def handle_delete_user_confirm(
try:
tasks = []
for email, client_id in key_records:
servers = await get_servers_from_db()
for cluster_id, cluster in servers.items():
servers = await get_servers()
for cluster_id, _cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
except Exception as e:
@@ -709,8 +526,7 @@ async def handle_delete_user_confirm(
try:
await delete_user_data(session, tg_id)
await callback_query.message.edit_text(
text=f"🗑️ Пользователь с ID {tg_id} был удален.",
reply_markup=build_editor_kb(callback_data.tg_id)
text=f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=build_editor_kb(callback_data.tg_id)
)
except Exception as e:
logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
@@ -719,37 +535,19 @@ async def handle_delete_user_confirm(
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_editor"),
IsAdminFilter()
)
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter())
async def handle_editor(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext,
session: Any
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any
):
await process_user_search(
callback_query.message,
state,
session,
callback_data.tg_id,
callback_data.edit
)
await process_user_search(callback_query.message, state, session, callback_data.tg_id, callback_data.edit)
async def process_user_search(
message: types.Message,
state: FSMContext,
session: Any,
tg_id: int,
edit: bool = False
message: types.Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False
) -> None:
await state.clear()
balance = await session.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
if balance is None:
await message.answer(
@@ -758,15 +556,9 @@ async def process_user_search(
)
return
username = await session.fetchval(
"SELECT username FROM users WHERE tg_id = $1", tg_id
)
key_records = await session.fetch(
"SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id
)
referral_count = await session.fetchval(
"SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
)
username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
key_records = await session.fetch("SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id)
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
text = (
f"<b>📊 Информация о пользователе</b>"
@@ -780,17 +572,11 @@ async def process_user_search(
if edit:
try:
await message.edit_text(
text=text,
reply_markup=kb
)
await message.edit_text(text=text, reply_markup=kb)
except TelegramBadRequest:
pass
else:
await message.answer(
text=text,
reply_markup=kb
)
await message.answer(text=text, reply_markup=kb)
async def get_key_details(email, session):
@@ -827,14 +613,12 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce
if client_id is None:
return ValueError(f"User with email {email} was not found")
server_id = await session.fetchrow(
"SELECT server_id FROM keys WHERE client_id = $1", client_id
)
server_id = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id)
if not server_id:
return ValueError(f"User with client_id {server_id} was not found")
clusters = await get_servers_from_db()
clusters = await get_servers()
async def update_key_on_all_servers():
tasks = [
@@ -859,7 +643,8 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce
async def get_user_balance(tg_id: int, session: Any) -> float:
try:
return await session.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id,
"SELECT balance FROM connections WHERE tg_id = $1",
tg_id,
)
except Exception as e:
logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}")
@@ -870,7 +655,8 @@ async def add_user_balance(tg_id: int, balance: int, session: Any) -> None:
try:
await session.execute(
"UPDATE connections SET balance = balance + $1 WHERE tg_id = $2",
balance, tg_id,
balance,
tg_id,
)
except Exception as e:
logger.error(f"Ошибка при добавлении баланса для пользователя {tg_id}: {e}")
@@ -880,7 +666,8 @@ async def set_user_balance(tg_id: int, balance: int, session: Any) -> None:
try:
await session.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2",
balance, tg_id,
balance,
tg_id,
)
except Exception as e:
logger.error(f"Ошибка при установке баланса для пользователя {tg_id}: {e}")
+1 -1
View File
@@ -1,6 +1,6 @@
GIFT = "🎁 Подарить подписку"
MY_GIFTS = "🎁 Мои подарки"
PROFILE = "👤 Личный кабинет"
BACK = "🔙 Назад"
BACK = "⬅️ Назад"
GIFTS_ABOUT = "<b>Дарите подарки и следите, чтобы они дошли до адресата! 🎄</b>"
SHARE_GIFT = "🎁 Поделиться подарком"
+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} рублей. Перейдите по ссылке для оплаты:"
+70
View File
@@ -0,0 +1,70 @@
import random
import secrets
from typing import Any
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CAPTCHA_EMOJIS
from logger import logger
router = Router()
async def generate_captcha(message: Message, state: FSMContext):
"""Генерирует новую капчу и сохраняет правильный ответ в состоянии
Пример словаря CAPTCHA_EMOJIS:
{
"🐶": "собаку", # Собака
"🐱": "кошку", # Кошка
"🐭": "мышь", # Мышь
"🐹": "хомяка", # Хомяк
"🐰": "кролика", # Кролик
...
}
"""
# Выбираем случайный эмодзи и его описание из конфига
correct_emoji, correct_text = secrets.choice(list(CAPTCHA_EMOJIS.items()))
# Получаем 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"🔒 Для подтверждения что вы не робот, выберите кнопку с {correct_text}",
"markup": builder.as_markup(),
}
@router.callback_query(F.data.startswith("captcha_"))
async def check_captcha(callback: CallbackQuery, state: FSMContext, session: Any, admin: bool):
from handlers.start import start_command
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(message, state, session, admin, False)
else:
logger.warning(f"Пользователь {callback.message.chat.id} неверно ответил на капчу")
captcha = await generate_captcha(message, state)
await callback.message.answer(text=captcha["text"], reply_markup=captcha["markup"])
+14 -44
View File
@@ -1,4 +1,3 @@
from datetime import datetime
from typing import Any
from aiogram import F, Router, types
@@ -7,7 +6,13 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from database import update_balance
from database import (
check_coupon_usage,
create_coupon_usage,
get_coupon_by_code,
update_balance,
update_coupon_usage_count,
)
class CouponActivationState(StatesGroup):
@@ -18,9 +23,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,58 +43,25 @@ 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(), parse_mode="HTML"
)
await message.answer(activation_result, reply_markup=builder.as_markup())
await state.clear()
async def activate_coupon(user_id: int, coupon_code: str, session: Any):
coupon_record = await session.fetchrow(
"""
SELECT id, usage_limit, usage_count, is_used, amount
FROM coupons
WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
""",
coupon_code,
)
coupon_record = await get_coupon_by_code(coupon_code, session)
if not coupon_record:
return "<b>❌ Купон не найден</b> 🚫 или его использование ограничено. 🔒 Пожалуйста, проверьте код и попробуйте снова. 🔍"
usage_exists = await session.fetchrow(
"""
SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2
""",
coupon_record["id"],
user_id,
)
usage_exists = await check_coupon_usage(coupon_record["id"], user_id, session)
if usage_exists:
return "<b>❌ Вы уже активировали этот купон.</b> 🚫 Купоны могут быть активированы только один раз. 🔒"
coupon_amount = coupon_record["amount"]
async with session.transaction():
await session.execute(
"""
UPDATE coupons
SET usage_count = usage_count + 1,
is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END
WHERE id = $1
""",
coupon_record["id"],
)
await update_coupon_usage_count(coupon_record["id"], session)
await create_coupon_usage(coupon_record["id"], user_id, session)
await session.execute(
"""
INSERT INTO coupon_usages (coupon_id, user_id, used_at)
VALUES ($1, $2, $3)
""",
coupon_record["id"],
user_id,
datetime.utcnow(),
)
await update_balance(user_id, coupon_amount)
await update_balance(user_id, coupon_amount, session)
return f"<b>✅ Купон успешно активирован! 🎉</b>\n\nНа ваш баланс добавлено <b>{coupon_amount} рублей</b> 💰."
+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(),
+17 -58
View File
@@ -1,12 +1,13 @@
import os
from typing import Any
import asyncpg
import aiofiles
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL
from database import get_key_details
from handlers.texts import (
CONNECT_TV_TEXT,
INSTRUCTION_PC,
@@ -29,9 +30,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
@@ -47,9 +46,10 @@ async def send_instructions(
else:
send_photo = callback_query_or_message.answer_photo
with open(image_path, "rb") as image_from_buffer:
async with aiofiles.open(image_path, "rb") as image_from_buffer:
image_data = await image_from_buffer.read()
await send_photo(
BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"),
BufferedInputFile(image_data, filename="instructions.jpg"),
caption=instructions_message,
reply_markup=builder.as_markup(),
)
@@ -57,23 +57,12 @@ async def send_instructions(
@router.callback_query(F.data.startswith("connect_pc|"))
async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
record = await session.fetchrow(
"""
SELECT k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
)
record = await get_key_details(key_name, session)
if not record:
await callback_query.message.answer(
"❌ <b>Ключ не найден. Проверьте имя ключа.</b> 🔍"
)
await callback_query.message.answer("❌ <b>Ключ не найден. Проверьте имя ключа.</b> 🔍")
return
key = record["key"]
@@ -81,21 +70,14 @@ 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"))
await callback_query.message.answer(
instruction_message,
reply_markup=builder.as_markup(),
parse_mode="HTML",
disable_web_page_preview=True,
)
@@ -105,56 +87,33 @@ 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(
text=CONNECT_TV_TEXT,
reply_markup=builder.as_markup(),
parse_mode="HTML",
disable_web_page_preview=True,
)
@router.callback_query(F.data.startswith("continue_tv|"))
async def process_continue_tv(callback_query: types.CallbackQuery):
async def process_continue_tv(callback_query: types.CallbackQuery, session: Any):
key_name = callback_query.data.split("|")[1]
tg_id = callback_query.from_user.id
logger.info(f"tg_id: {tg_id}, key_name: {key_name}")
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"""
SELECT k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
)
record = await get_key_details(key_name, session)
logger.info(f"Query result: {record}")
finally:
await conn.close()
logger.info(f"Query result: {record}")
subscription_link = record["key"]
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(), parse_mode="HTML"
)
await callback_query.message.answer(text=message_text, reply_markup=builder.as_markup())
+16 -41
View File
@@ -24,9 +24,10 @@ from config import (
USE_NEW_PAYMENT_FLOW,
)
from database import (
create_temporary_data,
get_balance,
get_key_details,
get_trial,
save_temporary_data,
store_key,
update_balance,
)
@@ -56,16 +57,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)
@@ -84,9 +81,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()
@@ -108,9 +103,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(
"💳 Выберите тарифный план для создания нового ключа:",
@@ -136,7 +129,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
if balance < plan_price:
required_amount = plan_price - balance
await save_temporary_data(
await create_temporary_data(
session,
tg_id,
"waiting_for_payment",
@@ -154,12 +147,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}₽.",
@@ -169,7 +158,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
expiry_time = datetime.utcnow() + timedelta(days=duration_days)
await create_key(tg_id, expiry_time, None, session, callback_query)
await update_balance(tg_id, -plan_price)
await update_balance(tg_id, -plan_price, session)
async def create_key(
@@ -201,15 +190,11 @@ async def create_key(
builder = InlineKeyboardBuilder()
for country in countries:
callback_data = f"select_country|{country}|{expiry_time.isoformat()}"
builder.row(
InlineKeyboardButton(
text=country, callback_data=callback_data
)
)
builder.row(InlineKeyboardButton(text=country, callback_data=callback_data))
logger.info(f"[Country Selection] Добавлена кнопка для страны: {country} с callback_data: {callback_data}")
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="profile"))
logger.info("[Country Selection] Добавлена кнопка '🔙 Назад'.")
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="profile"))
logger.info("[Country Selection] Добавлена кнопка '⬅️ Назад'.")
if isinstance(message_or_query, Message):
logger.info("[Country Selection] Сообщение пользователя - тип Message.")
@@ -243,11 +228,7 @@ async def create_key(
key_name = generate_random_email()
logger.info(f"[Key Generation] Сгенерировано имя ключа: {key_name} для пользователя {tg_id}")
existing_key = await session.fetchrow(
"SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
key_name,
tg_id,
)
existing_key = await get_key_details(key_name, session)
if not existing_key:
break
logger.warning(f"[Key Generation] Имя ключа {key_name} уже существует. Генерация нового.")
@@ -368,16 +349,10 @@ async def finalize_key_creation(
key_name = generate_random_email()
logger.info(f"Generated random key name for user {tg_id}: {key_name}")
existing_key = await session.fetchrow(
"SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
key_name,
tg_id,
)
existing_key = await get_key_details(key_name, session)
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()
+39 -61
View File
@@ -3,9 +3,9 @@ from typing import Any
from py3xui import AsyncApi
from client import add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, SUPERNODE, TOTAL_GB, PUBLIC_LINK
from database import get_servers_from_db, store_key
from client import ClientConfig, add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB
from database import get_servers, store_key
from handlers.utils import get_least_loaded_cluster
from logger import logger
@@ -15,7 +15,7 @@ async def create_key_on_cluster(cluster_id: str, tg_id: int, client_id: str, ema
Создает ключ на всех серверах указанного кластера.
"""
try:
servers = await get_servers_from_db()
servers = await get_servers()
cluster = servers.get(cluster_id)
if not cluster:
@@ -75,9 +75,7 @@ async def create_client_on_server(
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:
@@ -89,24 +87,27 @@ async def create_client_on_server(
await add_client(
xui,
client_id,
unique_email,
tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=sub_id,
ClientConfig(
client_id=client_id,
email=unique_email,
tg_id=tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=sub_id,
),
)
if SUPERNODE:
await asyncio.sleep(0.7)
async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb):
try:
servers = await get_servers_from_db()
servers = await get_servers()
cluster = servers.get(cluster_id)
if not cluster:
@@ -124,9 +125,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:
@@ -137,37 +136,20 @@ 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
async def delete_key_from_db(client_id, session):
try:
await session.execute("DELETE FROM keys WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
async def delete_key_from_cluster(cluster_id, email, client_id):
"""Удаление ключа с серверов в кластере"""
try:
servers = await get_servers_from_db()
servers = await get_servers()
cluster = servers.get(cluster_id)
if not cluster:
@@ -200,15 +182,13 @@ 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
async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id):
try:
servers = await get_servers_from_db()
servers = await get_servers()
cluster = servers.get(cluster_id)
if not cluster:
@@ -232,29 +212,27 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id
tasks.append(
add_client(
xui,
client_id,
email,
tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_time,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=email
ClientConfig(
client_id=client_id,
email=email,
tg_id=tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_time,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
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
+58 -151
View File
@@ -4,12 +4,12 @@ import os
from datetime import datetime, timedelta
from typing import Any
import aiofiles
import asyncpg
import pytz
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.payments.yookassa_pay import process_custom_amount_input
from bot import bot
from config import (
@@ -27,10 +27,14 @@ from config import (
USE_NEW_PAYMENT_FLOW,
)
from database import (
check_server_name_by_cluster,
create_temporary_data,
delete_key,
get_balance,
get_servers_from_db,
save_temporary_data,
get_key_details,
get_keys,
get_keys_by_server,
get_servers,
update_balance,
update_key_expiry,
)
@@ -44,11 +48,11 @@ from handlers.buttons.add_subscribe import (
)
from handlers.keys.key_utils import (
delete_key_from_cluster,
delete_key_from_db,
renew_key_in_cluster,
update_subscription,
)
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import (
DISCOUNTS,
KEY_NOT_FOUND_MSG,
@@ -67,7 +71,7 @@ router = Router()
@router.callback_query(F.data == "view_keys")
@router.message(F.text == "/subs")
async def process_callback_or_message_view_keys(
callback_query_or_message: types.Message | types.CallbackQuery, session: Any
callback_query_or_message: types.Message | types.CallbackQuery, session: Any
):
if isinstance(callback_query_or_message, types.CallbackQuery):
chat_id = callback_query_or_message.message.chat.id
@@ -79,21 +83,12 @@ async def process_callback_or_message_view_keys(
send_photo = callback_query_or_message.answer_photo
try:
records = await session.fetch(
"""
SELECT email, client_id, expiry_time
FROM keys
WHERE tg_id = $1
""",
chat_id,
)
records = await get_keys(chat_id, session)
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)
@@ -115,9 +110,7 @@ def build_keys_response(records):
if expiry_time:
expiry_date_full = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz)
formatted_date_full = expiry_date_full.strftime(
"до %d %B %Y года, %H:%M"
).lower()
formatted_date_full = expiry_date_full.strftime("до %d %B %Y года, %H:%M").lower()
formatted_date_short = expiry_date_full.strftime("до %d %B").lower()
else:
@@ -125,41 +118,31 @@ def build_keys_response(records):
formatted_date_short = "без срока действия"
button_text = f"{key_name} ({formatted_date_short})"
builder.row(
InlineKeyboardButton(
text=button_text, callback_data=f"view_key|{key_name}"
)
)
builder.row(InlineKeyboardButton(text=button_text, callback_data=f"view_key|{key_name}"))
response_message += f"• <b>{key_name}</b> ({formatted_date_full})\n"
else:
response_message = (
"<b>🔑 У вас пока нет подписок.</b>\n\n"
"Вы можете создать новую подписку для подключения устройств."
"<b>🔑 У вас пока нет подписок.</b>\n\nВы можете создать новую подписку для подключения устройств."
)
builder.row(
InlineKeyboardButton(text=" Добавить подписку", callback_data="create_key")
)
builder.row(InlineKeyboardButton(text=" Добавить подписку", callback_data="create_key"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
inline_keyboard = builder.as_markup()
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:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await send_photo(
photo=BufferedInputFile(
image_file.read(), filename=os.path.basename(image_path)
),
photo=BufferedInputFile(image_data, filename=os.path.basename(image_path)),
caption=text,
reply_markup=keyboard,
)
@@ -175,15 +158,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
record = await session.fetchrow(
"""
SELECT k.expiry_time, k.server_id, k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
)
record = await get_key_details(key_name, session)
if record:
key = record["key"]
@@ -207,9 +182,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()
@@ -223,46 +196,28 @@ 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}"),
)
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="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()
@@ -272,9 +227,10 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
await callback_query.message.answer("Файл изображения не найден.")
return
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"),
photo=BufferedInputFile(image_data, filename="pic_view.jpg"),
caption=response_message,
reply_markup=keyboard,
)
@@ -291,9 +247,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]
@@ -302,9 +256,7 @@ async def process_callback_update_subscription(
await process_callback_view_key(callback_query, session)
except Exception as e:
logger.error(f"Ошибка при обновлении ключа {email} пользователем: {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|"))
@@ -319,11 +271,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")],
]
)
@@ -341,14 +289,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
record = await session.fetchrow(
"""
SELECT client_id, expiry_time
FROM keys
WHERE email = $1
""",
key_name,
)
record = await get_key_details(key_name, session)
if record:
client_id = record["client_id"]
@@ -360,9 +301,8 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
months = plan_details["months"]
price = plan_details["price"]
discount = DISCOUNTS.get(plan_id, 0)
button_text = (
f'📅 {months} месяц{"а" if months > 1 else ""} ({price} руб.)'
+ (f' {discount}% скидка' if discount > 0 else "")
button_text = f"📅 {months} месяц{'а' if months > 1 else ''} ({price} руб.)" + (
f" {discount}% скидка" if discount > 0 else ""
)
builder.row(
InlineKeyboardButton(
@@ -371,19 +311,13 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data="view_keys"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
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(
@@ -397,38 +331,30 @@ 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 get_key_details(email, session)
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)
await delete_key(client_id, session)
await callback_query.message.answer(
response_message,
reply_markup=keyboard,
)
servers = await get_servers_from_db()
servers = await get_servers(session)
async def delete_key_from_servers():
try:
tasks = []
for cluster_id, cluster in servers.items():
tasks.append(
delete_key_from_cluster(cluster_id, email, client_id)
)
for cluster_id, _cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
@@ -437,13 +363,11 @@ async def process_callback_confirm_delete(
asyncio.create_task(delete_key_from_servers())
await delete_key_from_db(client_id, session)
await delete_key(client_id, session)
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(
@@ -464,10 +388,7 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi
total_gb = TOTAL_GB * gb_multiplier.get(plan, 1) if TOTAL_GB > 0 else 0
try:
record = await session.fetchrow(
"SELECT email, expiry_time FROM keys WHERE client_id = $1",
client_id,
)
record = await get_keys_by_server(tg_id, client_id, session)
if record:
email = record["email"]
@@ -486,9 +407,10 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi
required_amount = cost - balance
logger.info(
f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}")
f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}"
)
await save_temporary_data(
await create_temporary_data(
session,
tg_id,
"waiting_for_renewal_payment",
@@ -543,15 +465,7 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup())
conn = await asyncpg.connect(DATABASE_URL)
key_info = await conn.fetchrow(
"""
SELECT server_id
FROM keys
WHERE tg_id = $1 AND client_id = $2
""",
tg_id,
client_id,
)
key_info = await get_key_details(email, conn)
if not key_info:
logger.error(f"[RENEW] Ключ с client_id {client_id} для пользователя {tg_id} не найден.")
@@ -561,14 +475,7 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
server_id = key_info["server_id"]
if USE_COUNTRY_SELECTION:
cluster_info = await conn.fetchrow(
"""
SELECT cluster_name
FROM servers
WHERE server_name = $1
""",
server_id,
)
cluster_info = await check_server_name_by_cluster(server_id, conn)
if not cluster_info:
logger.error(f"[RENEW] Сервер {server_id} не найден в таблице servers.")
@@ -592,8 +499,8 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
total_gb,
)
await update_key_expiry(client_id, new_expiry_time)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time, conn)
await update_balance(tg_id, -cost, conn)
logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.")
await renew_key_on_cluster()
+23 -44
View File
@@ -7,12 +7,13 @@ import asyncpg
from aiohttp import web
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE, TRANSITION_DATE_STR
from database import get_servers_from_db
from database import get_key_details, get_servers
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,17 +34,16 @@ 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:
except TimeoutError:
logger.error(f"Таймаут при получении {url} для tg_id: {tg_id}")
return []
except Exception as e:
logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}")
return []
async def combine_unique_lines(urls, tg_id, query_string):
if SUPERNODE:
logger.info(f"Режим SUPERNODE активен. Возвращаем первую ссылку для tg_id: {tg_id}")
@@ -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 get_key_details(email, conn)
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} является новым.")
@@ -131,27 +122,22 @@ async def handle_old_subscription(request):
status=400,
)
servers = await get_servers_from_db()
servers = await get_servers()
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 get_key_details(email, conn)
if not client_data:
logger.warning(f"Клиент с email {email} не найден в базе.")
@@ -196,21 +180,17 @@ async def handle_new_subscription(request):
status=403,
)
servers = await get_servers_from_db()
servers = await get_servers()
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}")
+17 -17
View File
@@ -6,9 +6,9 @@ from typing import Any
import pytz
from py3xui import AsyncApi
from client import add_client
from client import ClientConfig, add_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME
from database import get_servers_from_db, store_key, use_trial
from database import get_servers, get_trial, store_key, update_trial
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
from logger import logger
@@ -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 get_trial(tg_id, session)
if trial_status == 1:
return {"error": "Вы уже использовали пробную версию."}
except Exception as e:
@@ -35,7 +33,7 @@ async def create_trial_key(tg_id: int, session: Any):
expiry_time = current_time + timedelta(days=TRIAL_TIME)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
clusters = await get_servers_from_db()
clusters = await get_servers(session)
least_loaded_cluster = await get_least_loaded_cluster()
if least_loaded_cluster not in clusters:
raise ValueError(f"Кластер {least_loaded_cluster} не найден в базе данных.")
@@ -58,16 +56,18 @@ async def create_trial_key(tg_id: int, session: Any):
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
),
client_id,
email,
tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(server_info["inbound_id"]),
sub_id=base_email
ClientConfig(
client_id=client_id,
email=email,
tg_id=tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(server_info["inbound_id"]),
sub_id=base_email,
),
)
)
@@ -83,5 +83,5 @@ async def create_trial_key(tg_id: int, session: Any):
session=session,
)
await use_trial(tg_id, session)
await update_trial(tg_id, 1, session)
return result
+62 -137
View File
@@ -2,6 +2,7 @@ import asyncio
import os
from datetime import datetime, timedelta
import aiofiles
import asyncpg
import pytz
from aiogram import Bot, Router, types
@@ -9,7 +10,6 @@ from aiogram.exceptions import TelegramForbiddenError
from aiogram.types import BufferedInputFile
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from .utils import format_time_until_deletion
from config import (
ADMIN_PASSWORD,
@@ -17,20 +17,20 @@ from config import (
AUTO_DELETE_EXPIRED_KEYS,
AUTO_RENEW_KEYS,
DATABASE_URL,
DELETE_KEYS_DELAY,
DEV_MODE,
EXPIRED_KEYS_CHECK_INTERVAL,
RENEWAL_PLANS,
TOTAL_GB,
TRIAL_TIME,
DELETE_KEYS_DELAY,
)
from database import (
add_blocked_user,
add_notification,
check_notification_time,
create_blocked_user,
delete_key,
get_balance,
get_servers_from_db,
get_servers,
update_balance,
update_key_expiry,
)
@@ -38,29 +38,10 @@ from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluste
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED
from logger import logger
from .utils import format_time_until_deletion
router = Router()
async def check_users_and_update_blocked(bot: Bot):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
users = await conn.fetch("SELECT tg_id FROM users")
for user in users:
try:
await bot.send_chat_action(user['tg_id'], "typing")
except (TelegramForbiddenError,Exception):
await conn.execute(
"INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING",
user['tg_id']
)
logger.info(f"User {user['tg_id']} added to blocked_users")
except Exception as e:
logger.error(f"Error in check_users_and_update_blocked: {e}")
finally:
if conn:
await conn.close()
async def periodic_expired_keys_check(bot: Bot):
"""Периодическая проверка истекших ключей с кастомным интервалом."""
@@ -80,7 +61,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:
@@ -95,8 +75,6 @@ async def notify_expiring_keys(bot: Bot):
await notify_inactive_trial_users(bot, conn)
await asyncio.sleep(0.5)
await check_online_users()
await asyncio.sleep(0.5)
await notify_10h_keys(bot, conn, current_time, threshold_time_10h)
await asyncio.sleep(0.5)
await notify_24h_keys(bot, conn, current_time, threshold_time_24h)
@@ -110,24 +88,6 @@ 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 'активен'}"
)
return blocked
except Exception as e:
logger.warning(
f"Не удалось проверить статус бота для пользователя {chat_id}: {e}"
)
return False
async def notify_10h_keys(
bot: Bot,
conn: asyncpg.Connection,
@@ -163,7 +123,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(
@@ -177,11 +141,11 @@ async def process_10h_record(record, bot, conn):
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn)
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(record["client_id"], new_expiry_time)
await update_key_expiry(record["client_id"], new_expiry_time, conn)
servers = await get_servers_from_db()
servers = await get_servers(conn)
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
@@ -194,10 +158,11 @@ async def process_10h_record(record, bot, conn):
)
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_10h.jpg"),
photo=BufferedInputFile(image_data, filename="notify_10h.jpg"),
caption=KEY_RENEWED.format(email=email),
reply_markup=keyboard,
)
@@ -212,7 +177,6 @@ async def process_10h_record(record, bot, conn):
await send_renewal_notification(bot, tg_id, email, message, conn, record["client_id"], "notified")
async def notify_24h_keys(
bot: Bot,
conn: asyncpg.Connection,
@@ -250,7 +214,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(
@@ -263,11 +231,11 @@ async def process_24h_record(record, bot, conn):
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn)
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(record["client_id"], new_expiry_time)
await update_key_expiry(record["client_id"], new_expiry_time, conn)
servers = await get_servers_from_db()
servers = await get_servers(conn)
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
@@ -280,10 +248,11 @@ async def process_24h_record(record, bot, conn):
)
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_24h.jpg"),
photo=BufferedInputFile(image_data, filename="notify_24h.jpg"),
caption=KEY_RENEWED.format(email=email),
reply_markup=keyboard,
)
@@ -308,10 +277,11 @@ async def send_renewal_notification(bot, tg_id, email, message, conn, client_id,
image_path = os.path.join("img", "notify_24h.jpg")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_24h.jpg"),
photo=BufferedInputFile(image_data, filename="notify_24h.jpg"),
caption=message,
reply_markup=keyboard.as_markup(),
)
@@ -320,7 +290,7 @@ async def send_renewal_notification(bot, tg_id, email, message, conn, client_id,
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
await conn.execute(f"UPDATE keys SET {flag} = TRUE WHERE client_id = $1", client_id)
await conn.execute("UPDATE keys SET notified_24h = $1 WHERE client_id = $2", flag, client_id)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
@@ -348,17 +318,10 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
username = user["username"]
first_name = user["first_name"]
last_name = user["last_name"]
display_name = (
username
or first_name
or last_name
or "Пользователь"
)
display_name = username or first_name or last_name or "Пользователь"
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()
@@ -368,11 +331,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 = (
@@ -384,26 +343,21 @@ 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."
)
await add_blocked_user(tg_id, conn)
logger.warning(f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users.")
await create_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}")
await asyncio.sleep(1)
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float):
logger.info("Проверка подписок, срок действия которых скоро истекает...")
@@ -440,19 +394,17 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
for record in expired_keys:
try:
await delete_key_from_cluster(
cluster_id=record["server_id"],
email=record["email"],
client_id=record["client_id"]
cluster_id=record["server_id"], email=record["email"], client_id=record["client_id"]
)
await conn.execute(
"DELETE FROM keys WHERE client_id = $1",
record["client_id"]
await delete_key(record["client_id"], conn)
logger.info(
f"Ключ {record['client_id']} удалён"
+ (f" после задержки {DELETE_KEYS_DELAY} сек." if DELETE_KEYS_DELAY > 0 else "")
)
logger.info(f"Ключ {record['client_id']} удалён" +
(f" после задержки {DELETE_KEYS_DELAY} сек." if DELETE_KEYS_DELAY > 0 else ""))
except Exception as e:
logger.error(f"Ошибка при удалении ключа {record['client_id']}: {e}")
async def process_key(record, bot, conn):
tg_id = record["tg_id"]
client_id = record["client_id"]
@@ -474,26 +426,20 @@ async def process_key(record, bot, conn):
keyboard = InlineKeyboardBuilder()
if DELETE_KEYS_DELAY > 0:
keyboard.row(types.InlineKeyboardButton(
text="🔄 Продлить",
callback_data=f"renew_key|{email}"
))
keyboard.row(types.InlineKeyboardButton(
text="👤 Личный кабинет",
callback_data="profile"
))
keyboard.row(types.InlineKeyboardButton(text="🔄 Продлить", callback_data=f"renew_key|{email}"))
keyboard.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
image_path = os.path.join("img", "notify_expired.jpg")
try:
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn)
new_expiry_time = int((datetime.now(moscow_tz) + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(client_id, new_expiry_time)
await update_key_expiry(client_id, new_expiry_time, conn)
servers = await get_servers_from_db()
servers = await get_servers(conn)
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB)
@@ -511,10 +457,11 @@ async def process_key(record, bot, conn):
try:
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"),
photo=BufferedInputFile(image_data, filename="notify_expired.jpg"),
caption=KEY_RENEWED.format(email=email),
reply_markup=keyboard.as_markup(),
)
@@ -531,7 +478,7 @@ async def process_key(record, bot, conn):
time_since_expiry = current_time_utc - expiry_time
if time_since_expiry <= EXPIRED_KEYS_CHECK_INTERVAL * 1000:
message_expired = f"Ваша подписка {email} истекла. Пополните баланс для продления."
if DELETE_KEYS_DELAY > 0:
time_until_deletion = format_time_until_deletion(DELETE_KEYS_DELAY)
if time_until_deletion != "0 минут":
@@ -539,19 +486,16 @@ async def process_key(record, bot, conn):
try:
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"),
photo=BufferedInputFile(image_data, filename="notify_expired.jpg"),
caption=message_expired,
reply_markup=keyboard.as_markup(),
)
else:
await bot.send_message(
tg_id,
text=message_expired,
reply_markup=keyboard.as_markup()
)
await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard.as_markup())
logger.info(f"Уведомление об истечении подписки отправлено пользователю {tg_id}.")
except Exception as e:
logger.error(f"Не удалось отправить уведомление об истечении клиенту {tg_id}: {e}")
@@ -560,9 +504,9 @@ async def process_key(record, bot, conn):
if AUTO_DELETE_EXPIRED_KEYS:
current_time = int(datetime.utcnow().timestamp() * 1000)
if DELETE_KEYS_DELAY == 0 or current_time >= expiry_time + (DELETE_KEYS_DELAY * 1000):
servers = await get_servers_from_db()
servers = await get_servers(conn)
for cluster_id in servers:
try:
@@ -592,22 +536,3 @@ async def process_key(record, bot, conn):
except Exception as e:
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
)
await xui.login()
try:
online_users = len(await xui.client.online())
logger.info(
f"Сервер '{server['server_name']}' доступен, текущее количество активных пользователей: {online_users}."
)
except Exception as 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"))
+20 -29
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(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
@@ -111,9 +107,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("|")
@@ -167,9 +161,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("Неверная подпись или данные запроса.")
@@ -208,9 +200,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}")
@@ -219,27 +209,25 @@ 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.")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_robokassa"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa"))
await callback_query.message.answer(
"Пожалуйста, введите сумму пополнения.",
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:
@@ -249,7 +237,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()
@@ -277,9 +264,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
+22 -35
View File
@@ -1,6 +1,7 @@
import os
from typing import Any
import aiofiles
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
@@ -8,7 +9,7 @@ from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS
from database import get_balance, get_key_count, get_referral_stats, get_trial
from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial
from handlers.buttons.profile import (
ADD_SUB,
BALANCE,
@@ -51,9 +52,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,22 +80,21 @@ 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):
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
if is_callback:
await callback_query_or_message.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
photo=BufferedInputFile(image_data, filename="pic.jpg"),
caption=profile_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query_or_message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
photo=BufferedInputFile(image_data, filename="pic.jpg"),
caption=profile_message,
reply_markup=builder.as_markup(),
)
@@ -122,10 +120,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):
@@ -133,22 +129,15 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session:
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
query = """
SELECT amount, payment_system, status, created_at
FROM payments
WHERE tg_id = $1
ORDER BY created_at DESC
LIMIT 3
"""
records = await session.fetch(query, callback_query.from_user.id)
records = await get_last_payments(callback_query.from_user.id, 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 +147,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")
@@ -183,9 +168,10 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery):
)
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="tariffs.jpg"),
photo=BufferedInputFile(image_data, filename="tariffs.jpg"),
caption=tariffs_message,
reply_markup=builder.as_markup(),
)
@@ -210,9 +196,10 @@ async def invite_handler(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic_invite.jpg"),
photo=BufferedInputFile(image_data, filename="pic_invite.jpg"),
caption=invite_message,
reply_markup=builder.as_markup(),
)
+47 -92
View File
@@ -1,6 +1,7 @@
import os
from typing import Any
import aiofiles
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
@@ -13,6 +14,7 @@ from aiogram.types import (
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CAPTCHA_ENABLE,
CHANNEL_EXISTS,
CHANNEL_URL,
CONNECT_ANDROID,
@@ -26,8 +28,10 @@ from database import (
add_connection,
add_referral,
check_connection_exists,
get_coupon_details,
get_referral_by_referred_id,
get_trial,
use_trial,
update_trial,
)
from handlers.buttons.add_subscribe import (
DOWNLOAD_ANDROID_BUTTON,
@@ -37,6 +41,7 @@ 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
@@ -47,18 +52,24 @@ 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 and captcha:
captcha = await generate_captcha(message, state)
await message.answer(text=captcha["text"], reply_markup=captcha["markup"])
return
if message.text:
try:
connection_exists = await check_connection_exists(message.chat.id)
@@ -66,9 +77,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}")
@@ -77,51 +86,29 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
recipient_tg_id = message.chat.id
gift_info = await session.fetchrow(
"SELECT * FROM gifts WHERE gift_id = $1 AND is_used = FALSE",
gift_id,
)
gift_info = await get_coupon_details(gift_id, session)
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)
await session.execute(
"""
INSERT INTO connections (tg_id, balance, trial)
VALUES ($1, 0, 1)
ON CONFLICT (tg_id) DO UPDATE SET trial = 1
""",
recipient_tg_id,
)
await add_connection(tg_id=recipient_tg_id, session=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",
@@ -132,9 +119,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:
@@ -142,36 +127,23 @@ 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(
"SELECT * FROM referrals WHERE referred_tg_id = $1",
message.chat.id,
)
existing_referral = await get_referral_by_referred_id(message.chat.id, session)
if existing_referral:
logger.info(f"Реферал с ID {message.chat.id} уже существует.")
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:
@@ -179,16 +151,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)
@@ -203,33 +171,28 @@ 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"))
if os.path.isfile(image_path):
with open(image_path, "rb") as image_from_buffer:
async with aiofiles.open(image_path, "rb") as image_from_buffer:
image_data = await image_from_buffer.read()
await message.answer_photo(
photo=BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"),
photo=BufferedInputFile(image_data, filename="pic.jpg"),
caption=WELCOME_TEXT,
reply_markup=builder.as_markup(),
)
@@ -249,7 +212,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any):
if "error" in trial_key_info:
await callback_query.message.answer(trial_key_info["error"])
else:
await use_trial(user_id, session)
await update_trial(user_id, 1, session)
key_message = (
f"🔑 <b>Ваш персональный ключ доступа:</b>\n"
@@ -268,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")
@@ -293,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),
@@ -306,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())
+22
View File
@@ -0,0 +1,22 @@
from typing import Any
from aiogram import Router
from aiogram.filters.chat_member_updated import KICKED, MEMBER, ChatMemberUpdatedFilter
from aiogram.types import ChatMemberUpdated
from database import create_blocked_user, delete_blocked_user
from logger import logger
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 create_blocked_user(event.from_user.id, session)
@router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=MEMBER))
async def user_unblocked_bot(event: ChatMemberUpdated, session: Any):
logger.info(f"User {event.from_user.id} unblocked the bot.")
await delete_blocked_user(event.from_user.id, session)
+16 -19
View File
@@ -1,23 +1,23 @@
import json
import random
import re
import secrets
import string
from datetime import datetime, timedelta
import aiohttp
import asyncpg
from bot import bot
from config import DATABASE_URL
from database import get_servers_from_db
from database import get_all_keys, get_servers
from logger import logger
from datetime import datetime, timedelta
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"])
@@ -52,7 +52,7 @@ def generate_random_email(length: int = 6) -> str:
Returns:
str: Сгенерированная случайная строка.
"""
return "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=length))
return "".join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length)) if length > 0 else ""
async def get_least_loaded_cluster() -> str:
@@ -62,13 +62,13 @@ async def get_least_loaded_cluster() -> str:
Returns:
str: Идентификатор наименее загруженного кластера.
"""
servers = await get_servers_from_db()
servers = await get_servers()
cluster_loads: dict[str, int] = {cluster_id: 0 for cluster_id in servers.keys()}
async with asyncpg.create_pool(DATABASE_URL) as pool:
async with pool.acquire() as conn:
keys = await conn.fetch("SELECT server_id FROM keys")
keys = await get_all_keys(conn)
for key in keys:
cluster_id = key["server_id"]
if cluster_id in cluster_loads:
@@ -87,9 +87,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:
"""
Обрабатывает ошибку, отправляя сообщение пользователю.
@@ -101,27 +99,26 @@ 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}")
await bot.send_message(tg_id, message, parse_mode="HTML")
await bot.send_message(tg_id, message)
except Exception as e:
logger.error(f"Ошибка при обработке ошибки: {e}")
def format_time_until_deletion(seconds: int) -> str:
if seconds <= 0:
return "0 минут"
days = seconds // (3600 * 24)
hours = (seconds % (3600 * 24)) // 3600
minutes = (seconds % 3600 + 59) // 60
parts = []
if days > 0:
if days == 1:
parts.append(f"{days} день")
@@ -129,7 +126,7 @@ def format_time_until_deletion(seconds: int) -> str:
parts.append(f"{days} дня")
else:
parts.append(f"{days} дней")
if hours > 0:
if hours == 1:
parts.append(f"{hours} час")
@@ -137,7 +134,7 @@ def format_time_until_deletion(seconds: int) -> str:
parts.append(f"{hours} часа")
else:
parts.append(f"{hours} часов")
if minutes > 0 and days == 0:
if minutes == 1:
parts.append("1 минута")
+4 -12
View File
@@ -1,21 +1,13 @@
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
def build_bans_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="📄 Выгрузить в CSV",
callback_data=AdminPanelCallback(action="bans_export").pack()
)
builder.button(
text="🗑️ Удалить из БД",
callback_data=AdminPanelCallback(action="bans_delete_banned").pack()
)
builder.row(
build_admin_back_btn("management")
)
builder.button(text="📄 Выгрузить в CSV", callback_data=AdminPanelCallback(action="bans_export").pack())
builder.button(text="🗑️ Удалить из БД", callback_data=AdminPanelCallback(action="bans_delete_banned").pack())
builder.row(build_admin_back_btn("management"))
builder.adjust(1)
return builder.as_markup()
+5 -17
View File
@@ -11,17 +11,9 @@ class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"):
def build_coupons_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text=" Создать купон",
callback_data=AdminPanelCallback(action="coupons_create").pack()
)
builder.button(
text="Купоны",
callback_data=AdminPanelCallback(action="coupons_list").pack()
)
builder.row(
build_admin_back_btn()
)
builder.button(text=" Создать купон", callback_data=AdminPanelCallback(action="coupons_create").pack())
builder.button(text="Купоны", callback_data=AdminPanelCallback(action="coupons_list").pack())
builder.row(build_admin_back_btn())
return builder.as_markup()
@@ -32,12 +24,8 @@ def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup:
coupon_code = coupon["code"]
builder.button(
text=f"❌ Удалить {coupon_code}",
callback_data=AdminCouponDeleteCallback(
coupon_code=coupon_code
).pack(),
callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code).pack(),
)
builder.row(
build_admin_back_btn("coupons")
)
builder.row(build_admin_back_btn("coupons"))
return builder.as_markup()
+16 -58
View File
@@ -1,5 +1,5 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -9,76 +9,36 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"):
def build_panel_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="👤 Поиск пользователя",
callback_data=AdminPanelCallback(action="search_user").pack()
)
builder.button(
text="🔑 Поиск по названию ключа",
callback_data=AdminPanelCallback(action="search_key").pack()
)
builder.button(text="👤 Поиск пользователя", callback_data=AdminPanelCallback(action="search_user").pack())
builder.button(text="🔑 Поиск по названию ключа", callback_data=AdminPanelCallback(action="search_key").pack())
builder.row(
InlineKeyboardButton(
text="🖥 Серверы",
callback_data=AdminPanelCallback(action="servers").pack()
),
InlineKeyboardButton(
text="🎟️ Купоны",
callback_data=AdminPanelCallback(action="coupons").pack()
)
)
builder.button(
text="📢 Рассылка",
callback_data=AdminPanelCallback(action="sender").pack()
InlineKeyboardButton(text="🖥️ Серверы", callback_data=AdminPanelCallback(action="servers").pack()),
InlineKeyboardButton(text="🎟 Купоны", callback_data=AdminPanelCallback(action="coupons").pack()),
)
builder.button(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack())
builder.row(
InlineKeyboardButton(
text="📊 Статистика",
callback_data=AdminPanelCallback(action="stats").pack()
),
InlineKeyboardButton(
text="🤖 Управление",
callback_data=AdminPanelCallback(action="management").pack()
)
)
builder.button(
text="Личный кабинет",
callback_data="profile"
InlineKeyboardButton(text="📊 Статистика", callback_data=AdminPanelCallback(action="stats").pack()),
InlineKeyboardButton(text="🤖 Управление", callback_data=AdminPanelCallback(action="management").pack()),
)
builder.button(text="Личный кабинет", callback_data="profile")
builder.adjust(1, 1, 2, 1, 2, 1)
return builder.as_markup()
def build_management_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="💾 Создать резервную копию",
callback_data=AdminPanelCallback(action="backups").pack()
)
builder.button(
text="🚫 Заблокировавшие бота",
callback_data=AdminPanelCallback(action="bans").pack()
)
builder.button(
text="🔄 Перезагрузить бота",
callback_data=AdminPanelCallback(action="restart").pack()
)
builder.row(
build_admin_back_btn()
)
builder.button(text="💾 Создать резервную копию", callback_data=AdminPanelCallback(action="backups").pack())
builder.button(text="🚫 Заблокировавшие бота", callback_data=AdminPanelCallback(action="bans").pack())
builder.button(text="🔄 Перезагрузить бота", callback_data=AdminPanelCallback(action="restart").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
def build_restart_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="✅ Да, перезагрузить",
callback_data=AdminPanelCallback(action="restart_confirm").pack()
)
builder.row(
build_admin_back_btn()
)
builder.button(text="✅ Да, перезагрузить", callback_data=AdminPanelCallback(action="restart_confirm").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
@@ -89,9 +49,7 @@ def build_admin_back_kb(action: str = "admin") -> InlineKeyboardMarkup:
def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
build_admin_btn(text, action)
)
builder.row(build_admin_btn(text, action))
return builder.as_markup()
+4 -21
View File
@@ -11,26 +11,9 @@ class AdminSenderCallback(CallbackData, prefix="admin_sender"):
def build_sender_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="👥 Все пользователи",
callback_data=AdminSenderCallback(
type="all"
).pack()
)
builder.button(
text="✅ Пользователи с подпиской",
callback_data=AdminSenderCallback(
type="subscribed"
).pack()
)
builder.button(
text="❌ Пользователи без подписки",
callback_data=AdminSenderCallback(
type="unsubscribed"
).pack()
)
builder.row(
build_admin_back_btn()
)
builder.button(text="👥 Все пользователи", callback_data=AdminSenderCallback(type="all").pack())
builder.button(text="✅ Пользователи с подпиской", callback_data=AdminSenderCallback(type="subscribed").pack())
builder.button(text="❌ Пользователи без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
+12 -50
View File
@@ -16,19 +16,11 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup:
for cluster_name in servers:
builder.button(
text=f"⚙️ {cluster_name}",
callback_data=AdminServerEditorCallback(
action="clusters_manage",
data=cluster_name
).pack()
callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack(),
)
builder.button(
text=" Добавить кластер",
callback_data=AdminPanelCallback(action="clusters_add").pack()
)
builder.row(
build_admin_back_btn()
)
builder.button(text=" Добавить кластер", callback_data=AdminPanelCallback(action="clusters_add").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
@@ -39,36 +31,22 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark
for server in cluster_servers:
builder.button(
text=f"🌍 {server['server_name']}",
callback_data=AdminServerEditorCallback(
action="servers_manage",
data=server["server_name"]
).pack()
callback_data=AdminServerEditorCallback(action="servers_manage", data=server["server_name"]).pack(),
)
builder.button(
text=" Добавить сервер",
callback_data=AdminServerEditorCallback(
action="servers_add",
data=cluster_name
).pack()
callback_data=AdminServerEditorCallback(action="servers_add", data=cluster_name).pack(),
)
builder.button(
text="🌐 Доступность серверов",
callback_data=AdminServerEditorCallback(
action="servers_availability",
data=cluster_name
).pack()
callback_data=AdminServerEditorCallback(action="servers_availability", data=cluster_name).pack(),
)
builder.button(
text="💾 Создать бэкап кластера",
callback_data=AdminServerEditorCallback(
action="clusters_backup",
data=cluster_name
).pack()
)
builder.row(
build_admin_back_btn("servers")
callback_data=AdminServerEditorCallback(action="clusters_backup", data=cluster_name).pack(),
)
builder.row(build_admin_back_btn("servers"))
builder.adjust(1)
return builder.as_markup()
@@ -76,18 +54,10 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark
def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🗑️ Удалить",
callback_data=AdminServerEditorCallback(
action="servers_delete",
data=server_name
).pack()
text="🗑️ Удалить", callback_data=AdminServerEditorCallback(action="servers_delete", data=server_name).pack()
)
builder.button(
text="🔙 Назад",
callback_data=AdminServerEditorCallback(
action="clusters_manage",
data=cluster_name
).pack()
text="🔙 Назад", callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack()
)
builder.adjust(1)
return builder.as_markup()
@@ -96,18 +66,10 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar
def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="✅ Да",
callback_data=AdminServerEditorCallback(
action="servers_delete_confirm",
data=server_name
).pack()
text="✅ Да", callback_data=AdminServerEditorCallback(action="servers_delete_confirm", data=server_name).pack()
)
builder.button(
text="🔙 Назад",
callback_data=AdminServerEditorCallback(
action="servers_manage",
data=server_name
).pack()
text="🔙 Назад", callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack()
)
builder.adjust(1)
return builder.as_markup()
+5 -11
View File
@@ -1,25 +1,19 @@
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
def build_stats_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🔄 Обновить",
callback_data=AdminPanelCallback(action="stats").pack()
)
builder.button(text="🔄 Обновить", callback_data=AdminPanelCallback(action="stats").pack())
builder.button(
text="📥 Выгрузить пользователей в CSV",
callback_data=AdminPanelCallback(action="stats_export_users_csv").pack()
callback_data=AdminPanelCallback(action="stats_export_users_csv").pack(),
)
builder.button(
text="📥 Выгрузить оплаты в CSV",
callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack()
)
builder.row(
build_admin_back_btn()
text="📥 Выгрузить оплаты в CSV", callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack()
)
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
+33 -147
View File
@@ -32,47 +32,25 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup:
days = (expiry - current_time).days
builder.button(
text=f"🔑 {email} ({'<1' if days < 1 else days} дн.)",
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=str(email)
).pack()
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=str(email)).pack(),
)
builder.button(
text="✉️ Сообщение",
callback_data=AdminUserEditorCallback(
action="users_send_message",
tg_id=tg_id
).pack()
text="✉️ Сообщение", callback_data=AdminUserEditorCallback(action="users_send_message", tg_id=tg_id).pack()
)
builder.button(
text="💸 Изменить баланс",
callback_data=AdminUserEditorCallback(
action="users_balance_edit",
tg_id=tg_id
).pack()
callback_data=AdminUserEditorCallback(action="users_balance_edit", tg_id=tg_id).pack(),
)
builder.button(
text="♻️ Восстановить триал",
callback_data=AdminUserEditorCallback(
action="users_trial_restore",
tg_id=tg_id
).pack()
callback_data=AdminUserEditorCallback(action="users_trial_restore", tg_id=tg_id).pack(),
)
builder.button(
text="❌ Удалить клиента",
callback_data=AdminUserEditorCallback(
action="users_delete_user",
tg_id=tg_id
).pack()
)
builder.row(
build_editor_btn("🔄 Обновить данные", tg_id, edit=True)
)
builder.row(
build_admin_back_btn()
text="❌ Удалить клиента", callback_data=AdminUserEditorCallback(action="users_delete_user", tg_id=tg_id).pack()
)
builder.row(build_editor_btn("🔄 Обновить данные", tg_id, edit=True))
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
@@ -81,10 +59,7 @@ def build_users_balance_change_kb(tg_id: int) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🔙 Назад", # todo: fix magic text was set
callback_data=AdminUserEditorCallback(
action="users_balance_edit",
tg_id=tg_id
).pack()
callback_data=AdminUserEditorCallback(action="users_balance_edit", tg_id=tg_id).pack(),
)
return builder.as_markup()
@@ -94,44 +69,23 @@ def build_users_balance_kb(tg_id: int) -> InlineKeyboardMarkup:
for month, amount in RENEWAL_PRICES.items():
builder.button(
text=f"+ {amount}Р ({month} мес.)",
callback_data=AdminUserEditorCallback(
action="users_balance_add",
tg_id=tg_id,
data=amount
).pack()
callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=amount).pack(),
)
builder.button(
text=f"- {amount}Р ({month} мес.)",
callback_data=AdminUserEditorCallback(
action="users_balance_add",
tg_id=tg_id,
data=-amount
).pack()
callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=-amount).pack(),
)
builder.button(
text="💵 Добавить",
callback_data=AdminUserEditorCallback(
action="users_balance_add",
tg_id=tg_id
).pack()
text="💵 Добавить", callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id).pack()
)
builder.button(
text="💵 Вычесть",
callback_data=AdminUserEditorCallback(
action="users_balance_take",
tg_id=tg_id
).pack()
text="💵 Вычесть", callback_data=AdminUserEditorCallback(action="users_balance_take", tg_id=tg_id).pack()
)
builder.button(
text="💵 Установить баланс",
callback_data=AdminUserEditorCallback(
action="users_balance_set",
tg_id=tg_id
).pack()
)
builder.row(
build_editor_back_btn(tg_id, True)
callback_data=AdminUserEditorCallback(action="users_balance_set", tg_id=tg_id).pack(),
)
builder.row(build_editor_back_btn(tg_id, True))
builder.adjust(2, 2, 2, 2, 2, 1)
return builder.as_markup()
@@ -140,12 +94,7 @@ def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🔙 Назад", # todo: fix magic text was set
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=email,
edit=True
).pack()
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email, edit=True).pack(),
)
return builder.as_markup()
@@ -156,53 +105,25 @@ def build_users_key_expiry_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
month = int(month)
builder.button(
text=f"+ {month} мес.",
callback_data=AdminUserKeyEditorCallback(
action="add",
tg_id=tg_id,
data=email,
month=month
).pack()
callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email, month=month).pack(),
)
builder.button(
text=f"- {month} мес.",
callback_data=AdminUserKeyEditorCallback(
action="add",
tg_id=tg_id,
data=email,
month=-month
).pack()
callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email, month=-month).pack(),
)
builder.button(
text="⏳ Добавить дни",
callback_data=AdminUserKeyEditorCallback(
action="add",
tg_id=tg_id,
data=email
).pack()
text="⏳ Добавить дни", callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email).pack()
)
builder.button(
text="⏳ Вычесть дни",
callback_data=AdminUserKeyEditorCallback(
action="take",
tg_id=tg_id,
data=email
).pack()
text="⏳ Вычесть дни", callback_data=AdminUserKeyEditorCallback(action="take", tg_id=tg_id, data=email).pack()
)
builder.button(
text="⏳ Установить дату истечения",
callback_data=AdminUserKeyEditorCallback(
action="set",
tg_id=tg_id,
data=email
).pack()
callback_data=AdminUserKeyEditorCallback(action="set", tg_id=tg_id, data=email).pack(),
)
builder.button(
text="🔙 Назад", # todo: fix magic text was set
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=email
).pack()
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
)
builder.adjust(2, 2, 2, 2, 2, 1)
return builder.as_markup()
@@ -212,14 +133,9 @@ def build_user_delete_kb(tg_id: int):
builder = InlineKeyboardBuilder()
builder.button(
text="❌ Да, удалить!",
callback_data=AdminUserEditorCallback(
action="users_delete_user_confirm",
tg_id=tg_id
).pack()
)
builder.row(
build_editor_back_btn(tg_id, True)
callback_data=AdminUserEditorCallback(action="users_delete_user_confirm", tg_id=tg_id).pack(),
)
builder.row(build_editor_back_btn(tg_id, True))
builder.adjust(1)
return builder.as_markup()
@@ -227,12 +143,7 @@ def build_user_delete_kb(tg_id: int):
def build_user_key_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text=f"🔙 Назад",
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=email
).pack()
text="🔙 Назад", callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack()
)
builder.adjust(1)
return builder.as_markup()
@@ -243,30 +154,18 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup:
builder.button(
text="⏳ Время истечения",
callback_data=AdminUserEditorCallback(
action="users_expiry_edit",
data=email,
tg_id=key_details["tg_id"]
).pack()
action="users_expiry_edit", data=email, tg_id=key_details["tg_id"]
).pack(),
)
builder.button(
text="🔄 Перевыпустить",
callback_data=AdminUserEditorCallback(
action="users_update_key",
data=email,
tg_id=key_details["tg_id"]
).pack()
callback_data=AdminUserEditorCallback(action="users_update_key", data=email, tg_id=key_details["tg_id"]).pack(),
)
builder.button(
text="❌ Удалить",
callback_data=AdminUserEditorCallback(
action="users_delete_key",
data=email,
tg_id=key_details["tg_id"]
).pack()
)
builder.row(
build_editor_back_btn(key_details["tg_id"], True)
callback_data=AdminUserEditorCallback(action="users_delete_key", data=email, tg_id=key_details["tg_id"]).pack(),
)
builder.row(build_editor_back_btn(key_details["tg_id"], True))
builder.adjust(1)
return builder.as_markup()
@@ -276,16 +175,10 @@ def build_key_delete_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
builder.row(
InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=AdminUserEditorCallback(
action="users_delete_key_confirm",
data=email,
tg_id=tg_id
).pack()
callback_data=AdminUserEditorCallback(action="users_delete_key_confirm", data=email, tg_id=tg_id).pack(),
)
)
builder.row(
build_editor_back_btn(tg_id)
)
builder.row(build_editor_back_btn(tg_id))
builder.adjust(1)
return builder.as_markup()
@@ -296,9 +189,7 @@ def build_editor_kb(tg_id: int, edit: bool = False) -> InlineKeyboardMarkup:
def build_editor_singleton_kb(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
build_editor_btn(text, tg_id, edit)
)
builder.row(build_editor_btn(text, tg_id, edit))
return builder.as_markup()
@@ -308,10 +199,5 @@ def build_editor_back_btn(tg_id: int, edit: bool = False) -> InlineKeyboardButto
def build_editor_btn(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardButton:
return InlineKeyboardButton(
text=text,
callback_data=AdminUserEditorCallback(
action="users_editor",
tg_id=tg_id,
edit=edit
).pack()
text=text, callback_data=AdminUserEditorCallback(action="users_editor", tg_id=tg_id, edit=edit).pack()
)
+25
View File
@@ -0,0 +1,25 @@
from typing import Any
from aiogram import Dispatcher
from .admin import AdminMiddleware
from .delete import DeleteMessageMiddleware
from .loggings import LoggingMiddleware
from .session import SessionMiddleware
from .throttling import ThrottlingMiddleware
from .user import UserMiddleware
def register_middleware(dispatcher: Dispatcher) -> None:
middlewares = [
AdminMiddleware(),
SessionMiddleware(),
DeleteMessageMiddleware(),
LoggingMiddleware(),
ThrottlingMiddleware(),
UserMiddleware(),
]
for middleware in middlewares:
dispatcher.message.outer_middleware(middleware)
dispatcher.callback_query.outer_middleware(middleware)
+9 -13
View File
@@ -9,6 +9,7 @@ from keyboards.admin.panel_kb import AdminPanelCallback
from keyboards.admin.sender_kb import AdminSenderCallback
from keyboards.admin.servers_kb import AdminServerEditorCallback
from keyboards.admin.users_kb import AdminUserEditorCallback, AdminUserKeyEditorCallback
from logger import logger
pass_callbacks = [
AdminPanelCallback,
@@ -22,22 +23,17 @@ pass_callbacks = [
class DeleteMessageMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
if isinstance(event, Message):
if (
not event.text
or not event.text.startswith("/start")
):
if not event.text or not event.text.startswith("/start"):
try:
await event.bot.delete_message(
event.chat.id, event.message_id - 1
)
except Exception:
pass
await event.bot.delete_message(event.chat.id, event.message_id - 1)
except Exception as e:
logger.error(e)
await event.delete()
if isinstance(event, CallbackQuery):
@@ -8,7 +8,7 @@ from aiogram.types import TelegramObject
from config import DATABASE_URL
class DatabaseMiddleware(BaseMiddleware):
class SessionMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
+51 -6
View File
@@ -1,13 +1,31 @@
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, MutableMapping
from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from aiogram.dispatcher.flags import get_flag
from aiogram.types import TelegramObject, Update, User
from cachetools import TTLCache
from logger import logger
class ThrottleMiddleware(BaseMiddleware):
def __init__(self, limit: int):
self.limit = limit
class ThrottlingMiddleware(BaseMiddleware):
def __init__(
self,
*,
default_key: str | None = "default",
default_ttl: float = 0.5,
**ttl_map: float,
) -> None:
if default_key:
ttl_map[default_key] = default_ttl
self.default_key = default_key
self.caches: dict[str, MutableMapping[int, None]] = {}
for name, ttl in ttl_map.items():
self.caches[name] = TTLCache(maxsize=10_000, ttl=ttl)
logger.debug("ThrottlingMiddleware initialized.")
async def __call__(
self,
@@ -15,5 +33,32 @@ class ThrottleMiddleware(BaseMiddleware):
event: TelegramObject,
data: dict[str, Any],
) -> Any:
#todo
if not isinstance(event, Update):
logger.debug(f"Received event of type {type(event)}, skipping throttling.")
return await handler(event, data)
if event.pre_checkout_query:
logger.debug("Pre-checkout query event, skipping throttling.")
return await handler(event, data)
if event.message and event.message.successful_payment:
logger.debug("Successful payment event, skipping throttling.")
return await handler(event, data)
user: User | None = data.get("event_from_user", None)
if user is not None:
key = get_flag(data, "throttling_key", default=self.default_key)
if key:
if user.id in self.caches[key]:
logger.warning(f"User {user.id} is being throttled with key: {key}")
return None
logger.debug(
f"User {user.id} is allowed to proceed, adding to cache with key: {key}",
)
self.caches[key][user.id] = None
else:
logger.debug(f"No throttling key provided for user {user.id}, proceeding without throttle.")
return await handler(event, data)
+3 -3
View File
@@ -1,10 +1,10 @@
[tool.ruff]
line-length = 120
target-version = "py310"
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "ANN", "ASYNC", "S", "BLE", "FBT", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE"]
ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001']
ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001','W291','ANN401','DTZ003','DTZ005','F401','FBT002','FBT001','FBT003','A005','E501','UP017','DTZ004','W293','ANN202','DTZ007']
exclude = [
".git",
"venv",
@@ -14,7 +14,7 @@ exclude = [
[tool.ruff.format]
quote-style = "double"
indent-style = "tab"
indent-style = "space"
[tool.darker]
src = ["."]
+3 -1
View File
@@ -32,4 +32,6 @@ sqlalchemy
robokassa
ping3
ruff
pytz
pytz
cachetools
babel
+24 -64
View File
@@ -9,16 +9,14 @@ from ping3 import ping
from bot import bot
from config import ADMIN_ID, DATABASE_URL, PING_TIME
from database import get_servers_from_db
from database import check_unique_server_name, create_server, get_servers
from logger import logger
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:
@@ -37,35 +33,21 @@ async def sync_servers_with_db():
logger.info("Подключение к базе данных для синхронизации серверов успешно.")
for cluster_name, servers in CLUSTERS.items():
for server_key, server_info in servers.items():
exists = await conn.fetchval(
"""
SELECT 1 FROM servers
WHERE cluster_name = $1 AND server_name = $2
""",
cluster_name,
server_info["name"],
)
for _server_key, server_info in servers.items():
exists = await check_unique_server_name(server_info["name"], conn, cluster_name)
if not exists:
await conn.execute(
"""
INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id)
VALUES ($1, $2, $3, $4, $5)
""",
cluster_name,
server_info["name"],
server_info["API_URL"],
server_info["SUBSCRIPTION"],
server_info["INBOUND_ID"],
)
logger.info(
f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных."
await create_server(
cluster_name=cluster_name,
server_name=server_info["name"],
api_url=server_info["API_URL"],
subscription_url=server_info["SUBSCRIPTION"],
inbound_id=server_info["INBOUND_ID"],
session=conn,
)
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 +86,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(
@@ -132,12 +103,9 @@ async def notify_admin(server_name: str):
"Проверьте соединение к серверу, подключение к панели или удалите его из таблицы серверов в боте, "
"чтобы не выдать подписку к неработающему серверу."
),
parse_mode="HTML",
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:
@@ -149,7 +117,7 @@ async def check_servers():
Периодическая проверка серверов с учетом извлечения хоста из `api_url`.
"""
while True:
servers = await get_servers_from_db()
servers = await get_servers()
current_time = datetime.now()
logger.info(f"Начинаю проверку серверов: {current_time}")
@@ -161,9 +129,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)
@@ -171,18 +137,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)
+2 -8
View File
@@ -35,10 +35,7 @@ async def export_users_csv(session: Any) -> BufferedInputFile:
# Перемещение указателя в начало для чтения
buffer.seek(0)
return BufferedInputFile(
file=buffer.getvalue().encode("utf-8-sig"),
filename="users_export.csv"
)
return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename="users_export.csv")
async def export_payments_csv(session: Any) -> BufferedInputFile:
@@ -95,7 +92,4 @@ def _export_payments_csv(payments: list, filename: str) -> BufferedInputFile:
# Перемещение указателя в начало для чтения
buffer.seek(0)
return BufferedInputFile(
file=buffer.getvalue().encode("utf-8-sig"),
filename=filename
)
return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename=filename)