diff --git a/backup.py b/backup.py index c838fdcf..c7a81649 100644 --- a/backup.py +++ b/backup.py @@ -4,18 +4,18 @@ from datetime import datetime, timedelta from pathlib import Path from typing import Optional, Tuple, Union -from aiogram.types import BufferedInputFile import aiofiles - +from aiogram.types import BufferedInputFile from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT -from logger import logger + from bot import bot +from logger import logger -async def backup_database() -> Optional[Exception]: +async def backup_database() -> Exception | None: """ Создает резервную копию базы данных и отправляет ее администраторам. - + Returns: Optional[Exception]: Исключение в случае ошибки или None при успешном выполнении """ @@ -39,10 +39,10 @@ async def backup_database() -> Optional[Exception]: return e -def _create_database_backup() -> Tuple[Optional[str], Optional[Exception]]: +def _create_database_backup() -> tuple[str | None, Exception | None]: """ Создает резервную копию базы данных PostgreSQL. - + Returns: Tuple[Optional[str], Optional[Exception]]: Путь к файлу бэкапа и исключение (если произошла ошибка) """ @@ -62,11 +62,16 @@ def _create_database_backup() -> Tuple[Optional[str], Optional[Exception]]: subprocess.run( [ "pg_dump", - "-U", DB_USER, - "-h", PG_HOST, - "-p", PG_PORT, - "-F", "c", - "-f", str(filename), + "-U", + DB_USER, + "-h", + PG_HOST, + "-p", + PG_PORT, + "-F", + "c", + "-f", + str(filename), DB_NAME, ], check=True, @@ -87,10 +92,10 @@ def _create_database_backup() -> Tuple[Optional[str], Optional[Exception]]: del os.environ["PGPASSWORD"] -def _cleanup_old_backups() -> Optional[Exception]: +def _cleanup_old_backups() -> Exception | None: """ Удаляет бэкапы старше 3 дней. - + Returns: Optional[Exception]: Исключение в случае ошибки или None при успешном выполнении """ @@ -98,10 +103,10 @@ def _cleanup_old_backups() -> Optional[Exception]: backup_dir = Path(BACK_DIR) if not backup_dir.exists(): return None - + # Вычисляем дату, старше которой нужно удалить файлы cutoff_date = datetime.now() - timedelta(days=3) - + # Находим и удаляем старые файлы бэкапов for backup_file in backup_dir.glob("*.sql"): if backup_file.is_file(): @@ -109,7 +114,7 @@ def _cleanup_old_backups() -> Optional[Exception]: if file_mtime < cutoff_date: backup_file.unlink() logger.info(f"Удален старый бэкап: {backup_file}") - + logger.info("Очистка старых бэкапов завершена") return None except Exception as e: @@ -120,7 +125,7 @@ def _cleanup_old_backups() -> Optional[Exception]: async def create_backup_and_send_to_admins(client) -> None: """ Создает бэкап и отправляет администраторам через переданный клиент. - + Args: client: Клиент для работы с базой данных """ @@ -131,22 +136,22 @@ async def create_backup_and_send_to_admins(client) -> None: async def _send_backup_to_admins(backup_file_path: str) -> None: """ Отправляет файл бэкапа всем администраторам через Telegram. - + Args: backup_file_path: Путь к файлу бэкапа - + Raises: Exception: При ошибке отправки файла """ if not backup_file_path or not os.path.exists(backup_file_path): raise FileNotFoundError(f"Файл бэкапа не найден: {backup_file_path}") - + try: async with aiofiles.open(backup_file_path, "rb") as backup_file: backup_data = await backup_file.read() filename = os.path.basename(backup_file_path) backup_input_file = BufferedInputFile(file=backup_data, filename=filename) - + # Отправляем файл каждому администратору for admin_id in ADMIN_ID: try: diff --git a/bot.py b/bot.py index d53f1188..ce55c505 100644 --- a/bot.py +++ b/bot.py @@ -8,8 +8,8 @@ from aiogram.filters import ExceptionTypeFilter from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import BufferedInputFile, ErrorEvent from aiogram.utils.markdown import hbold - from config import ADMIN_ID, API_TOKEN + from filters.private import IsPrivateFilter from logger import logger from middlewares import register_middleware diff --git a/client.py b/client.py index 750d5520..ef0643c3 100644 --- a/client.py +++ b/client.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from typing import Any import py3xui - from config import LIMIT_IP, SUPERNODE + from logger import logger diff --git a/database.py b/database.py index 60a2345a..1490e0b7 100644 --- a/database.py +++ b/database.py @@ -4,8 +4,8 @@ from typing import Any import asyncpg import pytz - from config import CASHBACK, CHECK_REFERRAL_REWARD_ISSUED, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES + from logger import logger @@ -842,7 +842,9 @@ async def get_total_referral_bonus(conn, referrer_tg_id: int, max_levels: int) - ORDER BY tg_id, created_at ) """ - bonus_query = bonus_cte + f""" + bonus_query = ( + bonus_cte + + f""" SELECT COALESCE(SUM( CASE @@ -859,6 +861,7 @@ async def get_total_referral_bonus(conn, referrer_tg_id: int, max_levels: int) - JOIN earliest_payments ep ON rl.referred_tg_id = ep.tg_id WHERE rl.level <= {max_levels} """ + ) else: bonus_cte = f""" WITH RECURSIVE @@ -881,7 +884,9 @@ async def get_total_referral_bonus(conn, referrer_tg_id: int, max_levels: int) - WHERE rl.level < {max_levels} ) """ - bonus_query = bonus_cte + f""" + bonus_query = ( + bonus_cte + + f""" SELECT COALESCE(SUM( CASE @@ -898,6 +903,7 @@ async def get_total_referral_bonus(conn, referrer_tg_id: int, max_levels: int) - JOIN payments p ON rl.referred_tg_id = p.tg_id WHERE p.status = 'success' AND rl.level <= {max_levels} """ + ) total_bonus = await conn.fetchval(bonus_query, referrer_tg_id) logger.debug(f"Получена общая сумма бонусов от рефералов: {total_bonus}") return total_bonus @@ -1102,7 +1108,7 @@ async def upsert_user( """ conn = None close_conn = False - + try: # Используем переданную сессию или создаем новое подключение if session: @@ -1137,9 +1143,9 @@ async def upsert_user( language_code, is_bot, ) - + logger.debug(f"Успешно обновлена информация о пользователе {tg_id}") - + # Преобразуем результат в словарь return dict(user_data) except Exception as e: diff --git a/filters/admin.py b/filters/admin.py index b6f6e0a2..d04fa5a3 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -1,6 +1,5 @@ from aiogram.filters import BaseFilter from aiogram.types import Message - from config import ADMIN_ID diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index a2ba1ca1..3dfc16f6 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -4,8 +4,8 @@ from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message - from config import USERNAME_BOT + from database import create_coupon, delete_coupon, get_all_coupons from filters.admin import IsAdminFilter from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 4ddae1a9..73cf9c65 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -6,10 +6,10 @@ from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL 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, delete_server, get_servers from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster diff --git a/handlers/admin/admin_stats.py b/handlers/admin/admin_stats.py index bb5697a8..187f3106 100644 --- a/handlers/admin/admin_stats.py +++ b/handlers/admin/admin_stats.py @@ -1,10 +1,10 @@ from datetime import datetime from typing import Any +import pytz from aiogram import F, Router from aiogram.exceptions import TelegramBadRequest from aiogram.types import CallbackQuery -import pytz from filters.admin import IsAdminFilter from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index 343ab510..c219cf11 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -8,8 +8,8 @@ from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message - from config import TOTAL_GB + from database import ( delete_key, delete_user_data, diff --git a/handlers/captcha.py b/handlers/captcha.py index 66643304..4f38075c 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -6,8 +6,8 @@ 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() diff --git a/handlers/coupons.py b/handlers/coupons.py index ed8a3126..fc230c49 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -38,9 +38,9 @@ async def handle_activate_coupon(callback_query_or_message: Message | CallbackQu await edit_or_send_message( target_message=target_message, text="🎫 Введите код купона:\n\n" - "📝 Пожалуйста, введите действующий код купона, который вы хотите активировать. 🔑", + "📝 Пожалуйста, введите действующий код купона, который вы хотите активировать. 🔑", reply_markup=builder.as_markup(), - media_path=None + media_path=None, ) await state.set_state(CouponActivationState.waiting_for_coupon_code) diff --git a/handlers/donate.py b/handlers/donate.py index 73d6f76e..66c1c392 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import RUB_TO_XTR + from logger import logger diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 0f1ad70e..e8019b72 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -8,8 +8,8 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL + from database import get_key_details from handlers.texts import ( CONNECT_TV_TEXT, diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 3902c216..5afb5f8c 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -8,10 +8,6 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder -from py3xui import AsyncApi - -from bot import bot -from client import delete_client from config import ( ADMIN_PASSWORD, ADMIN_USERNAME, @@ -29,6 +25,10 @@ from config import ( USE_COUNTRY_SELECTION, USE_NEW_PAYMENT_FLOW, ) +from py3xui import AsyncApi + +from bot import bot +from client import delete_client from database import ( add_connection, check_connection_exists, diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 197d5ef7..834de969 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,10 +1,10 @@ import asyncio from typing import Any +from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, USE_COUNTRY_SELECTION from py3xui import AsyncApi from client import ClientConfig, add_client, delete_client, extend_client_key, get_client_traffic, toggle_client -from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, USE_COUNTRY_SELECTION from database import get_servers, store_key from handlers.utils import get_least_loaded_cluster from logger import logger diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 23d4e0c1..1a155bca 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -9,8 +9,6 @@ import pytz from aiogram import F, Router, types from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder - -from bot import bot from config import ( CONNECT_ANDROID, CONNECT_IOS, @@ -26,6 +24,8 @@ from config import ( USE_COUNTRY_SELECTION, USE_NEW_PAYMENT_FLOW, ) + +from bot import bot from database import ( check_server_name_by_cluster, create_temporary_data, diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 022a644f..970858ac 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -11,7 +11,6 @@ import aiohttp import asyncpg import pytz from aiohttp import web - from config import ( DATABASE_URL, PROJECT_NAME, @@ -23,20 +22,21 @@ from config import ( USE_COUNTRY_SELECTION, USERNAME_BOT, ) + from database import get_key_details, get_servers from handlers.utils import convert_to_bytes from logger import logger # Функции для работы с URL и подписками -async def fetch_url_content(url: str, identifier: str) -> List[str]: +async def fetch_url_content(url: str, identifier: str) -> list[str]: """ Получает содержимое подписки по URL и декодирует его. - + Args: url: URL для получения содержимого identifier: Идентификатор пользователя (tg_id или email) - + Returns: Список строк из подписки """ @@ -50,7 +50,9 @@ async def fetch_url_content(url: str, identifier: str) -> List[str]: logger.info(f"Успешно получен контент с {url} для идентификатора: {identifier}") return base64.b64decode(content).decode("utf-8").split("\n") else: - logger.error(f"Не удалось получить {url} для идентификатора: {identifier}, статус: {response.status}") + logger.error( + f"Не удалось получить {url} для идентификатора: {identifier}, статус: {response.status}" + ) return [] except TimeoutError: logger.error(f"Таймаут при получении {url} для идентификатора: {identifier}") @@ -60,15 +62,15 @@ async def fetch_url_content(url: str, identifier: str) -> List[str]: return [] -async def combine_unique_lines(urls: List[str], identifier: str, query_string: str) -> List[str]: +async def combine_unique_lines(urls: list[str], identifier: str, query_string: str) -> list[str]: """ Объединяет строки подписки из нескольких URL, удаляя дубликаты. - + Args: urls: Список URL для получения подписок identifier: Идентификатор пользователя (tg_id или email) query_string: Строка запроса для добавления к URL - + Returns: Список уникальных строк из всех подписок """ @@ -88,19 +90,21 @@ async def combine_unique_lines(urls: List[str], identifier: str, query_string: s all_lines = set() for lines in results: all_lines.update(filter(None, lines)) - logger.info(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для идентификатора: {identifier}") + logger.info( + f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для идентификатора: {identifier}" + ) return list(all_lines) -async def get_subscription_urls(server_id: str, email: str, conn) -> List[str]: +async def get_subscription_urls(server_id: str, email: str, conn) -> list[str]: """ Получает список URL-адресов для подписки в зависимости от режима выбора страны. - + Args: server_id: Идентификатор сервера или кластера email: Email пользователя conn: Соединение с базой данных - + Returns: Список URL-адресов для подписки """ @@ -129,7 +133,7 @@ async def get_subscription_urls(server_id: str, email: str, conn) -> List[str]: def get_transition_timestamp() -> int: """ Получает временную метку перехода с учетом часового пояса Москвы. - + Returns: Временная метка перехода в миллисекундах """ @@ -141,34 +145,34 @@ def get_transition_timestamp() -> int: # Функции для обработки и форматирования данных -def calculate_traffic(cleaned_subscriptions: List[str], expiry_time_ms: Optional[int]) -> str: +def calculate_traffic(cleaned_subscriptions: list[str], expiry_time_ms: int | None) -> str: """ Рассчитывает информацию о трафике на основе подписок. - + Args: cleaned_subscriptions: Список строк подписки expiry_time_ms: Время истечения подписки в миллисекундах - + Returns: Строка с информацией о трафике """ expire_timestamp = int(expiry_time_ms / 1000) if expiry_time_ms else 0 - + if TOTAL_GB != 0: country_remaining = {} for line in cleaned_subscriptions: if "#" not in line: continue - + try: _, meta = line.split("#", 1) except ValueError: continue - + parts = meta.split("-") country = parts[0].strip() remaining_str = parts[1].strip() if len(parts) == 2 else "" - + if remaining_str: remaining_str = remaining_str.replace(",", ".") m_total = re.search(r"([\d\.]+)\s*([GMKTB]B)", remaining_str, re.IGNORECASE) @@ -177,12 +181,12 @@ def calculate_traffic(cleaned_subscriptions: List[str], expiry_time_ms: Optional unit = m_total.group(2).upper() remaining_bytes = convert_to_bytes(value, unit) country_remaining[country] = remaining_bytes - + num_countries = len(country_remaining) issued_per_country = TOTAL_GB total_traffic_bytes = issued_per_country * num_countries consumed_traffic_bytes = total_traffic_bytes - sum(country_remaining.values()) - + if consumed_traffic_bytes < 0: consumed_traffic_bytes = 0 else: @@ -195,67 +199,68 @@ def calculate_traffic(cleaned_subscriptions: List[str], expiry_time_ms: Optional def clean_subscription_line(line: str) -> str: """ Очищает строку подписки, оставляя только нужную информацию. - + Args: line: Исходная строка подписки - + Returns: Очищенная строка подписки """ if "#" not in line: return line - + try: base, meta = line.split("#", 1) except ValueError: return line - + parts = meta.split("-") country = parts[0].strip() if parts else "" traffic = "" - + for part in parts[1:]: part_decoded = urllib.parse.unquote(part).strip() if re.search(r"\d+(?:[.,]\d+)?\s*(?:GB|MB|KB|TB)", part_decoded, re.IGNORECASE): traffic = part_decoded break - + meta_clean = f"{country} - {traffic}" if traffic else country return base + "#" + meta_clean -def format_time_left(expiry_time_ms: Optional[int]) -> str: +def format_time_left(expiry_time_ms: int | None) -> str: """ Форматирует оставшееся время подписки. - + Args: expiry_time_ms: Время истечения подписки в миллисекундах - + Returns: Отформатированная строка с оставшимся временем """ if not expiry_time_ms: return "N/A" - + now_ms = int(time.time() * 1000) remaining_sec = max((expiry_time_ms - now_ms) / 1000, 0) days = int(remaining_sec // 86400) hours = int((remaining_sec % 86400) // 3600) - + return f"{days}D,{hours}H ⏳" if days else f"{hours}H ⏳" -def prepare_headers(user_agent: str, project_name: str, subscription_info: str, - subscription_userinfo: str) -> Dict[str, str]: +def prepare_headers( + user_agent: str, project_name: str, subscription_info: str, subscription_userinfo: str +) -> dict[str, str]: """ Подготавливает заголовки ответа в зависимости от User-Agent клиента. - + Args: user_agent: User-Agent клиента project_name: Название проекта subscription_info: Информация о подписке subscription_userinfo: Информация о трафике - + Returns: Словарь с заголовками ответа """ @@ -293,11 +298,11 @@ def prepare_headers(user_agent: str, project_name: str, subscription_info: str, async def handle_subscription(request: web.Request, old_subscription: bool = False) -> web.Response: """ Обрабатывает запрос на подписку (старую или новую). - + Args: request: Объект запроса old_subscription: Флаг, указывающий на тип подписки (старая или новая) - + Returns: Ответ с подпиской или сообщением об ошибке """ @@ -311,7 +316,7 @@ async def handle_subscription(request: web.Request, old_subscription: bool = Fal logger.info( f"Обработка запроса для {'старого' if old_subscription else 'нового'} клиента: email={email}, tg_id={tg_id}" ) - + async with asyncpg.connect(DATABASE_URL) as conn: # Получение данных клиента client_data = await get_key_details(email, conn) @@ -333,11 +338,11 @@ async def handle_subscription(request: web.Request, old_subscription: bool = Fal # Используем pytz для корректного сравнения времени created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000) logger.info(f"created_at для {email}: {created_at_datetime}, server_id: {server_id}") - + # Получаем временную метку перехода transition_timestamp_ms = get_transition_timestamp() logger.info(f"Время перехода (с учетом часового пояса Москвы): {transition_timestamp_ms}") - + if created_at_ms >= transition_timestamp_ms: logger.info(f"Клиент с email {email} является новым.") return web.Response(text="❌ Эта ссылка устарела. Пожалуйста, обновите ссылку.", status=400) diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index d9cf2152..75af5057 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta import asyncpg import pytz from aiogram import Bot, Router - from config import ( DATABASE_URL, NOTIFICATION_TIME, @@ -17,6 +16,7 @@ from config import ( TOTAL_GB, TRIAL_TIME_DISABLE, ) + from database import ( add_notification, check_notification_time, diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py index ced6f1df..cd48c2fa 100644 --- a/handlers/notifications/special_notifications.py +++ b/handlers/notifications/special_notifications.py @@ -6,8 +6,8 @@ import pytz from aiogram import Bot, Router, types from aiogram.exceptions import TelegramForbiddenError from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME + from database import ( add_notification, check_notification_time, diff --git a/handlers/pay.py b/handlers/pay.py index 3f7604cd..353b5fde 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -1,7 +1,6 @@ from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import ( CRYPTO_BOT_ENABLE, DONATIONS_ENABLE, diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py index e1ccb3fc..ba0f040b 100644 --- a/handlers/payments/__init__.py +++ b/handlers/payments/__init__.py @@ -1,7 +1,6 @@ __all__ = ("router",) from aiogram import Router - from config import ( CRYPTO_BOT_ENABLE, ROBOKASSA_ENABLE, diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 93e8b747..442f707a 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -8,8 +8,6 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web -from robokassa import HashAlgorithm, Robokassa - from config import ( DATABASE_URL, ROBOKASSA_ENABLE, @@ -18,6 +16,8 @@ from config import ( ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE, ) +from robokassa import HashAlgorithm, Robokassa + from database import ( add_connection, add_payment, @@ -28,9 +28,8 @@ from database import ( ) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS -from logger import logger - from handlers.utils import edit_or_send_message +from logger import logger router = Router() @@ -54,9 +53,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, @@ -68,9 +65,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.") @@ -108,16 +103,14 @@ async def process_callback_pay_robokassa( target_message=callback_query.message, text="Выберите сумму пополнения:", reply_markup=builder.as_markup(), - force_text=True + force_text=True, ) await state.set_state(ReplenishBalanceState.choosing_amount_robokassa) logger.info(f"Displayed amount selection for user {tg_id}.") @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("|") @@ -127,7 +120,7 @@ async def process_amount_selection( target_message=callback_query.message, text="Ошибка: данные повреждены.", reply_markup=types.InlineKeyboardMarkup(), - force_text=True + force_text=True, ) return @@ -142,7 +135,7 @@ async def process_amount_selection( target_message=callback_query.message, text="Некорректная сумма.", reply_markup=types.InlineKeyboardMarkup(), - force_text=True + force_text=True, ) return @@ -166,7 +159,7 @@ async def process_amount_selection( target_message=callback_query.message, text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:", reply_markup=confirm_keyboard, - force_text=True + force_text=True, ) logger.info(f"Payment link sent to user {callback_query.message.chat.id}.") @@ -183,9 +176,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("Неверная подпись или данные запроса.") @@ -224,9 +215,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}") @@ -235,9 +224,7 @@ def check_payment_signature(params): @router.callback_query(F.data == "enter_custom_amount_robokassa") -async def process_custom_amount_selection( - callback_query: types.CallbackQuery, state: FSMContext -): +async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} chose to enter a custom amount.") @@ -248,16 +235,16 @@ async def process_custom_amount_selection( target_message=callback_query.message, text="Пожалуйста, введите сумму пополнения.", reply_markup=builder.as_markup(), - force_text=True + force_text=True, ) - 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 target_message = message.message @@ -277,7 +264,7 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer await edit_or_send_message( target_message=target_message, text="Данные для оплаты не найдены. Попробуйте снова.", - reply_markup=types.InlineKeyboardMarkup() + reply_markup=types.InlineKeyboardMarkup(), ) return @@ -288,7 +275,7 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer await edit_or_send_message( target_message=target_message, text="Недостаточная сумма для пополнения.", - reply_markup=types.InlineKeyboardMarkup() + reply_markup=types.InlineKeyboardMarkup(), ) return @@ -300,22 +287,22 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")) 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 edit_or_send_message( target_message=target_message, text="Некорректное состояние данных. Попробуйте снова.", - reply_markup=types.InlineKeyboardMarkup() + reply_markup=types.InlineKeyboardMarkup(), ) return - await edit_or_send_message( - target_message=target_message, - text=message_text, - reply_markup=builder.as_markup() - ) + await edit_or_send_message(target_message=target_message, text=message_text, reply_markup=builder.as_markup()) if isinstance(state, FSMContext): await state.clear() @@ -325,5 +312,5 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer await edit_or_send_message( target_message=target_message, text="Произошла ошибка при создании платежа. Попробуйте позже.", - reply_markup=types.InlineKeyboardMarkup() + reply_markup=types.InlineKeyboardMarkup(), ) diff --git a/handlers/profile.py b/handlers/profile.py index 4805363a..fe1bff39 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -14,7 +14,6 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import ( DATABASE_URL, INLINE_MODE, @@ -25,6 +24,7 @@ from config import ( TRIAL_TIME, USERNAME_BOT, ) + from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial from handlers.buttons.profile import ( ADD_SUB, diff --git a/handlers/start.py b/handlers/start.py index b8793223..9e32037c 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -10,8 +10,6 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder - -from bot import bot from config import ( CAPTCHA_ENABLE, CHANNEL_EXISTS, @@ -21,6 +19,8 @@ from config import ( DONATIONS_ENABLE, SUPPORT_CHAT_URL, ) + +from bot import bot from database import ( add_connection, add_referral, diff --git a/handlers/utils.py b/handlers/utils.py index ffd17160..19d2d972 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -8,9 +8,9 @@ import aiofiles import aiohttp import asyncpg from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message +from config import DATABASE_URL from bot import bot -from config import DATABASE_URL from database import get_all_keys, get_servers from logger import logger diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py index 10118eba..e7d148e8 100644 --- a/keyboards/admin/servers_kb.py +++ b/keyboards/admin/servers_kb.py @@ -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 from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn @@ -46,7 +46,7 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark InlineKeyboardButton( text="🔄 Синхронизация", callback_data=AdminServerEditorCallback(action="clusters_sync", data=cluster_name).pack(), - ) + ), ) builder.button( text="💾 Создать бэкап кластера", diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index be179bc1..52931cf9 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -3,8 +3,8 @@ from datetime import datetime, timezone from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import RENEWAL_PRICES + from keyboards.admin.panel_kb import build_admin_back_btn diff --git a/middlewares/__init__.py b/middlewares/__init__.py index 5b0900d7..f2b1d9d9 100644 --- a/middlewares/__init__.py +++ b/middlewares/__init__.py @@ -1,4 +1,5 @@ -from typing import Any, Iterable, Optional, Type, Union +from collections.abc import Iterable +from typing import Any, Optional, Type, Union from aiogram import Dispatcher from aiogram.dispatcher.middlewares.base import BaseMiddleware @@ -12,11 +13,11 @@ from .user import UserMiddleware def register_middleware( dispatcher: Dispatcher, - middlewares: Optional[Iterable[Union[BaseMiddleware, Type[BaseMiddleware]]]] = None, - exclude: Optional[Iterable[str]] = None, + middlewares: Iterable[BaseMiddleware | type[BaseMiddleware]] | None = None, + exclude: Iterable[str] | None = None, ) -> None: """Регистрирует middleware в диспетчере. - + Args: dispatcher: Экземпляр диспетчера Aiogram middlewares: Опциональный список middleware для регистрации. @@ -34,15 +35,11 @@ def register_middleware( "throttling": ThrottlingMiddleware(), "user": UserMiddleware(), } - + # Фильтруем middleware по списку исключений exclude_set = set(exclude or []) - middlewares = [ - middleware - for name, middleware in available_middlewares.items() - if name not in exclude_set - ] - + middlewares = [middleware for name, middleware in available_middlewares.items() if name not in exclude_set] + # Регистрируем middleware для всех типов обработчиков handlers = [ dispatcher.message, @@ -50,12 +47,12 @@ def register_middleware( dispatcher.inline_query, # Можно добавить другие типы обработчиков при необходимости ] - + # Регистрируем каждый middleware для каждого типа обработчика for middleware in middlewares: # Если передан класс, а не экземпляр, создаем экземпляр if isinstance(middleware, type): middleware = middleware() - + for handler in handlers: handler.outer_middleware(middleware) diff --git a/middlewares/admin.py b/middlewares/admin.py index ef0c00bf..afc2129a 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -2,34 +2,33 @@ from collections.abc import Awaitable, Callable from typing import Any, Dict, Set, cast from aiogram import BaseMiddleware -from aiogram.types import Message, CallbackQuery, TelegramObject - +from aiogram.types import CallbackQuery, Message, TelegramObject from config import ADMIN_ID class AdminMiddleware(BaseMiddleware): """Middleware для проверки прав администратора. - + Добавляет в data['admin'] = True/False в зависимости от того, является ли пользователь администратором. """ - + # Кэш ID администраторов для быстрого доступа - _admin_ids: Set[int] = set(ADMIN_ID) if isinstance(ADMIN_ID, (list, tuple)) else {ADMIN_ID} - + _admin_ids: set[int] = set(ADMIN_ID) if isinstance(ADMIN_ID, (list, tuple)) else {ADMIN_ID} + async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: """Обрабатывает событие и добавляет флаг администратора в data. - + Args: handler: Обработчик события event: Событие Telegram data: Словарь с данными события - + Returns: Результат выполнения обработчика """ @@ -38,10 +37,10 @@ class AdminMiddleware(BaseMiddleware): def _check_admin_access(self, event: TelegramObject) -> bool: """Проверяет, имеет ли пользователь права администратора. - + Args: event: Событие Telegram - + Returns: True, если пользователь администратор, иначе False """ @@ -50,9 +49,9 @@ class AdminMiddleware(BaseMiddleware): return event.from_user and event.from_user.id in self._admin_ids elif isinstance(event, CallbackQuery): return event.from_user and event.from_user.id in self._admin_ids - + # Для других типов событий пытаемся получить from_user - user_id = getattr(getattr(event, 'from_user', None), 'id', None) + user_id = getattr(getattr(event, "from_user", None), "id", None) return user_id in self._admin_ids if user_id else False except Exception: return False diff --git a/middlewares/loggings.py b/middlewares/loggings.py index a62388e6..42035ecc 100644 --- a/middlewares/loggings.py +++ b/middlewares/loggings.py @@ -1,5 +1,5 @@ from collections.abc import Awaitable, Callable -from typing import Any, Dict, Optional, Union, TypedDict +from typing import Any, Dict, Optional, TypedDict, Union from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, InlineQuery, Message, TelegramObject, User @@ -8,22 +8,22 @@ from logger import logger class UserInfo(TypedDict): - user_id: Optional[int] - username: Optional[str] - action: Optional[str] + user_id: int | None + username: str | None + action: str | None class LoggingMiddleware(BaseMiddleware): """Middleware для логирования действий пользователя.""" - + async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: user_info = self._extract_user_info(event) - + if user_info["user_id"]: logger.info( f"Активность пользователя - " @@ -31,29 +31,25 @@ class LoggingMiddleware(BaseMiddleware): f"Имя пользователя: {user_info['username'] or 'Не указано'}, " f"Действие: {user_info['action'] or 'Неизвестно'}" ) - + return await handler(event, data) def _extract_user_info(self, event: TelegramObject) -> UserInfo: """Извлекает информацию о пользователе из различных типов событий. - + Args: event: Событие Telegram - + Returns: Словарь с информацией о пользователе """ - result: UserInfo = { - "user_id": None, - "username": None, - "action": None - } - + result: UserInfo = {"user_id": None, "username": None, "action": None} + # Обработка общих атрибутов для разных типов событий if hasattr(event, "from_user") and isinstance(event.from_user, User): result["user_id"] = event.from_user.id result["username"] = event.from_user.username - + # Определение типа действия в зависимости от типа события if isinstance(event, Message): result["action"] = f"Сообщение: {event.text}" @@ -61,5 +57,5 @@ class LoggingMiddleware(BaseMiddleware): result["action"] = f"Обратный вызов: {event.data}" elif isinstance(event, InlineQuery): result["action"] = f"Inline запрос: {event.query}" - + return result diff --git a/middlewares/session.py b/middlewares/session.py index 6e7c9bc2..c6cfdd55 100644 --- a/middlewares/session.py +++ b/middlewares/session.py @@ -4,13 +4,12 @@ from typing import Any, Optional import asyncpg from aiogram import BaseMiddleware from aiogram.types import TelegramObject - from config import DATABASE_URL class SessionMiddleware(BaseMiddleware): - pool: Optional[asyncpg.Pool] = None - + pool: asyncpg.Pool | None = None + async def __call__( self, handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], @@ -19,17 +18,13 @@ class SessionMiddleware(BaseMiddleware): ) -> Any: # Создаем пул соединений при первом вызове if self.pool is None: - self.pool = await asyncpg.create_pool( - DATABASE_URL, - min_size=5, - max_size=20 - ) - + self.pool = await asyncpg.create_pool(DATABASE_URL, min_size=5, max_size=20) + # Получаем соединение из пула async with self.pool.acquire() as conn: data["session"] = conn return await handler(event, data) - + @classmethod async def close(cls) -> None: """Закрыть пул соединений при завершении работы приложения.""" diff --git a/middlewares/throttling.py b/middlewares/throttling.py index fb683cce..a55b2989 100644 --- a/middlewares/throttling.py +++ b/middlewares/throttling.py @@ -1,5 +1,5 @@ from collections.abc import Awaitable, Callable, MutableMapping -from typing import Any, Optional, Dict, Union +from typing import Any, Dict, Optional, Union from aiogram import BaseMiddleware from aiogram.dispatcher.flags import get_flag @@ -14,18 +14,18 @@ class ThrottlingMiddleware(BaseMiddleware): Middleware для ограничения частоты запросов от пользователей. Позволяет настраивать разные временные интервалы для разных типов запросов. """ - + def __init__( self, *, - default_key: Optional[str] = "default", + default_key: str | None = "default", default_ttl: float = 0.5, cache_size: int = 10_000, **ttl_map: float, ) -> None: """ Инициализация middleware для ограничения частоты запросов. - + Args: default_key: Ключ по умолчанию для ограничения default_ttl: Время ограничения по умолчанию в секундах @@ -37,33 +37,33 @@ class ThrottlingMiddleware(BaseMiddleware): ttl_map[default_key] = default_ttl self.default_key = default_key - self.caches: Dict[str, MutableMapping[int, None]] = {} + self.caches: dict[str, MutableMapping[int, None]] = {} self.cache_size = cache_size # Инициализация кэшей для каждого ключа for name, ttl in ttl_map.items(): self.caches[name] = TTLCache(maxsize=self.cache_size, ttl=ttl) - + logger.debug(f"ThrottlingMiddleware initialized with {len(self.caches)} throttling keys") def _should_skip_throttling(self, event: Update) -> bool: """ Проверяет, нужно ли пропустить ограничение для данного события. - + Args: event: Событие Telegram - + Returns: True, если ограничение следует пропустить """ # Пропускаем предварительные запросы на оплату if event.pre_checkout_query: return True - + # Пропускаем уведомления об успешной оплате if event.message and event.message.successful_payment: return True - + return False async def __call__( @@ -83,27 +83,27 @@ class ThrottlingMiddleware(BaseMiddleware): return await handler(event, data) # Получаем пользователя из данных события - user: Optional[User] = data.get("event_from_user") - + user: User | None = data.get("event_from_user") + if user is None: logger.debug("No user found in event data, proceeding without throttle") return await handler(event, data) - + # Получаем ключ ограничения из флагов или используем ключ по умолчанию key = get_flag(data, "throttling_key", default=self.default_key) - + if not key: logger.debug(f"No throttling key provided for user {user.id}, proceeding without throttle") return await handler(event, data) - + # Проверяем, находится ли пользователь в кэше (т.е. ограничен) if user.id in self.caches[key]: logger.warning(f"User {user.id} is throttled with key: {key}") return None - + # Добавляем пользователя в кэш self.caches[key][user.id] = None logger.debug(f"User {user.id} allowed to proceed with key: {key}") - + # Продолжаем обработку события return await handler(event, data) diff --git a/middlewares/user.py b/middlewares/user.py index 39d33de3..e9a65750 100644 --- a/middlewares/user.py +++ b/middlewares/user.py @@ -13,7 +13,7 @@ class UserMiddleware(BaseMiddleware): Middleware для обработки информации о пользователе. Сохраняет или обновляет данные пользователя в базе данных. """ - + async def __call__( self, handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], @@ -33,18 +33,18 @@ class UserMiddleware(BaseMiddleware): except Exception as e: # Логируем ошибку, но не прерываем обработку события logger.error(f"Ошибка при обработке пользователя: {e}") - + # Продолжаем обработку события в любом случае return await handler(event, data) async def _process_user(self, user: User, session: Any = None) -> dict: """ Обрабатывает информацию о пользователе и сохраняет её в базу данных. - + Args: user (User): Объект пользователя Telegram session (Any, optional): Сессия базы данных, если доступна - + Returns: dict: Словарь с информацией о пользователе из базы данных """ @@ -59,6 +59,6 @@ class UserMiddleware(BaseMiddleware): is_bot=user.is_bot, session=session, # Передаем сессию, если она есть ) - + logger.debug(f"Получены данные пользователя из БД: {user.id}") return user_data diff --git a/servers.py b/servers.py index caa0921f..63ecc5c1 100644 --- a/servers.py +++ b/servers.py @@ -4,10 +4,10 @@ from datetime import datetime, timedelta from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder +from config import ADMIN_ID, DATABASE_URL, PING_TIME from ping3 import ping from bot import bot -from config import ADMIN_ID, DATABASE_URL, PING_TIME from database import get_servers from keyboards.admin.servers_kb import AdminServerEditorCallback from logger import logger