@@ -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
@@ -54,5 +54,6 @@ scripts
|
||||
models.py
|
||||
Dockerfile
|
||||
.csv
|
||||
|
||||
setup.py
|
||||
/logs
|
||||
setup.py
|
||||
.ruff_cache
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)} пользователях и связанных записях.",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,6 +1,6 @@
|
||||
GIFT = "🎁 Подарить подписку"
|
||||
MY_GIFTS = "🎁 Мои подарки"
|
||||
PROFILE = "👤 Личный кабинет"
|
||||
BACK = "🔙 Назад"
|
||||
BACK = "⬅️ Назад"
|
||||
GIFTS_ABOUT = "<b>Дарите подарки и следите, чтобы они дошли до адресата! 🎄</b>"
|
||||
SHARE_GIFT = "🎁 Поделиться подарком"
|
||||
|
||||
@@ -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} рублей. Перейдите по ссылке для оплаты:"
|
||||
|
||||
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"))
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
@@ -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 минута")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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
@@ -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]],
|
||||
@@ -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
@@ -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
@@ -32,4 +32,6 @@ sqlalchemy
|
||||
robokassa
|
||||
ping3
|
||||
ruff
|
||||
pytz
|
||||
pytz
|
||||
cachetools
|
||||
babel
|
||||
+24
-64
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user