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