Merge pull request #163 from izzzzzi/main

Рефакторинг и оптимизация кода
This commit is contained in:
Oleg Kraev
2025-03-07 16:13:02 +03:00
committed by GitHub
42 changed files with 263 additions and 127 deletions
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
# Сохраняем текущие изменения
echo "Сохранение текущих изменений..."
git stash -q --keep-index
# Запускаем форматирование кода
echo "Запуск Ruff format..."
ruff format . --config pyproject.toml --exclude main.py,handlers/payments
# Запускаем проверку и исправление кода
echo "Запуск Ruff check с автоисправлением..."
ruff check . --config pyproject.toml --exclude main.py,handlers/payments --fix
# Добавляем изменения, внесенные форматированием
git add -u
# Восстанавливаем сохраненные изменения
echo "Восстановление сохраненных изменений..."
git stash pop -q
# Выход с кодом 0 (успешно)
exit 0
+3 -1
View File
@@ -1,14 +1,16 @@
import os
import subprocess
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 config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT
from logger import logger
+2 -1
View File
@@ -8,12 +8,13 @@ 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
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
+8 -6
View File
@@ -1,9 +1,11 @@
from dataclasses import dataclass
from typing import Any
import httpx
import httpx
import py3xui
from config import LIMIT_IP, SUPERNODE
from logger import logger
@@ -54,7 +56,7 @@ async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, An
logger.info(f"Клиент {config.email} успешно добавлен с ID {config.client_id}")
return response if response else {"status": "failed"}
except httpx.ConnectTimeout as e:
logger.error(f"Ошибка при добавлении клиента {config.email}: {e}")
return {"status": "failed", "error": "Timeout"}
@@ -114,7 +116,7 @@ async def extend_client_key(
await xui.client.reset_stats(inbound_id, email)
logger.info(f"Ключ клиента {email} успешно продлён до {new_expiry_time}")
return True
except httpx.ConnectTimeout as e:
logger.error(f"Ошибка при обновлении клиента {email}: {e}")
return False
@@ -159,7 +161,7 @@ async def delete_client(
await xui.client.delete(inbound_id, client.id)
logger.info(f"Клиент с ID {client_id} был удален успешно")
return True
except httpx.ConnectTimeout as e:
logger.error(f"Ошибка при удалении клиента {email}: {e}")
return False
@@ -190,7 +192,7 @@ async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str,
logger.info(f"Трафик для клиента {client_id} успешно получен.")
return {"status": "success", "client_id": client_id, "traffic": traffic_data}
except httpx.ConnectTimeout as e:
logger.error(f"Ошибка при получении трафика клиента {client_id}: {e}")
return {"status": "error", "error": "Timeout"}
@@ -241,7 +243,7 @@ async def toggle_client(xui: py3xui.AsyncApi, inbound_id: int, email: str, clien
status = "включении" if enable else "отключении"
logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}")
return False
except Exception as e:
status = "включении" if enable else "отключении"
logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}")
+21 -25
View File
@@ -1,4 +1,5 @@
import json
from datetime import datetime
from typing import Any
@@ -6,6 +7,7 @@ import asyncpg
import pytz
from config import CASHBACK, CHECK_REFERRAL_REWARD_ISSUED, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from logger import logger
@@ -674,7 +676,7 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
"""
if amount <= 0:
return
return
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
@@ -852,14 +854,12 @@ async def get_total_referral_bonus(conn, referrer_tg_id: int, max_levels: int) -
COALESCE(SUM(
CASE
{
" ".join(
[
f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * ep.amount"
if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float)
else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}"
for level in REFERRAL_BONUS_PERCENTAGES
]
)
" ".join([
f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * ep.amount"
if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float)
else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}"
for level in REFERRAL_BONUS_PERCENTAGES
])
}
ELSE 0
END
@@ -898,14 +898,12 @@ async def get_total_referral_bonus(conn, referrer_tg_id: int, max_levels: int) -
COALESCE(SUM(
CASE
{
" ".join(
[
f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * p.amount"
if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float)
else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}"
for level in REFERRAL_BONUS_PERCENTAGES
]
)
" ".join([
f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * p.amount"
if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float)
else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}"
for level in REFERRAL_BONUS_PERCENTAGES
])
}
ELSE 0
END
@@ -1339,14 +1337,12 @@ async def get_servers(session: Any = None):
if cluster_name not in servers:
servers[cluster_name] = []
servers[cluster_name].append(
{
"server_name": row["server_name"],
"api_url": row["api_url"],
"subscription_url": row["subscription_url"],
"inbound_id": row["inbound_id"],
}
)
servers[cluster_name].append({
"server_name": row["server_name"],
"api_url": row["api_url"],
"subscription_url": row["subscription_url"],
"inbound_id": row["inbound_id"],
})
return servers
-1
View File
@@ -1,6 +1,5 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
+1
View File
@@ -14,6 +14,7 @@ from .payments import router as payments_router
from .profile import router as profile_router
from .start import router as start_router
router = Router(name="handlers_main_router")
router.include_routers(
+1
View File
@@ -12,6 +12,7 @@ from .admin_servers import router as servers_router
from .admin_stats import router as stats_router
from .admin_users import router as users_router
router = Router(name="admins_main_router")
router.include_routers(
+1
View File
@@ -5,6 +5,7 @@ from backup import backup_database
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
router = Router()
+1
View File
@@ -8,6 +8,7 @@ from filters.admin import IsAdminFilter
from keyboards.admin.bans_kb import build_bans_kb
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
router = Router()
+2 -1
View File
@@ -4,14 +4,15 @@ 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
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from logger import logger
router = Router()
+1
View File
@@ -7,6 +7,7 @@ from bot import version
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_management_kb, build_panel_kb
router = Router()
+1
View File
@@ -6,6 +6,7 @@ from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb, build_restart_kb
router = Router()
+1
View File
@@ -11,6 +11,7 @@ from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.sender_kb import AdminSenderCallback, build_clusters_kb, build_sender_kb
from logger import logger
router = Router()
+4 -1
View File
@@ -1,15 +1,17 @@
import asyncio
from typing import Any
import asyncpg
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
@@ -23,6 +25,7 @@ from keyboards.admin.servers_kb import (
)
from logger import logger
router = Router()
+2
View File
@@ -2,6 +2,7 @@ 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
@@ -12,6 +13,7 @@ from keyboards.admin.stats_kb import build_stats_kb
from logger import logger
from utils.csv_export import export_payments_csv, export_users_csv
router = Router()
+4 -1
View File
@@ -1,15 +1,17 @@
import asyncio
from datetime import datetime, timedelta, timezone
from typing import Any
import pytz
from aiogram import F, Router, types
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,
@@ -40,6 +42,7 @@ from keyboards.admin.users_kb import (
)
from logger import logger
MOSCOW_TZ = pytz.timezone("Europe/Moscow")
router = Router()
+3 -1
View File
@@ -1,17 +1,19 @@
import random
import secrets
from typing import Any
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CAPTCHA_EMOJIS
from logger import logger
from .utils import edit_or_send_message
router = Router()
+1 -1
View File
@@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RUB_TO_XTR
from logger import logger
from .utils import edit_or_send_message
+1
View File
@@ -4,6 +4,7 @@ from aiogram import Router
from .instructions import router as instructions_router
router = Router(name="instructions_main_router")
router.include_routers(
+4 -4
View File
@@ -1,4 +1,5 @@
import os
from typing import Any
from aiogram import F, Router, types
@@ -8,18 +9,19 @@ 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,
INSTRUCTION_PC,
INSTRUCTIONS,
INSTRUCTION_PC,
KEY_MESSAGE,
SUBSCRIPTION_DETAILS_TEXT,
)
from handlers.utils import edit_or_send_message
router = Router()
@@ -80,7 +82,6 @@ async def process_connect_pc(callback_query: CallbackQuery, session: Any):
)
@router.callback_query(F.data.startswith("connect_tv|"))
async def process_connect_tv(callback_query: CallbackQuery):
key_name = callback_query.data.split("|")[1]
@@ -102,7 +103,6 @@ async def process_connect_tv(callback_query: CallbackQuery):
@router.callback_query(F.data.startswith("continue_tv|"))
async def process_continue_tv(callback_query: CallbackQuery, session: Any):
key_name = callback_query.data.split("|")[1]
tg_id = callback_query.from_user.id
record = await get_key_details(key_name, session)
subscription_link = record["key"]
+1
View File
@@ -5,6 +5,7 @@ from aiogram import Router
from .key_management import router as management_router
from .keys import router as keys_router
router = Router(name="keys_main_router")
router.include_routers(
+11 -6
View File
@@ -1,17 +1,15 @@
import asyncio
import uuid
from datetime import datetime, timedelta
from typing import Any
import pytz
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 +27,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,
@@ -55,6 +57,7 @@ from handlers.texts import DISCOUNTS, key_message_success
from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster
from logger import logger
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
@@ -91,9 +94,11 @@ async def handle_key_creation(
updated = await update_trial(tg_id, 1, session)
if updated:
await edit_or_send_message(
target_message=message_or_query if isinstance(message_or_query, Message) else message_or_query.message,
target_message=message_or_query
if isinstance(message_or_query, Message)
else message_or_query.message,
text="⏳ Пожалуйста, подождите, создаем вам подключение...",
reply_markup=None,
reply_markup=None,
)
await create_key(tg_id, expiry_time, state, session, message_or_query)
+2 -1
View File
@@ -1,10 +1,11 @@
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
+6 -3
View File
@@ -1,16 +1,16 @@
import asyncio
import locale
import os
from datetime import datetime, timedelta
from typing import Any
import asyncpg
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 +26,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,
@@ -64,6 +66,7 @@ from handlers.texts import (
from handlers.utils import edit_or_send_message, handle_error
from logger import logger
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
router = Router()
@@ -382,7 +385,7 @@ async def process_callback_confirm_delete(callback_query: CallbackQuery, session
servers = await get_servers(session)
async def delete_key_from_servers():
try: # lol
try: # lol
tasks = []
for cluster_id, _cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
+5 -3
View File
@@ -4,13 +4,14 @@ import random
import re
import time
import urllib.parse
from datetime import datetime
import aiohttp
import asyncpg
import pytz
from aiohttp import web
from aiohttp import web
from config import (
DATABASE_URL,
PROJECT_NAME,
@@ -19,9 +20,10 @@ from config import (
SUPPORT_CHAT_URL,
TOTAL_GB,
TRANSITION_DATE_STR,
USE_COUNTRY_SELECTION,
USERNAME_BOT,
USE_COUNTRY_SELECTION,
)
from database import get_key_details, get_servers
from handlers.utils import convert_to_bytes
from logger import logger
@@ -275,7 +277,7 @@ def prepare_headers(
"subscription-userinfo": subscription_userinfo,
}
elif "Hiddify" in user_agent:
parts = subscription_info.split(' - ')[0].split(': ')
parts = subscription_info.split(" - ")[0].split(": ")
key_info = parts[1] if len(parts) > 1 else parts[0]
encoded_project_name = f"{project_name}\n📄 Подписка: {key_info}"
+1
View File
@@ -5,6 +5,7 @@ from aiogram import Router
from .general_notifications import router as general_notifications_router
from .special_notifications import router as special_notifications_router
router = Router(name="notifications_main_router")
router.include_routers(general_notifications_router, special_notifications_router)
@@ -1,10 +1,11 @@
import asyncio
from datetime import datetime, timedelta
import asyncpg
import pytz
from aiogram import Bot, Router
from aiogram import Bot, Router
from config import (
DATABASE_URL,
NOTIFICATION_TIME,
@@ -17,6 +18,7 @@ from config import (
TOTAL_GB,
TRIAL_TIME_DISABLE,
)
from database import (
add_notification,
check_notification_time,
@@ -35,6 +37,7 @@ from logger import logger
from .notify_utils import send_notification
from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
+64 -33
View File
@@ -1,7 +1,10 @@
import os
from typing import Optional
import aiofiles
from aiogram import Bot, types
from aiogram import Bot
from aiogram.exceptions import TelegramForbiddenError
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup
@@ -13,40 +16,68 @@ async def send_notification(
tg_id: int,
image_filename: str,
caption: str,
keyboard: InlineKeyboardMarkup,
):
keyboard: InlineKeyboardMarkup | None = None,
) -> bool:
"""
Отправляет уведомление с изображением, если файл существует, иначе отправляет текстовое сообщение.
Если возникает TelegramForbiddenError (например, бот заблокирован пользователем),
функция логирует ошибку и прекращает попытки отправки уведомления.
Отправляет уведомление пользователю.
Args:
bot: Экземпляр бота для отправки сообщений
tg_id: Telegram ID пользователя
image_filename: Имя файла изображения в директории img
caption: Текст сообщения
keyboard: Клавиатура для сообщения (опционально)
Returns:
bool: True если сообщение успешно отправлено, False в случае ошибки
"""
photo_path = os.path.join("img", image_filename)
# Проверяем существование файла изображения
if os.path.isfile(photo_path):
try:
async with aiofiles.open(photo_path, "rb") as image_file:
image_data = await image_file.read()
buffered_photo = BufferedInputFile(image_data, filename=image_filename)
await bot.send_photo(tg_id, buffered_photo, caption=caption, reply_markup=keyboard)
except TelegramForbiddenError as e:
logger.error(f"Ошибка отправки фото для пользователя {tg_id}: {e}")
return
except Exception as e:
logger.error(f"Ошибка отправки фото для пользователя {tg_id}: {e}")
try:
await bot.send_message(tg_id, caption, reply_markup=keyboard)
except TelegramForbiddenError as e:
logger.error(f"Ошибка отправки fallback-сообщения для пользователя {tg_id}: {e}")
return
except Exception as e:
logger.error(f"Неизвестная ошибка при отправке fallback-сообщения для пользователя {tg_id}: {e}")
return
return await _send_photo_notification(bot, tg_id, photo_path, image_filename, caption, keyboard)
else:
logger.error(f"Файл с изображением не найден: {photo_path}")
try:
await bot.send_message(tg_id, caption, reply_markup=keyboard)
except TelegramForbiddenError as e:
logger.error(f"Ошибка отправки сообщения для пользователя {tg_id}: {e}")
return
except Exception as e:
logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}")
return
logger.warning(f"Файл с изображением не найден: {photo_path}")
return await _send_text_notification(bot, tg_id, caption, keyboard)
async def _send_photo_notification(
bot: Bot,
tg_id: int,
photo_path: str,
image_filename: str,
caption: str,
keyboard: InlineKeyboardMarkup | None = None,
) -> bool:
"""Отправляет уведомление с изображением."""
try:
async with aiofiles.open(photo_path, "rb") as image_file:
image_data = await image_file.read()
buffered_photo = BufferedInputFile(image_data, filename=image_filename)
await bot.send_photo(tg_id, buffered_photo, caption=caption, reply_markup=keyboard)
return True
except TelegramForbiddenError:
logger.error(f"Пользователь {tg_id} заблокировал бота")
return False
except Exception as e:
logger.error(f"Ошибка отправки фото для пользователя {tg_id}: {e}")
# Пробуем отправить текстовое сообщение как запасной вариант
return await _send_text_notification(bot, tg_id, caption, keyboard)
async def _send_text_notification(
bot: Bot,
tg_id: int,
caption: str,
keyboard: InlineKeyboardMarkup | None = None,
) -> bool:
"""Отправляет текстовое уведомление."""
try:
await bot.send_message(tg_id, caption, reply_markup=keyboard)
return True
except TelegramForbiddenError:
logger.error(f"Пользователь {tg_id} заблокировал бота")
return False
except Exception as e:
logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}")
return False
@@ -1,13 +1,15 @@
import asyncio
from datetime import datetime, timedelta
import asyncpg
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,
@@ -16,6 +18,7 @@ from database import (
from handlers.keys.key_utils import get_user_traffic
from logger import logger
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
@@ -167,7 +170,7 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti
continue
total_traffic = sum(
value if isinstance(value, (int, float)) else 0 for value in traffic_data.get("traffic", {}).values()
value if isinstance(value, int | float) else 0 for value in traffic_data.get("traffic", {}).values()
)
logger.info(f"Ключ для {email}: общий трафик: {total_traffic} ГБ")
+1 -1
View File
@@ -1,7 +1,6 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CRYPTO_BOT_ENABLE,
DONATIONS_ENABLE,
@@ -13,6 +12,7 @@ from config import (
from .utils import edit_or_send_message
router = Router()
+12 -11
View File
@@ -1,7 +1,10 @@
import html
import os
from typing import Any
import asyncpg
from aiogram import F, Router
from aiogram.enums import ParseMode
from aiogram.fsm.context import FSMContext
@@ -14,7 +17,6 @@ from aiogram.types import (
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
DATABASE_URL,
INLINE_MODE,
@@ -25,6 +27,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,
@@ -40,10 +43,10 @@ from handlers.buttons.profile import (
from handlers.texts import get_referral_link, invite_message_send, profile_message_send
from keyboards.admin.panel_kb import AdminPanelCallback
from logger import logger
import html
from .utils import edit_or_send_message
router = Router()
@@ -56,7 +59,7 @@ async def process_callback_view_profile(
):
if isinstance(callback_query_or_message, CallbackQuery):
chat_id = callback_query_or_message.message.chat.id
username = html.escape(callback_query_or_message.from_user.full_name)
username = html.escape(callback_query_or_message.from_user.full_name)
target_message = callback_query_or_message.message
else:
chat_id = callback_query_or_message.chat.id
@@ -176,14 +179,12 @@ async def view_tariffs_handler(callback_query: CallbackQuery):
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
image_path = os.path.join("img", "tariffs.jpg")
tariffs_message = "<b>🚀 Доступные тарифы VPN:</b>\n\n" + "\n".join(
[
f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: "
f"{RENEWAL_PLANS[months]['price']} "
f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей"
for months in sorted(RENEWAL_PLANS.keys(), key=int)
]
)
tariffs_message = "<b>🚀 Доступные тарифы VPN:</b>\n\n" + "\n".join([
f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: "
f"{RENEWAL_PLANS[months]['price']} "
f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей"
for months in sorted(RENEWAL_PLANS.keys(), key=int)
])
await edit_or_send_message(
target_message=callback_query.message,
+13 -5
View File
@@ -1,4 +1,5 @@
import os
from typing import Any
from aiogram import F, Router
@@ -10,8 +11,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 +20,8 @@ from config import (
DONATIONS_ENABLE,
SUPPORT_CHAT_URL,
)
from bot import bot
from database import (
add_connection,
add_referral,
@@ -37,6 +38,7 @@ from logger import logger
from .utils import edit_or_send_message
router = Router()
@@ -78,7 +80,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
)
return
else:
logger.info(f"Пользователь {message.chat.id} подписан на канал (статус: {member.status}). Продолжаем работу.")
logger.info(
f"Пользователь {message.chat.id} подписан на канал (статус: {member.status}). Продолжаем работу."
)
except Exception as e:
logger.error(f"Ошибка проверки подписки пользователя {message.chat.id}: {e}")
await state.update_data(start_text=text_to_process)
@@ -259,8 +263,12 @@ async def process_start_logic(
logger.info(f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}")
await message.answer(f"Вы стали рефералом пользователя с ID {referrer_tg_id}")
try:
await bot.send_message(referrer_tg_id, f"🎉 Ваш реферал {message.chat.id} успешно зарегистрировался!")
logger.info(f"Уведомление отправлено пользователю {referrer_tg_id} о новом реферале {message.chat.id}")
await bot.send_message(
referrer_tg_id, f"🎉 Ваш реферал {message.chat.id} успешно зарегистрировался!"
)
logger.info(
f"Уведомление отправлено пользователю {referrer_tg_id} о новом реферале {message.chat.id}"
)
except Exception as e:
logger.error(f"Не удалось отправить уведомление пригласившему ({referrer_tg_id}): {e}")
+4 -3
View File
@@ -7,10 +7,11 @@ import string
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
@@ -65,7 +66,7 @@ async def get_least_loaded_cluster() -> str:
"""
servers = await get_servers()
cluster_loads: dict[str, int] = {cluster_id: 0 for cluster_id in servers.keys()}
cluster_loads: dict[str, int] = dict.fromkeys(servers.keys(), 0)
async with asyncpg.create_pool(DATABASE_URL) as pool:
async with pool.acquire() as conn:
@@ -199,7 +200,7 @@ async def edit_or_send_message(
disable_web_page_preview=disable_web_page_preview,
)
return
except Exception as e:
except Exception:
await target_message.answer(
text=text,
reply_markup=reply_markup,
+1 -1
View File
@@ -7,7 +7,7 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"):
action: str
page: int
def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None:
if "page" not in kwargs or kwargs["page"] is None:
kwargs["page"] = 1
super().__init__(*args, **kwargs)
+1 -1
View File
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RENEWAL_PRICES
from keyboards.admin.panel_kb import build_admin_back_btn
+2
View File
@@ -1,10 +1,12 @@
import logging
import os
import sys
from datetime import timedelta
from loguru import logger
log_folder = "logs"
if not os.path.exists(log_folder):
+1 -2
View File
@@ -3,7 +3,6 @@ from typing import Any, Dict, Set, cast
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from config import ADMIN_ID
@@ -15,7 +14,7 @@ class AdminMiddleware(BaseMiddleware):
"""
# Кэш 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,
+1 -1
View File
@@ -2,9 +2,9 @@ from collections.abc import Awaitable, Callable
from typing import Any, Optional
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import DATABASE_URL
+9 -7
View File
@@ -1,9 +1,11 @@
from typing import Any, Awaitable, Callable, Dict
from collections.abc import Awaitable, Callable
from typing import Any, Dict
from aiogram import BaseMiddleware, Bot
from aiogram.types import TelegramObject, CallbackQuery
from aiogram.types import CallbackQuery, TelegramObject
from cachetools import TTLCache
THROTTLE_TIME = 1.0
@@ -13,10 +15,10 @@ class ThrottlingMiddleware(BaseMiddleware):
self.throttle_notice_cache = TTLCache(maxsize=10_000, ttl=3.0)
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
bot: Bot | None = data.get("bot", None)
user_id = event.from_user.id if event.from_user else None
@@ -27,7 +29,7 @@ class ThrottlingMiddleware(BaseMiddleware):
await bot.answer_callback_query(
callback_query_id=event.id,
text="Слишком много запросов! Пожалуйста, подождите...",
show_alert=False
show_alert=False,
)
return None
else:
+30 -1
View File
@@ -44,13 +44,16 @@ dependencies = [
[tool.ruff]
line-length = 120
target-version = "py312"
fix = true
unsafe-fixes = true
preview = true
[tool.uv]
package = false
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "ANN", "ASYNC", "S", "BLE", "FBT", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE"]
ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001','W291','ANN401','DTZ003','DTZ005','F401','FBT002','FBT001','FBT003','A005','E501','UP017','DTZ004','W293','ANN202','DTZ007']
ignore = ["ANN101", "ANN102", "S101", "ANN201", "ANN001", "BLE001", "W291", "ANN401", "DTZ003", "DTZ005", "F401", "FBT002", "FBT001", "FBT003", "A005", "E501", "UP017", "DTZ004", "W293", "ANN202", "DTZ007"]
exclude = [
".git",
"venv",
@@ -61,6 +64,32 @@ exclude = [
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = true
docstring-code-line-length = 88
[tool.ruff.lint.isort]
case-sensitive = true
combine-as-imports = true
force-wrap-aliases = true
known-first-party = ["Solo_bot"]
lines-after-imports = 2
lines-between-types = 1
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"
multiline-quotes = "double"
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.pycodestyle]
max-doc-length = 120
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.darker]
src = ["."]
+4 -2
View File
@@ -1,17 +1,19 @@
import asyncio
import re
from datetime import datetime, timedelta
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import ADMIN_ID, PING_TIME
from ping3 import ping
from bot import bot
from config import ADMIN_ID, PING_TIME
from database import get_servers
from keyboards.admin.servers_kb import AdminServerEditorCallback
from logger import logger
last_ping_times = {}
last_notification_times = {}
PING_SEMAPHORE = asyncio.Semaphore(3)
@@ -32,7 +34,7 @@ async def ping_server(server_ip: str) -> bool:
async def check_tcp_connection(host: str, port: int) -> bool:
"""Проверяет доступность сервера через TCP (порт 443)."""
try:
reader, writer = await asyncio.open_connection(host, port)
_reader, writer = await asyncio.open_connection(host, port)
writer.close()
await writer.wait_closed()
return True