Рефакторинг и оптимизация импортов в модулях проекта

- Упорядочивание и группировка импортов в соответствии с PEP 8
- Удаление неиспользуемых импортов
- Перемещение локальных импортов в начало списка
- Добавление пустых строк для улучшения читаемости
- Систематизация порядка импортов в различных модулях проекта
This commit is contained in:
izzzzzi
2025-02-25 04:14:58 +05:00
parent c1dc6efb4c
commit 0657136ae5
34 changed files with 208 additions and 221 deletions
+27 -22
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -2,8 +2,8 @@ from dataclasses import dataclass
from typing import Any
import py3xui
from config import LIMIT_IP, SUPERNODE
from logger import logger
+12 -6
View File
@@ -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:
-1
View File
@@ -1,6 +1,5 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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()
+2 -2
View File
@@ -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="<b>🎫 Введите код купона:</b>\n\n"
"📝 Пожалуйста, введите действующий код купона, который вы хотите активировать. 🔑",
"📝 Пожалуйста, введите действующий код купона, который вы хотите активировать. 🔑",
reply_markup=builder.as_markup(),
media_path=None
media_path=None,
)
await state.set_state(CouponActivationState.waiting_for_coupon_code)
+1 -1
View File
@@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RUB_TO_XTR
from logger import logger
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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,
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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,
+47 -42
View File
@@ -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)
@@ -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,
@@ -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,
-1
View File
@@ -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,
-1
View File
@@ -1,7 +1,6 @@
__all__ = ("router",)
from aiogram import Router
from config import (
CRYPTO_BOT_ENABLE,
ROBOKASSA_ENABLE,
+29 -42
View File
@@ -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(),
)
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
+2 -2
View File
@@ -1,5 +1,5 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
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="💾 Создать бэкап кластера",
+1 -1
View File
@@ -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
+10 -13
View File
@@ -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)
+13 -14
View File
@@ -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
+15 -19
View File
@@ -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
+5 -10
View File
@@ -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:
"""Закрыть пул соединений при завершении работы приложения."""
+17 -17
View File
@@ -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)
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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