Merge pull request #160 from izzzzzi/main
Рефакторинг и оптимизация обработки подписок
This commit is contained in:
@@ -1,14 +1,24 @@
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
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 bot import bot
|
||||
from logger import logger
|
||||
|
||||
|
||||
async def backup_database() -> Exception | None:
|
||||
"""
|
||||
Создает резервную копию базы данных и отправляет ее администраторам.
|
||||
|
||||
Returns:
|
||||
Optional[Exception]: Исключение в случае ошибки или None при успешном выполнении
|
||||
"""
|
||||
backup_file_path, exception = _create_database_backup()
|
||||
|
||||
if exception:
|
||||
@@ -30,16 +40,25 @@ async def backup_database() -> Exception | None:
|
||||
|
||||
|
||||
def _create_database_backup() -> tuple[str | None, Exception | None]:
|
||||
"""
|
||||
Создает резервную копию базы данных PostgreSQL.
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[str], Optional[Exception]]: Путь к файлу бэкапа и исключение (если произошла ошибка)
|
||||
"""
|
||||
date_formatted = datetime.now().strftime("%Y-%m-%d-%H%M%S")
|
||||
|
||||
if not os.path.exists(BACK_DIR):
|
||||
os.makedirs(BACK_DIR)
|
||||
# Создаем директорию для бэкапов, если она не существует
|
||||
backup_dir = Path(BACK_DIR)
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
filename = os.path.join(BACK_DIR, f"{DB_NAME}-backup-{date_formatted}.sql")
|
||||
filename = backup_dir / f"{DB_NAME}-backup-{date_formatted}.sql"
|
||||
|
||||
try:
|
||||
# Устанавливаем пароль PostgreSQL через переменную окружения
|
||||
os.environ["PGPASSWORD"] = DB_PASSWORD
|
||||
|
||||
# Запускаем pg_dump для создания бэкапа
|
||||
subprocess.run(
|
||||
[
|
||||
"pg_dump",
|
||||
@@ -52,61 +71,94 @@ def _create_database_backup() -> tuple[str | None, Exception | None]:
|
||||
"-F",
|
||||
"c",
|
||||
"-f",
|
||||
filename,
|
||||
str(filename),
|
||||
DB_NAME,
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
logger.info(f"Бэкап базы данных создан: {filename}")
|
||||
return filename, None
|
||||
return str(filename), None
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка при выполнении pg_dump: {e.stderr}")
|
||||
return None, e
|
||||
except Exception as e:
|
||||
logger.error(f"Непредвиденная ошибка при создании бэкапа: {e}")
|
||||
return None, e
|
||||
finally:
|
||||
del os.environ["PGPASSWORD"]
|
||||
# Удаляем переменную окружения с паролем
|
||||
if "PGPASSWORD" in os.environ:
|
||||
del os.environ["PGPASSWORD"]
|
||||
|
||||
|
||||
def _cleanup_old_backups() -> None | Exception:
|
||||
def _cleanup_old_backups() -> Exception | None:
|
||||
"""
|
||||
Удаляет бэкапы старше 3 дней.
|
||||
|
||||
Returns:
|
||||
Optional[Exception]: Исключение в случае ошибки или None при успешном выполнении
|
||||
"""
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"find",
|
||||
BACK_DIR,
|
||||
"-type",
|
||||
"f",
|
||||
"-name",
|
||||
"*.sql",
|
||||
"-mtime",
|
||||
"+3",
|
||||
"-exec",
|
||||
"rm",
|
||||
"{}",
|
||||
";",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
logger.info("Старые бэкапы удалены.")
|
||||
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():
|
||||
file_mtime = datetime.fromtimestamp(backup_file.stat().st_mtime)
|
||||
if file_mtime < cutoff_date:
|
||||
backup_file.unlink()
|
||||
logger.info(f"Удален старый бэкап: {backup_file}")
|
||||
|
||||
logger.info("Очистка старых бэкапов завершена")
|
||||
return None
|
||||
except subprocess.CalledProcessError as e:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
|
||||
return e
|
||||
|
||||
|
||||
async def create_backup_and_send_to_admins(xui) -> None:
|
||||
await xui.login()
|
||||
await xui.database.export()
|
||||
async def create_backup_and_send_to_admins(client) -> None:
|
||||
"""
|
||||
Создает бэкап и отправляет администраторам через переданный клиент.
|
||||
|
||||
Args:
|
||||
client: Клиент для работы с базой данных
|
||||
"""
|
||||
await client.login()
|
||||
await client.database.export()
|
||||
|
||||
|
||||
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:
|
||||
import aiofiles
|
||||
|
||||
from bot import bot
|
||||
|
||||
async with aiofiles.open(backup_file_path, "rb") as backup_file:
|
||||
backup_data = await backup_file.read()
|
||||
backup_input_file = BufferedInputFile(file=backup_data, filename=os.path.basename(backup_file_path))
|
||||
admin_ids = ADMIN_ID if isinstance(ADMIN_ID, list) else [ADMIN_ID]
|
||||
for admin_id in admin_ids:
|
||||
await bot.send_document(chat_id=admin_id, document=backup_input_file)
|
||||
logger.info(f"Бэкап базы данных отправлен админу: {admin_id}")
|
||||
filename = os.path.basename(backup_file_path)
|
||||
backup_input_file = BufferedInputFile(file=backup_data, filename=filename)
|
||||
|
||||
# Отправляем файл каждому администратору
|
||||
for admin_id in ADMIN_ID:
|
||||
try:
|
||||
await bot.send_document(chat_id=admin_id, document=backup_input_file)
|
||||
logger.info(f"Бэкап базы данных отправлен админу: {admin_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить бэкап админу {admin_id}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
|
||||
raise
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
+39
-11
@@ -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
|
||||
@@ -1080,7 +1086,8 @@ async def upsert_user(
|
||||
last_name: str = None,
|
||||
language_code: str = None,
|
||||
is_bot: bool = False,
|
||||
):
|
||||
session: Any = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Обновляет или вставляет информацию о пользователе в базу данных.
|
||||
|
||||
@@ -1091,16 +1098,29 @@ async def upsert_user(
|
||||
last_name (str, optional): Фамилия пользователя
|
||||
language_code (str, optional): Код языка пользователя
|
||||
is_bot (bool, optional): Флаг, указывающий является ли пользователь ботом
|
||||
session (Any, optional): Существующая сессия базы данных
|
||||
|
||||
Returns:
|
||||
dict: Словарь с информацией о пользователе после обновления/вставки
|
||||
|
||||
Raises:
|
||||
Exception: В случае ошибки при работе с базой данных
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}")
|
||||
close_conn = False
|
||||
|
||||
await conn.execute(
|
||||
try:
|
||||
# Используем переданную сессию или создаем новое подключение
|
||||
if session:
|
||||
conn = session
|
||||
logger.debug(f"Используем существующую сессию для обновления пользователя {tg_id}")
|
||||
else:
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
close_conn = True
|
||||
logger.info(f"Установлено новое подключение к базе данных для обновления пользователя {tg_id}")
|
||||
|
||||
# Выполняем вставку/обновление и сразу получаем обновленные данные
|
||||
user_data = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO users (tg_id, username, first_name, last_name, language_code, is_bot, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
@@ -1112,6 +1132,9 @@ async def upsert_user(
|
||||
language_code = COALESCE(EXCLUDED.language_code, users.language_code),
|
||||
is_bot = EXCLUDED.is_bot,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING
|
||||
id, tg_id, username, first_name, last_name, language_code,
|
||||
is_bot, created_at, updated_at
|
||||
""",
|
||||
tg_id,
|
||||
username,
|
||||
@@ -1120,14 +1143,19 @@ async def upsert_user(
|
||||
language_code,
|
||||
is_bot,
|
||||
)
|
||||
logger.info(f"Успешно обновлена информация о пользователе {tg_id}")
|
||||
|
||||
logger.debug(f"Успешно обновлена информация о пользователе {tg_id}")
|
||||
|
||||
# Преобразуем результат в словарь
|
||||
return dict(user_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении информации о пользователе {tg_id}: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
# Закрываем соединение только если мы его создали
|
||||
if conn and close_conn:
|
||||
await conn.close()
|
||||
logger.info("Закрытие подключения к базе данных")
|
||||
logger.debug("Закрытие подключения к базе данных")
|
||||
|
||||
|
||||
async def add_payment(tg_id: int, amount: float, payment_system: str):
|
||||
|
||||
+1
-4
@@ -1,14 +1,11 @@
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from config import ADMIN_ID
|
||||
|
||||
|
||||
class IsAdminFilter(BaseFilter):
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
try:
|
||||
admin_ids: int | list[int] = ADMIN_ID
|
||||
if isinstance(admin_ids, list):
|
||||
return message.from_user.id in admin_ids
|
||||
return message.from_user.id in ADMIN_ID
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -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,
|
||||
|
||||
+215
-101
@@ -5,11 +5,12 @@ import re
|
||||
import time
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
import aiohttp
|
||||
import asyncpg
|
||||
import pytz
|
||||
from aiohttp import web
|
||||
|
||||
from config import (
|
||||
DATABASE_URL,
|
||||
PROJECT_NAME,
|
||||
@@ -21,75 +22,91 @@ 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
|
||||
|
||||
db_pool = None
|
||||
|
||||
# Функции для работы с URL и подписками
|
||||
async def fetch_url_content(url: str, identifier: str) -> list[str]:
|
||||
"""
|
||||
Получает содержимое подписки по URL и декодирует его.
|
||||
|
||||
async def init_db_pool():
|
||||
"""Инициализация пула соединений, если он ещё не создан."""
|
||||
global db_pool
|
||||
if not db_pool:
|
||||
db_pool = await asyncpg.create_pool(dsn=DATABASE_URL, min_size=5, max_size=20)
|
||||
Args:
|
||||
url: URL для получения содержимого
|
||||
identifier: Идентификатор пользователя (tg_id или email)
|
||||
|
||||
|
||||
async def fetch_url_content(url, tg_id):
|
||||
"""Получает содержимое подписки по URL и декодирует его."""
|
||||
Returns:
|
||||
Список строк из подписки
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Получение URL: {url} для tg_id: {tg_id}")
|
||||
logger.info(f"Получение URL: {url} для идентификатора: {identifier}")
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url, ssl=False) as response:
|
||||
if response.status == 200:
|
||||
content = await response.text()
|
||||
logger.info(f"Успешно получен контент с {url} для tg_id: {tg_id}")
|
||||
logger.info(f"Успешно получен контент с {url} для идентификатора: {identifier}")
|
||||
return base64.b64decode(content).decode("utf-8").split("\n")
|
||||
else:
|
||||
logger.error(f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}")
|
||||
logger.error(
|
||||
f"Не удалось получить {url} для идентификатора: {identifier}, статус: {response.status}"
|
||||
)
|
||||
return []
|
||||
except TimeoutError:
|
||||
logger.error(f"Таймаут при получении {url} для tg_id: {tg_id}")
|
||||
logger.error(f"Таймаут при получении {url} для идентификатора: {identifier}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}")
|
||||
logger.error(f"Ошибка при получении {url} для идентификатора: {identifier}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def combine_unique_lines(urls, tg_id, query_string):
|
||||
"""Объединяет строки подписки, удаляя дубликаты."""
|
||||
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:
|
||||
Список уникальных строк из всех подписок
|
||||
"""
|
||||
if SUPERNODE:
|
||||
logger.info(f"Режим SUPERNODE активен. Возвращаем первую ссылку для tg_id: {tg_id}")
|
||||
logger.info(f"Режим SUPERNODE активен. Возвращаем первую ссылку для идентификатора: {identifier}")
|
||||
if not urls:
|
||||
return []
|
||||
url_with_query = f"{urls[0]}?{query_string}" if query_string else urls[0]
|
||||
return await fetch_url_content(url_with_query, tg_id)
|
||||
return await fetch_url_content(url_with_query, identifier)
|
||||
|
||||
logger.info(f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}")
|
||||
logger.info(f"Начинаем объединение подписок для идентификатора: {identifier}, запрос: {query_string}")
|
||||
urls_with_query = [f"{url}?{query_string}" if query_string else url for url in urls]
|
||||
logger.info(f"Составлены URL-адреса: {urls_with_query}")
|
||||
|
||||
tasks = [fetch_url_content(url, tg_id) for url in urls_with_query]
|
||||
tasks = [fetch_url_content(url, identifier) for url in urls_with_query]
|
||||
results = await asyncio.gather(*tasks)
|
||||
all_lines = set()
|
||||
for lines in results:
|
||||
all_lines.update(filter(None, lines))
|
||||
logger.info(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}")
|
||||
logger.info(
|
||||
f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для идентификатора: {identifier}"
|
||||
)
|
||||
return list(all_lines)
|
||||
|
||||
|
||||
transition_date = datetime.strptime(TRANSITION_DATE_STR, "%Y-%m-%d %H:%M:%S")
|
||||
transition_timestamp_ms = int(transition_date.timestamp() * 1000)
|
||||
transition_timestamp_ms_adjusted = transition_timestamp_ms - (3 * 60 * 60 * 1000)
|
||||
logger.info(f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}")
|
||||
|
||||
|
||||
async def get_subscription_urls(server_id: str, email: str, conn) -> list:
|
||||
async def get_subscription_urls(server_id: str, email: str, conn) -> list[str]:
|
||||
"""
|
||||
Универсальная функция, которая в зависимости от флага USE_COUNTRY_SELECTION
|
||||
получает список URL-адресов для подписки. Возвращает пустой список, если
|
||||
нужные данные не найдены.
|
||||
Получает список URL-адресов для подписки в зависимости от режима выбора страны.
|
||||
|
||||
Args:
|
||||
server_id: Идентификатор сервера или кластера
|
||||
email: Email пользователя
|
||||
conn: Соединение с базой данных
|
||||
|
||||
Returns:
|
||||
Список URL-адресов для подписки
|
||||
"""
|
||||
if USE_COUNTRY_SELECTION:
|
||||
logger.info(f"Режим выбора страны активен. Ищем сервер {server_id} в БД.")
|
||||
@@ -102,7 +119,7 @@ async def get_subscription_urls(server_id: str, email: str, conn) -> list:
|
||||
logger.info(f"Используем подписку {urls[0]}")
|
||||
return urls
|
||||
|
||||
servers = await get_servers()
|
||||
servers = await get_servers(conn)
|
||||
logger.info(f"Режим выбора страны отключен. Используем кластер {server_id}.")
|
||||
cluster_servers = servers.get(server_id, [])
|
||||
if not cluster_servers:
|
||||
@@ -113,20 +130,49 @@ async def get_subscription_urls(server_id: str, email: str, conn) -> list:
|
||||
return urls
|
||||
|
||||
|
||||
def calculate_traffic(cleaned_subscriptions, expiry_time_ms):
|
||||
def get_transition_timestamp() -> int:
|
||||
"""
|
||||
Получает временную метку перехода с учетом часового пояса Москвы.
|
||||
|
||||
Returns:
|
||||
Временная метка перехода в миллисекундах
|
||||
"""
|
||||
moscow_tz = pytz.timezone("Europe/Moscow")
|
||||
transition_date_naive = datetime.strptime(TRANSITION_DATE_STR, "%Y-%m-%d %H:%M:%S")
|
||||
transition_date = moscow_tz.localize(transition_date_naive)
|
||||
transition_timestamp_ms = int(transition_date.timestamp() * 1000)
|
||||
return transition_timestamp_ms
|
||||
|
||||
|
||||
# Функции для обработки и форматирования данных
|
||||
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)
|
||||
@@ -135,10 +181,12 @@ def calculate_traffic(cleaned_subscriptions, expiry_time_ms):
|
||||
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:
|
||||
@@ -148,8 +196,116 @@ def calculate_traffic(cleaned_subscriptions, expiry_time_ms):
|
||||
return f"upload=0; download={consumed_traffic_bytes}; total={total_traffic_bytes}; expire={expire_timestamp}"
|
||||
|
||||
|
||||
async def handle_subscription(request, old_subscription=False):
|
||||
"""Обрабатывает запрос на подписку (старую или новую)."""
|
||||
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: 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]:
|
||||
"""
|
||||
Подготавливает заголовки ответа в зависимости от User-Agent клиента.
|
||||
|
||||
Args:
|
||||
user_agent: User-Agent клиента
|
||||
project_name: Название проекта
|
||||
subscription_info: Информация о подписке
|
||||
subscription_userinfo: Информация о трафике
|
||||
|
||||
Returns:
|
||||
Словарь с заголовками ответа
|
||||
"""
|
||||
if "Happ" in user_agent:
|
||||
encoded_project_name = f"{project_name}"
|
||||
announce_str = f"↖️Бот | {subscription_info} | Поддержка↗️"
|
||||
return {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": "inline",
|
||||
"profile-update-interval": "3",
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
|
||||
"support-url": SUPPORT_CHAT_URL,
|
||||
"announce": "base64:" + base64.b64encode(announce_str.encode("utf-8")).decode("utf-8"),
|
||||
"profile-web-page-url": f"https://t.me/{USERNAME_BOT}",
|
||||
"subscription-userinfo": subscription_userinfo,
|
||||
}
|
||||
elif "Hiddify" in user_agent:
|
||||
encoded_project_name = f"{project_name}\n📄 Подписка: {subscription_info.split(' - ')[0].split(': ')[1]}"
|
||||
return {
|
||||
"profile-update-interval": "3",
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
|
||||
"subscription-userinfo": subscription_userinfo,
|
||||
}
|
||||
else:
|
||||
encoded_project_name = f"{project_name}\n{subscription_info}"
|
||||
return {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": "inline",
|
||||
"profile-update-interval": "3",
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
|
||||
}
|
||||
|
||||
|
||||
# Основные обработчики запросов
|
||||
async def handle_subscription(request: web.Request, old_subscription: bool = False) -> web.Response:
|
||||
"""
|
||||
Обрабатывает запрос на подписку (старую или новую).
|
||||
|
||||
Args:
|
||||
request: Объект запроса
|
||||
old_subscription: Флаг, указывающий на тип подписки (старая или новая)
|
||||
|
||||
Returns:
|
||||
Ответ с подпиской или сообщением об ошибке
|
||||
"""
|
||||
email = request.match_info.get("email")
|
||||
tg_id = request.match_info.get("tg_id") if not old_subscription else None
|
||||
|
||||
@@ -160,9 +316,9 @@ async def handle_subscription(request, old_subscription=False):
|
||||
logger.info(
|
||||
f"Обработка запроса для {'старого' if old_subscription else 'нового'} клиента: email={email}, tg_id={tg_id}"
|
||||
)
|
||||
await init_db_pool()
|
||||
|
||||
async with db_pool.acquire() as conn:
|
||||
async with asyncpg.connect(DATABASE_URL) as conn:
|
||||
# Получение данных клиента
|
||||
client_data = await get_key_details(email, conn)
|
||||
if not client_data:
|
||||
logger.warning(f"Клиент с email {email} не найден в базе.")
|
||||
@@ -171,103 +327,61 @@ async def handle_subscription(request, old_subscription=False):
|
||||
stored_tg_id = client_data.get("tg_id")
|
||||
server_id = client_data["server_id"]
|
||||
|
||||
# Проверка tg_id для новых клиентов
|
||||
if not old_subscription and str(tg_id) != str(stored_tg_id):
|
||||
logger.warning(f"Неверный tg_id для клиента с email {email}.")
|
||||
return web.Response(text="❌ Неверные данные. Получите свой ключ в боте.", status=403)
|
||||
|
||||
# Проверка даты создания для старых клиентов
|
||||
if old_subscription:
|
||||
created_at_ms = client_data["created_at"]
|
||||
# Используем pytz для корректного сравнения времени
|
||||
created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000)
|
||||
logger.info(f"created_at для {email}: {created_at_datetime}, server_id: {server_id}")
|
||||
if created_at_ms >= transition_timestamp_ms_adjusted:
|
||||
|
||||
# Получаем временную метку перехода
|
||||
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)
|
||||
|
||||
# Форматирование оставшегося времени
|
||||
expiry_time_ms = client_data.get("expiry_time")
|
||||
if expiry_time_ms:
|
||||
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)
|
||||
time_left = f"{days}D,{hours}H ⏳" if days else f"{hours}H ⏳"
|
||||
else:
|
||||
time_left = "N/A"
|
||||
time_left = format_time_left(expiry_time_ms)
|
||||
|
||||
# Получение URL-адресов подписки
|
||||
urls = await get_subscription_urls(server_id, email, conn)
|
||||
if not urls:
|
||||
return web.Response(text="❌ Сервер не найден.", status=404)
|
||||
|
||||
# Получение и обработка подписок
|
||||
query_string = request.query_string if not old_subscription else ""
|
||||
combined_subscriptions = await combine_unique_lines(urls, tg_id or email, query_string)
|
||||
random.shuffle(combined_subscriptions)
|
||||
|
||||
cleaned_subscriptions = []
|
||||
for line in combined_subscriptions:
|
||||
if "#" in line:
|
||||
try:
|
||||
base, meta = line.split("#", 1)
|
||||
except ValueError:
|
||||
cleaned_line = line
|
||||
else:
|
||||
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
|
||||
cleaned_line = base + "#" + meta_clean
|
||||
else:
|
||||
cleaned_line = line
|
||||
cleaned_subscriptions.append(cleaned_line)
|
||||
# Очистка строк подписки
|
||||
cleaned_subscriptions = [clean_subscription_line(line) for line in combined_subscriptions]
|
||||
|
||||
final_subscriptions = cleaned_subscriptions
|
||||
base64_encoded = base64.b64encode("\n".join(final_subscriptions).encode("utf-8")).decode("utf-8")
|
||||
# Кодирование подписки в base64
|
||||
base64_encoded = base64.b64encode("\n".join(cleaned_subscriptions).encode("utf-8")).decode("utf-8")
|
||||
subscription_info = f"📄 Подписка: {email} - {time_left}"
|
||||
|
||||
# Подготовка заголовков ответа
|
||||
user_agent = request.headers.get("User-Agent", "")
|
||||
subscription_userinfo = calculate_traffic(cleaned_subscriptions, expiry_time_ms)
|
||||
if "Happ" in user_agent:
|
||||
encoded_project_name = f"{PROJECT_NAME}"
|
||||
support_username = SUPPORT_CHAT_URL.split("https://t.me/")[-1]
|
||||
announce_str = f"↖️Бот | {subscription_info} | Поддержка↗️"
|
||||
headers = {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": "inline",
|
||||
"profile-update-interval": "3",
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
|
||||
"support-url": SUPPORT_CHAT_URL,
|
||||
"announce": "base64:" + base64.b64encode(announce_str.encode("utf-8")).decode("utf-8"),
|
||||
"profile-web-page-url": f"https://t.me/{USERNAME_BOT}",
|
||||
"subscription-userinfo": subscription_userinfo,
|
||||
}
|
||||
elif "Hiddify" in user_agent:
|
||||
encoded_project_name = f"{PROJECT_NAME}\n📄 Подписка: {email}"
|
||||
headers = {
|
||||
"profile-update-interval": "3",
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
|
||||
"subscription-userinfo": subscription_userinfo,
|
||||
}
|
||||
else:
|
||||
encoded_project_name = f"{PROJECT_NAME}\n{subscription_info}"
|
||||
headers = {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": "inline",
|
||||
"profile-update-interval": "3",
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
|
||||
}
|
||||
headers = prepare_headers(user_agent, PROJECT_NAME, subscription_info, subscription_userinfo)
|
||||
|
||||
logger.info(f"Возвращаем объединенные подписки для email: {email}")
|
||||
return web.Response(text=base64_encoded, headers=headers)
|
||||
|
||||
|
||||
async def handle_old_subscription(request):
|
||||
async def handle_old_subscription(request: web.Request) -> web.Response:
|
||||
"""Обработка запроса для старых клиентов."""
|
||||
return await handle_subscription(request, old_subscription=True)
|
||||
|
||||
|
||||
async def handle_new_subscription(request):
|
||||
async def handle_new_subscription(request: web.Request) -> web.Response:
|
||||
"""Обработка запроса для новых клиентов."""
|
||||
return await handle_subscription(request, old_subscription=False)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
+45
-11
@@ -1,6 +1,8 @@
|
||||
from typing import Any
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Optional, Type, Union
|
||||
|
||||
from aiogram import Dispatcher
|
||||
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||
|
||||
from .admin import AdminMiddleware
|
||||
from .loggings import LoggingMiddleware
|
||||
@@ -9,16 +11,48 @@ from .throttling import ThrottlingMiddleware
|
||||
from .user import UserMiddleware
|
||||
|
||||
|
||||
def register_middleware(dispatcher: Dispatcher) -> None:
|
||||
middlewares = [
|
||||
AdminMiddleware(),
|
||||
SessionMiddleware(),
|
||||
LoggingMiddleware(),
|
||||
ThrottlingMiddleware(),
|
||||
UserMiddleware(),
|
||||
def register_middleware(
|
||||
dispatcher: Dispatcher,
|
||||
middlewares: Iterable[BaseMiddleware | type[BaseMiddleware]] | None = None,
|
||||
exclude: Iterable[str] | None = None,
|
||||
) -> None:
|
||||
"""Регистрирует middleware в диспетчере.
|
||||
|
||||
Args:
|
||||
dispatcher: Экземпляр диспетчера Aiogram
|
||||
middlewares: Опциональный список middleware для регистрации.
|
||||
Если не указан, регистрируются все стандартные middleware.
|
||||
exclude: Опциональный список имен middleware, которые нужно исключить из регистрации.
|
||||
Применяется только если middlewares не указан.
|
||||
"""
|
||||
# Если middleware не указаны, используем стандартный набор
|
||||
if middlewares is None:
|
||||
# Словарь всех доступных middleware
|
||||
available_middlewares = {
|
||||
"admin": AdminMiddleware(),
|
||||
"session": SessionMiddleware(),
|
||||
"logging": LoggingMiddleware(),
|
||||
"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]
|
||||
|
||||
# Регистрируем middleware для всех типов обработчиков
|
||||
handlers = [
|
||||
dispatcher.message,
|
||||
dispatcher.callback_query,
|
||||
dispatcher.inline_query,
|
||||
# Можно добавить другие типы обработчиков при необходимости
|
||||
]
|
||||
|
||||
# Регистрируем каждый middleware для каждого типа обработчика
|
||||
for middleware in middlewares:
|
||||
dispatcher.message.outer_middleware(middleware)
|
||||
dispatcher.callback_query.outer_middleware(middleware)
|
||||
dispatcher.inline_query.outer_middleware(middleware)
|
||||
# Если передан класс, а не экземпляр, создаем экземпляр
|
||||
if isinstance(middleware, type):
|
||||
middleware = middleware()
|
||||
|
||||
for handler in handlers:
|
||||
handler.outer_middleware(middleware)
|
||||
|
||||
+37
-7
@@ -1,27 +1,57 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from typing import Any, Dict, Set, cast
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import 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}
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
"""Обрабатывает событие и добавляет флаг администратора в data.
|
||||
|
||||
Args:
|
||||
handler: Обработчик события
|
||||
event: Событие Telegram
|
||||
data: Словарь с данными события
|
||||
|
||||
Returns:
|
||||
Результат выполнения обработчика
|
||||
"""
|
||||
data["admin"] = self._check_admin_access(event)
|
||||
return await handler(event, data)
|
||||
|
||||
def _check_admin_access(self, event: TelegramObject) -> bool:
|
||||
"""Проверяет, имеет ли пользователь права администратора.
|
||||
|
||||
Args:
|
||||
event: Событие Telegram
|
||||
|
||||
Returns:
|
||||
True, если пользователь администратор, иначе False
|
||||
"""
|
||||
try:
|
||||
admin_ids: int | list[int] = ADMIN_ID
|
||||
if isinstance(admin_ids, list):
|
||||
return event.from_user.id in admin_ids
|
||||
return event.from_user.id == admin_ids
|
||||
if isinstance(event, Message):
|
||||
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)
|
||||
return user_id in self._admin_ids if user_id else False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
+41
-28
@@ -1,13 +1,21 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from typing import Any, Dict, Optional, TypedDict, Union
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import CallbackQuery, InlineQuery, Message, TelegramObject
|
||||
from aiogram.types import CallbackQuery, InlineQuery, Message, TelegramObject, User
|
||||
|
||||
from logger import logger
|
||||
|
||||
|
||||
class UserInfo(TypedDict):
|
||||
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]],
|
||||
@@ -16,33 +24,38 @@ class LoggingMiddleware(BaseMiddleware):
|
||||
) -> Any:
|
||||
user_info = self._extract_user_info(event)
|
||||
|
||||
logger.info(
|
||||
f"Активность пользователя - "
|
||||
f"ID пользователя: {user_info['user_id']}, "
|
||||
f"Имя пользователя: {user_info['username']}, "
|
||||
f"Действие: {user_info['action']}"
|
||||
)
|
||||
if user_info["user_id"]:
|
||||
logger.info(
|
||||
f"Активность пользователя - "
|
||||
f"ID пользователя: {user_info['user_id']}, "
|
||||
f"Имя пользователя: {user_info['username'] or 'Не указано'}, "
|
||||
f"Действие: {user_info['action'] or 'Неизвестно'}"
|
||||
)
|
||||
|
||||
return await handler(event, data)
|
||||
|
||||
def _extract_user_info(self, event: TelegramObject) -> dict[str, str | None]:
|
||||
user_id = None
|
||||
username = None
|
||||
action = None
|
||||
def _extract_user_info(self, event: TelegramObject) -> UserInfo:
|
||||
"""Извлекает информацию о пользователе из различных типов событий.
|
||||
|
||||
if isinstance(event, Message):
|
||||
user = event.from_user
|
||||
user_id = user.id
|
||||
username = user.username
|
||||
action = f"Сообщение: {event.text}"
|
||||
elif isinstance(event, CallbackQuery):
|
||||
user = event.from_user
|
||||
user_id = user.id
|
||||
username = user.username
|
||||
action = f"Обратный вызов: {event.data}"
|
||||
elif isinstance(event, InlineQuery):
|
||||
user = event.from_user
|
||||
user_id = user.id
|
||||
username = user.username
|
||||
action = f"Inline запрос: {event.query}"
|
||||
Args:
|
||||
event: Событие Telegram
|
||||
|
||||
return {"user_id": user_id, "username": username, "action": action}
|
||||
Returns:
|
||||
Словарь с информацией о пользователе
|
||||
"""
|
||||
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}"
|
||||
elif isinstance(event, CallbackQuery):
|
||||
result["action"] = f"Обратный вызов: {event.data}"
|
||||
elif isinstance(event, InlineQuery):
|
||||
result["action"] = f"Inline запрос: {event.query}"
|
||||
|
||||
return result
|
||||
|
||||
+16
-6
@@ -1,23 +1,33 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
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: asyncpg.Pool | None = None
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
# Создаем пул соединений при первом вызове
|
||||
if self.pool is None:
|
||||
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)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@classmethod
|
||||
async def close(cls) -> None:
|
||||
"""Закрыть пул соединений при завершении работы приложения."""
|
||||
if cls.pool is not None:
|
||||
await cls.pool.close()
|
||||
cls.pool = None
|
||||
|
||||
+66
-21
@@ -1,5 +1,5 @@
|
||||
from collections.abc import Awaitable, Callable, MutableMapping
|
||||
from typing import Any
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.dispatcher.flags import get_flag
|
||||
@@ -10,22 +10,61 @@ from logger import logger
|
||||
|
||||
|
||||
class ThrottlingMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для ограничения частоты запросов от пользователей.
|
||||
Позволяет настраивать разные временные интервалы для разных типов запросов.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
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: Время ограничения по умолчанию в секундах
|
||||
cache_size: Максимальный размер кэша для каждого ключа
|
||||
**ttl_map: Словарь с ключами и временем ограничения
|
||||
"""
|
||||
# Добавляем ключ по умолчанию в карту TTL, если он указан
|
||||
if default_key:
|
||||
ttl_map[default_key] = default_ttl
|
||||
|
||||
self.default_key = default_key
|
||||
self.caches: dict[str, MutableMapping[int, None]] = {}
|
||||
self.cache_size = cache_size
|
||||
|
||||
# Инициализация кэшей для каждого ключа
|
||||
for name, ttl in ttl_map.items():
|
||||
self.caches[name] = TTLCache(maxsize=10_000, ttl=ttl)
|
||||
logger.debug("ThrottlingMiddleware initialized.")
|
||||
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__(
|
||||
self,
|
||||
@@ -33,32 +72,38 @@ class ThrottlingMiddleware(BaseMiddleware):
|
||||
event: TelegramObject,
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
# Проверяем, что событие является экземпляром Update
|
||||
if not isinstance(event, Update):
|
||||
logger.debug(f"Received event of type {type(event)}, skipping throttling.")
|
||||
logger.debug(f"Skipping throttling for non-Update event: {type(event).__name__}")
|
||||
return await handler(event, data)
|
||||
|
||||
if event.pre_checkout_query:
|
||||
logger.debug("Pre-checkout query event, skipping throttling.")
|
||||
# Проверяем, нужно ли пропустить ограничение
|
||||
if self._should_skip_throttling(event):
|
||||
logger.debug("Skipping throttling for special event type")
|
||||
return await handler(event, data)
|
||||
|
||||
if event.message and event.message.successful_payment:
|
||||
logger.debug("Successful payment event, skipping throttling.")
|
||||
# Получаем пользователя из данных события
|
||||
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)
|
||||
|
||||
user: User | None = data.get("event_from_user", None)
|
||||
# Получаем ключ ограничения из флагов или используем ключ по умолчанию
|
||||
key = get_flag(data, "throttling_key", default=self.default_key)
|
||||
|
||||
if user is not None:
|
||||
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 key:
|
||||
if user.id in self.caches[key]:
|
||||
logger.warning(f"User {user.id} is being throttled with key: {key}")
|
||||
return None
|
||||
logger.debug(
|
||||
f"User {user.id} is allowed to proceed, adding to cache with key: {key}",
|
||||
)
|
||||
self.caches[key][user.id] = None
|
||||
else:
|
||||
logger.debug(f"No throttling key provided for user {user.id}, proceeding without throttle.")
|
||||
# Проверяем, находится ли пользователь в кэше (т.е. ограничен)
|
||||
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)
|
||||
|
||||
+39
-4
@@ -5,25 +5,60 @@ from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, User
|
||||
|
||||
from database import upsert_user
|
||||
from logger import logger
|
||||
|
||||
|
||||
class UserMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для обработки информации о пользователе.
|
||||
Сохраняет или обновляет данные пользователя в базе данных.
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
if user := data.get("event_from_user"):
|
||||
await self._process_user(user)
|
||||
try:
|
||||
# Получаем пользователя из данных события
|
||||
if user := data.get("event_from_user"):
|
||||
# Получаем сессию из контекста, если она есть
|
||||
session = data.get("session")
|
||||
# Обрабатываем пользователя и получаем его данные из БД
|
||||
db_user = await self._process_user(user, session)
|
||||
# Добавляем данные пользователя из БД в контекст
|
||||
if db_user:
|
||||
data["user"] = db_user
|
||||
except Exception as e:
|
||||
# Логируем ошибку, но не прерываем обработку события
|
||||
logger.error(f"Ошибка при обработке пользователя: {e}")
|
||||
|
||||
# Продолжаем обработку события в любом случае
|
||||
return await handler(event, data)
|
||||
|
||||
async def _process_user(self, user: User) -> None:
|
||||
await upsert_user(
|
||||
async def _process_user(self, user: User, session: Any = None) -> dict:
|
||||
"""
|
||||
Обрабатывает информацию о пользователе и сохраняет её в базу данных.
|
||||
|
||||
Args:
|
||||
user (User): Объект пользователя Telegram
|
||||
session (Any, optional): Сессия базы данных, если доступна
|
||||
|
||||
Returns:
|
||||
dict: Словарь с информацией о пользователе из базы данных
|
||||
"""
|
||||
logger.debug(f"Обработка пользователя: {user.id}")
|
||||
# Получаем данные пользователя из БД после вставки/обновления
|
||||
user_data = await upsert_user(
|
||||
tg_id=user.id,
|
||||
username=user.username,
|
||||
first_name=user.first_name,
|
||||
last_name=user.last_name,
|
||||
language_code=user.language_code,
|
||||
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