diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit
new file mode 100644
index 00000000..93f252c1
--- /dev/null
+++ b/.github/hooks/pre-commit
@@ -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
\ No newline at end of file
diff --git a/backup.py b/backup.py
index 0a37fa09..9ff6850f 100644
--- a/backup.py
+++ b/backup.py
@@ -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
diff --git a/bot.py b/bot.py
index ce907401..912d1721 100644
--- a/bot.py
+++ b/bot.py
@@ -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)
diff --git a/client.py b/client.py
index d48654f7..6cd29eec 100644
--- a/client.py
+++ b/client.py
@@ -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}")
diff --git a/database.py b/database.py
index 29009431..1ac0c694 100644
--- a/database.py
+++ b/database.py
@@ -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
diff --git a/filters/admin.py b/filters/admin.py
index b6f6e0a2..d04fa5a3 100644
--- a/filters/admin.py
+++ b/filters/admin.py
@@ -1,6 +1,5 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
-
from config import ADMIN_ID
diff --git a/handlers/__init__.py b/handlers/__init__.py
index c2cd5125..bceb1c61 100644
--- a/handlers/__init__.py
+++ b/handlers/__init__.py
@@ -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(
diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py
index cb91e6f3..50669db1 100644
--- a/handlers/admin/__init__.py
+++ b/handlers/admin/__init__.py
@@ -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(
diff --git a/handlers/admin/admin_backups.py b/handlers/admin/admin_backups.py
index e838b891..69ad7cc9 100644
--- a/handlers/admin/admin_backups.py
+++ b/handlers/admin/admin_backups.py
@@ -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()
diff --git a/handlers/admin/admin_bans.py b/handlers/admin/admin_bans.py
index 833f3084..44379b5a 100644
--- a/handlers/admin/admin_bans.py
+++ b/handlers/admin/admin_bans.py
@@ -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()
diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py
index a2ba1ca1..fc402ea9 100644
--- a/handlers/admin/admin_coupons.py
+++ b/handlers/admin/admin_coupons.py
@@ -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()
diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py
index 37869aad..d940c2c7 100644
--- a/handlers/admin/admin_panel.py
+++ b/handlers/admin/admin_panel.py
@@ -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()
diff --git a/handlers/admin/admin_restart.py b/handlers/admin/admin_restart.py
index d2501758..9acc1195 100644
--- a/handlers/admin/admin_restart.py
+++ b/handlers/admin/admin_restart.py
@@ -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()
diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py
index b9bcde6e..e85338aa 100644
--- a/handlers/admin/admin_sender.py
+++ b/handlers/admin/admin_sender.py
@@ -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()
diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py
index b44edbc3..b1c6a67e 100644
--- a/handlers/admin/admin_servers.py
+++ b/handlers/admin/admin_servers.py
@@ -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()
diff --git a/handlers/admin/admin_stats.py b/handlers/admin/admin_stats.py
index 187f3106..042036c7 100644
--- a/handlers/admin/admin_stats.py
+++ b/handlers/admin/admin_stats.py
@@ -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()
diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py
index 222c5f9c..de43a8e4 100644
--- a/handlers/admin/admin_users.py
+++ b/handlers/admin/admin_users.py
@@ -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()
diff --git a/handlers/captcha.py b/handlers/captcha.py
index ad399510..f6408f88 100644
--- a/handlers/captcha.py
+++ b/handlers/captcha.py
@@ -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()
diff --git a/handlers/donate.py b/handlers/donate.py
index c1675149..49fe0f3d 100644
--- a/handlers/donate.py
+++ b/handlers/donate.py
@@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
-
from config import RUB_TO_XTR
+
from logger import logger
from .utils import edit_or_send_message
diff --git a/handlers/instructions/__init__.py b/handlers/instructions/__init__.py
index 06350a7c..3756810c 100644
--- a/handlers/instructions/__init__.py
+++ b/handlers/instructions/__init__.py
@@ -4,6 +4,7 @@ from aiogram import Router
from .instructions import router as instructions_router
+
router = Router(name="instructions_main_router")
router.include_routers(
diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py
index a4cf5f89..56343f64 100644
--- a/handlers/instructions/instructions.py
+++ b/handlers/instructions/instructions.py
@@ -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"]
diff --git a/handlers/keys/__init__.py b/handlers/keys/__init__.py
index 064024e6..6a5ecfac 100644
--- a/handlers/keys/__init__.py
+++ b/handlers/keys/__init__.py
@@ -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(
diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py
index a63d17df..ed8344e7 100644
--- a/handlers/keys/key_management.py
+++ b/handlers/keys/key_management.py
@@ -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)
diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py
index ce38ab1f..5b8856f4 100644
--- a/handlers/keys/key_utils.py
+++ b/handlers/keys/key_utils.py
@@ -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
diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py
index 97a3ba2e..f5cb6148 100644
--- a/handlers/keys/keys.py
+++ b/handlers/keys/keys.py
@@ -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))
diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py
index 10f9a246..d789359a 100644
--- a/handlers/keys/subscriptions.py
+++ b/handlers/keys/subscriptions.py
@@ -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}"
diff --git a/handlers/notifications/__init__.py b/handlers/notifications/__init__.py
index ee8f833d..6b90acab 100644
--- a/handlers/notifications/__init__.py
+++ b/handlers/notifications/__init__.py
@@ -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)
diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py
index d9cf2152..dc6dadc2 100644
--- a/handlers/notifications/general_notifications.py
+++ b/handlers/notifications/general_notifications.py
@@ -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")
diff --git a/handlers/notifications/notify_utils.py b/handlers/notifications/notify_utils.py
index 9e248179..5dbb3eb0 100644
--- a/handlers/notifications/notify_utils.py
+++ b/handlers/notifications/notify_utils.py
@@ -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
diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py
index 76089778..c4a57d7f 100644
--- a/handlers/notifications/special_notifications.py
+++ b/handlers/notifications/special_notifications.py
@@ -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} ГБ")
diff --git a/handlers/pay.py b/handlers/pay.py
index 3f7604cd..cc041a3f 100644
--- a/handlers/pay.py
+++ b/handlers/pay.py
@@ -1,7 +1,6 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-
from config import (
CRYPTO_BOT_ENABLE,
DONATIONS_ENABLE,
@@ -13,6 +12,7 @@ from config import (
from .utils import edit_or_send_message
+
router = Router()
diff --git a/handlers/profile.py b/handlers/profile.py
index b8b703f6..b592c192 100644
--- a/handlers/profile.py
+++ b/handlers/profile.py
@@ -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 = "🚀 Доступные тарифы VPN:\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 = "🚀 Доступные тарифы VPN:\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,
diff --git a/handlers/start.py b/handlers/start.py
index d2e06271..e76c7514 100644
--- a/handlers/start.py
+++ b/handlers/start.py
@@ -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}")
diff --git a/handlers/utils.py b/handlers/utils.py
index b32798f5..913bb4c4 100644
--- a/handlers/utils.py
+++ b/handlers/utils.py
@@ -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,
diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py
index 0056c3c4..e3408fd8 100644
--- a/keyboards/admin/panel_kb.py
+++ b/keyboards/admin/panel_kb.py
@@ -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)
diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py
index be179bc1..52931cf9 100644
--- a/keyboards/admin/users_kb.py
+++ b/keyboards/admin/users_kb.py
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
-
from config import RENEWAL_PRICES
+
from keyboards.admin.panel_kb import build_admin_back_btn
diff --git a/logger.py b/logger.py
index 7130c5e9..91c16f53 100644
--- a/logger.py
+++ b/logger.py
@@ -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):
diff --git a/middlewares/admin.py b/middlewares/admin.py
index 9cea568a..f9182f37 100644
--- a/middlewares/admin.py
+++ b/middlewares/admin.py
@@ -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,
diff --git a/middlewares/session.py b/middlewares/session.py
index 0076d6ed..839f60ad 100644
--- a/middlewares/session.py
+++ b/middlewares/session.py
@@ -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
diff --git a/middlewares/throttling.py b/middlewares/throttling.py
index 52beb7fd..ab2262ed 100644
--- a/middlewares/throttling.py
+++ b/middlewares/throttling.py
@@ -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:
diff --git a/pyproject.toml b/pyproject.toml
index 782b2de5..6fd09c92 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 = ["."]
diff --git a/servers.py b/servers.py
index 25fa3ceb..c870de47 100644
--- a/servers.py
+++ b/servers.py
@@ -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