Рефакторинг и оптимизация импортов в модулях проекта
- Упорядочивание и группировка импортов в соответствии с PEP 8 - Удаление неиспользуемых импортов - Перемещение локальных импортов в начало списка - Добавление пустых строк для улучшения читаемости - Систематизация порядка импортов в различных модулях проекта
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,6 +1,5 @@
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from config import ADMIN_ID
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,7 +1,6 @@
|
||||
__all__ = ("router",)
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
from config import (
|
||||
CRYPTO_BOT_ENABLE,
|
||||
ROBOKASSA_ENABLE,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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="💾 Создать бэкап кластера",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user