From a018a42f2c1e57ff1fbee402c665b8822aa11fa3 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Fri, 10 Jan 2025 14:51:13 +0300 Subject: [PATCH] Add antiflood and lint --- backup.py | 2 +- bot.py | 7 +- client.py | 7 +- database.py | 45 +++++----- filters/admin.py | 2 - handlers/admin/admin_panel.py | 116 ++++++++++++++++---------- handlers/admin/admin_servers.py | 9 +- handlers/admin/admin_user_editor.py | 98 ++++++++++++++++------ handlers/donate.py | 2 +- handlers/instructions/instructions.py | 36 ++++---- handlers/keys/key_management.py | 84 ++++++++++++++----- handlers/keys/key_utils.py | 4 +- handlers/keys/keys.py | 32 +++---- handlers/keys/subscriptions.py | 17 ++-- handlers/keys/trial_key.py | 12 ++- handlers/notifications.py | 65 ++++++++++----- handlers/pay.py | 11 ++- handlers/payments/__init__.py | 1 - handlers/payments/robokassa_pay.py | 4 +- handlers/profile.py | 29 ++++--- handlers/start.py | 99 +++++++++++++++------- handlers/utils.py | 8 +- middlewares/admin.py | 1 - middlewares/database.py | 1 - middlewares/throttling.py | 42 ++++++++++ servers.py | 7 +- 26 files changed, 491 insertions(+), 250 deletions(-) create mode 100644 middlewares/throttling.py diff --git a/backup.py b/backup.py index c6811fa7..329d7a22 100644 --- a/backup.py +++ b/backup.py @@ -3,8 +3,8 @@ import subprocess from datetime import datetime from aiogram.types import BufferedInputFile - from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER + from logger import logger diff --git a/bot.py b/bot.py index 16ef87d6..9c98ab3d 100644 --- a/bot.py +++ b/bot.py @@ -5,13 +5,14 @@ from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import ErrorEvent - from config import API_TOKEN + from logger import logger from middlewares.admin import AdminMiddleware from middlewares.database import DatabaseMiddleware from middlewares.delete import DeleteMessageMiddleware from middlewares.logging import LoggingMiddleware +from middlewares.throttling import ThrottlingMiddleware from middlewares.user import UserMiddleware bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) @@ -29,6 +30,10 @@ dp.callback_query.middleware(UserMiddleware()) dp.message.middleware(DatabaseMiddleware()) dp.callback_query.middleware(DatabaseMiddleware()) +# Add throttling middleware +dp.message.middleware(ThrottlingMiddleware(limit=1)) # 1 message per second +dp.callback_query.middleware(ThrottlingMiddleware(limit=1)) + dp.message.outer_middleware(DeleteMessageMiddleware()) dp.callback_query.outer_middleware(DeleteMessageMiddleware()) diff --git a/client.py b/client.py index 83b80f93..3b9336d0 100644 --- a/client.py +++ b/client.py @@ -1,6 +1,6 @@ import py3xui - from config import LIMIT_IP + from logger import logger @@ -44,14 +44,15 @@ async def add_client( error_message = str(e) if "Duplicate email" in error_message: - logger.warning(f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}") + logger.warning( + f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}" + ) return {"status": "duplicate", "email": email} logger.error(f"Ошибка при добавлении клиента {email}: {error_message}") return {"status": "failed", "error": error_message} - async def extend_client_key( xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int ): diff --git a/database.py b/database.py index 66e49453..9deff30d 100644 --- a/database.py +++ b/database.py @@ -3,8 +3,8 @@ from datetime import datetime from typing import Any import asyncpg - from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES + from logger import logger @@ -17,36 +17,34 @@ async def save_temporary_data(session, tg_id: int, state: str, data: dict): ON CONFLICT (tg_id) DO UPDATE SET state = $2, data = $3, updated_at = $4 """, - tg_id, state, json.dumps(data), datetime.utcnow() + tg_id, + state, + json.dumps(data), + datetime.utcnow(), ) + async def get_temporary_data(session, tg_id: int) -> dict | None: """Извлекает временные данные пользователя.""" result = await session.fetchrow( - "SELECT state, data FROM temporary_data WHERE tg_id = $1", - tg_id + "SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id ) if result: - return { - "state": result["state"], - "data": json.loads(result["data"]) - } + return {"state": result["state"], "data": json.loads(result["data"])} return None + async def clear_temporary_data(session, tg_id: int): - await session.execute( - "DELETE FROM temporary_data WHERE tg_id = $1", - tg_id - ) + await session.execute("DELETE FROM temporary_data WHERE tg_id = $1", tg_id) + async def add_blocked_user(tg_id: int, conn: asyncpg.Connection): await conn.execute( "INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", - tg_id + tg_id, ) - async def init_db(file_path: str = "assets/schema.sql"): with open(file_path) as file: sql_content = file.read() @@ -610,7 +608,9 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): try: if referred_tg_id == referrer_tg_id: - logger.warning(f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку.") + logger.warning( + f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку." + ) return await session.execute( @@ -1244,11 +1244,11 @@ async def get_servers_from_db(): async def delete_user_data(session: Any, tg_id: int): try: - await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id) - except Exception as e: - logger.warning( - f"У Вас версия без подарков для {tg_id}: {e}" + await session.execute( + "DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id ) + except Exception as e: + logger.warning(f"У Вас версия без подарков для {tg_id}: {e}") await session.execute("DELETE FROM payments WHERE tg_id = $1", tg_id) await session.execute("DELETE FROM users WHERE tg_id = $1", tg_id) await session.execute("DELETE FROM connections WHERE tg_id = $1", tg_id) @@ -1257,7 +1257,12 @@ async def delete_user_data(session: Any, tg_id: int): async def store_gift_link( - gift_id: str, sender_tg_id: int, selected_months: int, expiry_time: datetime, gift_link: str, session: Any = None + gift_id: str, + sender_tg_id: int, + selected_months: int, + expiry_time: datetime, + gift_link: str, + session: Any = None, ): """ Добавляет информацию о подарке в базу данных. diff --git a/filters/admin.py b/filters/admin.py index 81f80564..a5d7c387 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -1,7 +1,5 @@ - from aiogram.filters import BaseFilter from aiogram.types import Message - from config import ADMIN_ID diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 98734ca1..699bef1b 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -10,10 +10,10 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder +from config import DATABASE_URL from backup import backup_database from bot import bot -from config import DATABASE_URL from database import delete_user_data from filters.admin import IsAdminFilter from logger import logger @@ -37,7 +37,7 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC async def handle_admin_message(message: types.Message, state: FSMContext): await state.clear() - BOT_VERSION = "4.0.0-preAlpha(9)" + BOT_VERSION = "4.0.0-preAlpha(9)" builder = InlineKeyboardBuilder() builder.row( @@ -64,17 +64,12 @@ async def handle_admin_message(message: types.Message, state: FSMContext): InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to") ) builder.row( - InlineKeyboardButton( - text="🤖 Управление Ботом", callback_data="bot_management" - ) - ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + InlineKeyboardButton(text="🤖 Управление Ботом", callback_data="bot_management") ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer( f"🤖 Панель администратора\n\nВерсия бота: {BOT_VERSION}", - reply_markup=builder.as_markup(), - parse_mode="HTML" + reply_markup=builder.as_markup() ) @@ -87,19 +82,14 @@ async def handle_bot_management(callback_query: types.CallbackQuery): builder.row( InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot") ) - builder.row( - InlineKeyboardButton(text="🚫 Баны", callback_data="ban_user") - ) - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="admin") - ) + builder.row(InlineKeyboardButton(text="🚫 Баны", callback_data="ban_user")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin")) await callback_query.message.answer( "🤖 Управление ботом", reply_markup=builder.as_markup(), ) - @router.callback_query(F.data == "user_stats", IsAdminFilter()) async def user_stats_menu(callback_query: CallbackQuery, session: Any): try: @@ -209,7 +199,9 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): ) return - csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" + csv_data = ( + "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" + ) for user in users: csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n" @@ -285,16 +277,31 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): @router.callback_query(F.data == "send_to", IsAdminFilter()) async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all")) - builder.row(InlineKeyboardButton(text="📢 Отправить с подпиской", callback_data="send_to_subscribed")) - builder.row(InlineKeyboardButton(text="📢 Отправить без подписки", callback_data="send_to_unsubscribed")) - builder.row(InlineKeyboardButton(text="📢 Рассылка по кластеру", callback_data="send_to_cluster")) + builder.row( + InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all") + ) + builder.row( + InlineKeyboardButton( + text="📢 Отправить с подпиской", callback_data="send_to_subscribed" + ) + ) + builder.row( + InlineKeyboardButton( + text="📢 Отправить без подписки", callback_data="send_to_unsubscribed" + ) + ) + builder.row( + InlineKeyboardButton( + text="📢 Рассылка по кластеру", callback_data="send_to_cluster" + ) + ) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) await callback_query.message.answer( "✍️ Выберите группу пользователей и введите текст сообщения для рассылки:", reply_markup=builder.as_markup(), ) + @router.callback_query(F.data == "send_to_all", IsAdminFilter()) async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): await state.update_data(send_to="all") @@ -303,6 +310,7 @@ async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): ) await state.set_state(UserEditorState.waiting_for_message) + @router.callback_query(F.data == "send_to_subscribed", IsAdminFilter()) async def handle_send_to_subscribed(callback_query: CallbackQuery, state: FSMContext): await state.update_data(send_to="subscribed") @@ -311,6 +319,7 @@ async def handle_send_to_subscribed(callback_query: CallbackQuery, state: FSMCon ) await state.set_state(UserEditorState.waiting_for_message) + @router.callback_query(F.data == "send_to_unsubscribed", IsAdminFilter()) async def handle_send_to_unsubscribed(callback_query: CallbackQuery, state: FSMContext): await state.update_data(send_to="unsubscribed") @@ -319,8 +328,11 @@ async def handle_send_to_unsubscribed(callback_query: CallbackQuery, state: FSMC ) await state.set_state(UserEditorState.waiting_for_message) + @router.callback_query(F.data == "send_to_cluster", IsAdminFilter()) -async def handle_send_to_cluster(callback_query: CallbackQuery, state: FSMContext, session: Any): +async def handle_send_to_cluster( + callback_query: CallbackQuery, state: FSMContext, session: Any +): clusters = await session.fetch("SELECT DISTINCT cluster_name FROM servers") builder = InlineKeyboardBuilder() @@ -328,7 +340,7 @@ async def handle_send_to_cluster(callback_query: CallbackQuery, state: FSMContex builder.row( InlineKeyboardButton( text=f"🌐 {cluster['cluster_name']}", - callback_data=f"send_cluster|{cluster['cluster_name']}" + callback_data=f"send_cluster|{cluster['cluster_name']}", ) ) @@ -357,34 +369,43 @@ async def process_message_to_all( try: state_data = await state.get_data() - send_to = state_data.get('send_to', 'all') + send_to = state_data.get("send_to", "all") - if send_to == 'all': + if send_to == "all": tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections") - elif send_to == 'subscribed': - tg_ids = await session.fetch(""" + elif send_to == "subscribed": + tg_ids = await session.fetch( + """ SELECT DISTINCT c.tg_id FROM connections c JOIN keys k ON c.tg_id = k.tg_id WHERE k.expiry_time > $1 - """, int(datetime.utcnow().timestamp() * 1000)) - elif send_to == 'unsubscribed': - tg_ids = await session.fetch(""" + """, + int(datetime.utcnow().timestamp() * 1000), + ) + elif send_to == "unsubscribed": + tg_ids = await session.fetch( + """ SELECT c.tg_id FROM connections c LEFT JOIN keys k ON c.tg_id = k.tg_id GROUP BY c.tg_id HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1 - """, int(datetime.utcnow().timestamp() * 1000)) - elif send_to == 'cluster': - cluster_name = state_data.get('cluster_name') - tg_ids = await session.fetch(""" + """, + int(datetime.utcnow().timestamp() * 1000), + ) + elif send_to == "cluster": + cluster_name = state_data.get("cluster_name") + tg_ids = await session.fetch( + """ SELECT DISTINCT c.tg_id FROM connections c JOIN keys k ON c.tg_id = k.tg_id JOIN servers s ON k.server_id = s.cluster_name WHERE s.cluster_name = $1 - """, cluster_name) + """, + cluster_name, + ) total_users = len(tg_ids) success_count = 0 @@ -503,11 +524,11 @@ async def handle_ban_user(callback_query: types.CallbackQuery): InlineKeyboardButton(text="📄 Выгрузить в CSV", callback_data="export_to_csv") ) builder.row( - InlineKeyboardButton(text="🗑️ Удалить из БД", callback_data="delete_banned_users") - ) - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") + InlineKeyboardButton( + text="🗑️ Удалить из БД", callback_data="delete_banned_users" + ) ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")) await callback_query.message.answer( "🚫 Заблокировавшие бота\n\n" "Здесь можно просматривать и удалять пользователей, которые забанили вашего бота!", @@ -523,6 +544,7 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery): import csv import io + csv_output = io.StringIO() writer = csv.writer(csv_output) writer.writerow(["tg_id", "blocked_at"]) @@ -532,8 +554,7 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery): csv_output.seek(0) document = BufferedInputFile( - file=csv_output.getvalue().encode("utf-8"), - filename="banned_users.csv" + file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv" ) builder = InlineKeyboardBuilder() @@ -564,16 +585,20 @@ async def delete_banned_users(callback_query: types.CallbackQuery): conn = await asyncpg.connect(DATABASE_URL) try: blocked_users = await conn.fetch("SELECT tg_id FROM blocked_users") - blocked_ids = [record['tg_id'] for record in blocked_users] + blocked_ids = [record["tg_id"] for record in blocked_users] if not blocked_ids: - await callback_query.message.answer("📂 Нет заблокировавших пользователей для удаления.") + await callback_query.message.answer( + "📂 Нет заблокировавших пользователей для удаления." + ) return for tg_id in blocked_ids: await delete_user_data(conn, tg_id) - await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids) + await conn.execute( + "DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids + ) builder = InlineKeyboardBuilder() builder.row( @@ -594,4 +619,3 @@ async def delete_banned_users(callback_query: types.CallbackQuery): ) finally: await conn.close() - diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 2369095d..a2ee4ed4 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -6,10 +6,10 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder +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, get_servers_from_db from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster @@ -369,7 +369,11 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery): await callback_query.message.answer( f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", reply_markup=InlineKeyboardBuilder() - .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) + .row( + InlineKeyboardButton( + text="🔙 Назад", callback_data="servers_editor" + ) + ) .as_markup(), ) return @@ -408,7 +412,6 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery): await conn.close() - @router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter()) async def handle_check_server_availability(callback_query: types.CallbackQuery): cluster_name = callback_query.data.split("|")[1] diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index abad6a08..da18697e 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -7,9 +7,15 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import TOTAL_GB -from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry + +from database import ( + delete_user_data, + get_client_id_by_email, + get_servers_from_db, + restore_trial, + update_key_expiry, +) from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( delete_key_from_cluster, @@ -56,7 +62,7 @@ async def prompt_username(callback_query: CallbackQuery, state: FSMContext): async def handle_username_input( message: types.Message, state: FSMContext, session: Any ): - username = message.text.strip().lstrip('@').replace('https://t.me/', '') + username = message.text.strip().lstrip("@").replace("https://t.me/", "") user_record = await session.fetchrow( "SELECT tg_id FROM users WHERE username = $1", username ) @@ -113,12 +119,19 @@ async def handle_username_input( callback_data=f"restore_trial_{tg_id}", ) ) - builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) builder.row( InlineKeyboardButton( - text="✉️ Отправить сообщение", - callback_data=f"send_message_{tg_id}" + text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" + ) + ) + builder.row( + InlineKeyboardButton( + text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" + ) + ) + builder.row( + InlineKeyboardButton( + text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" ) ) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) @@ -201,8 +214,16 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: callback_data=f"change_balance_{tg_id}", ) ) - builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) + builder.row( + InlineKeyboardButton( + text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" + ) + ) + builder.row( + InlineKeyboardButton( + text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" + ) + ) builder.row( InlineKeyboardButton( text="🔄 Восстановить пробник", @@ -211,8 +232,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: ) builder.row( InlineKeyboardButton( - text="✉️ Отправить сообщение", - callback_data=f"send_message_{tg_id}" + text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" ) ) @@ -645,14 +665,29 @@ async def handle_user_info( InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") ) - builder.row(InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}")) - builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) builder.row( InlineKeyboardButton( - text="✉️ Отправить сообщение", - callback_data=f"send_message_{tg_id}" + text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}" + ) + ) + builder.row( + InlineKeyboardButton( + text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}" + ) + ) + builder.row( + InlineKeyboardButton( + text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" + ) + ) + builder.row( + InlineKeyboardButton( + text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" + ) + ) + builder.row( + InlineKeyboardButton( + text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" ) ) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) @@ -671,19 +706,26 @@ async def handle_user_info( @router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter()) -async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def confirm_delete_user( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = int(callback_query.data.split("_")[3]) confirmation_markup = InlineKeyboardMarkup( row_width=2, inline_keyboard=[ - [InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}")], + [ + InlineKeyboardButton( + text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}" + ) + ], [InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")], ], ) await callback_query.message.answer( - f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", reply_markup=confirmation_markup + f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", + reply_markup=confirmation_markup, ) @@ -691,7 +733,9 @@ async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMCon async def delete_user(callback_query: types.CallbackQuery, session: Any): tg_id = int(callback_query.data.split("_")[2]) - key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) + key_records = await session.fetch( + "SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id + ) async def delete_keys_from_servers(): try: @@ -702,7 +746,9 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any): tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) await asyncio.gather(*tasks) except Exception as e: - logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}" + ) await delete_keys_from_servers() @@ -712,9 +758,13 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any): back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard) + await callback_query.message.answer( + f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard + ) except Exception as e: - logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}" + ) await callback_query.message.answer( f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." ) diff --git a/handlers/donate.py b/handlers/donate.py index ad460085..6a37edc8 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 InlineKeyboardButton, LabeledPrice, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import RUB_TO_XTR + from logger import logger diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 296a6006..ec10b117 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -5,9 +5,15 @@ import asyncpg from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL -from handlers.texts import CONNECT_TV_TEXT, INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE, SUBSCRIPTION_DETAILS_TEXT + +from handlers.texts import ( + CONNECT_TV_TEXT, + INSTRUCTION_PC, + INSTRUCTIONS, + KEY_MESSAGE, + SUBSCRIPTION_DETAILS_TEXT, +) from logger import logger router = Router() @@ -15,13 +21,17 @@ router = Router() @router.callback_query(F.data == "instructions") @router.message(F.text == "/instructions") -async def send_instructions(callback_query_or_message: types.CallbackQuery | types.Message): +async def send_instructions( + callback_query_or_message: types.CallbackQuery | types.Message, +): instructions_message = INSTRUCTIONS image_path = os.path.join("img", "instructions.jpg") if not os.path.isfile(image_path): if isinstance(callback_query_or_message, types.CallbackQuery): - await callback_query_or_message.message.answer("Файл изображения не найден.") + await callback_query_or_message.message.answer( + "Файл изображения не найден." + ) else: await callback_query_or_message.answer("Файл изображения не найден.") return @@ -45,7 +55,6 @@ async def send_instructions(callback_query_or_message: types.CallbackQuery | typ ) - @router.callback_query(F.data.startswith("connect_pc|")) async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.message.chat.id @@ -101,15 +110,13 @@ async def process_connect_tv(callback_query: types.CallbackQuery): text="▶ Продолжить", callback_data=f"continue_tv|{key_name}" ) ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( text=CONNECT_TV_TEXT, reply_markup=builder.as_markup(), parse_mode="HTML", - disable_web_page_preview=True + disable_web_page_preview=True, ) @@ -136,8 +143,7 @@ async def process_continue_tv(callback_query: types.CallbackQuery): finally: await conn.close() - - subscription_link = record['key'] + subscription_link = record["key"] message_text = SUBSCRIPTION_DETAILS_TEXT.format(subscription_link=subscription_link) @@ -147,12 +153,8 @@ async def process_continue_tv(callback_query: types.CallbackQuery): text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html" ) ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( - text=message_text, - reply_markup=builder.as_markup(), - parse_mode="HTML" + text=message_text, reply_markup=builder.as_markup(), parse_mode="HTML" ) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 5f17c149..b9b88f10 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -8,8 +8,6 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder - -from bot import bot from config import ( CONNECT_ANDROID, CONNECT_IOS, @@ -21,7 +19,15 @@ from config import ( TRIAL_TIME, USE_NEW_PAYMENT_FLOW, ) -from database import get_balance, get_trial, save_temporary_data, store_key, update_balance + +from bot import bot +from database import ( + get_balance, + get_trial, + save_temporary_data, + store_key, + update_balance, +) from handlers.buttons.add_subscribe import ( DOWNLOAD_ANDROID_BUTTON, DOWNLOAD_IOS_BUTTON, @@ -47,18 +53,25 @@ class Form(StatesGroup): @router.callback_query(F.data == "create_key") -async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any): +async def confirm_create_new_key( + callback_query: CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} confirmed creation of a new key.") - logger.info(f"Balance for user {tg_id} is sufficient. Proceeding with key creation.") + logger.info( + f"Balance for user {tg_id} is sufficient. Proceeding with key creation." + ) await handle_key_creation(tg_id, state, session, callback_query) async def handle_key_creation( - tg_id: int, state: FSMContext, session: Any, message_or_query: Message | CallbackQuery + tg_id: int, + state: FSMContext, + session: Any, + message_or_query: Message | CallbackQuery, ): """Создание ключа с учётом выбора тарифного плана.""" current_time = datetime.utcnow() @@ -68,7 +81,9 @@ async def handle_key_creation( expiry_time = current_time + timedelta(days=TRIAL_TIME) logger.info(f"Assigned 1-day trial to user {tg_id}.") - await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id) + await session.execute( + "UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id + ) await create_key(tg_id, expiry_time, state, session, message_or_query) else: builder = InlineKeyboardBuilder() @@ -85,14 +100,18 @@ async def handle_key_creation( builder.row( InlineKeyboardButton( - text=f"📅 {plan_id} мес. - {price}₽{discount_text}", callback_data=f"select_plan_{plan_id}" + text=f"📅 {plan_id} мес. - {price}₽{discount_text}", + callback_data=f"select_plan_{plan_id}", ) ) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) await message_or_query.message.answer( - "💳 Выберите тарифный план для создания нового ключа:", reply_markup=builder.as_markup() + "💳 Выберите тарифный план для создания нового ключа:", + reply_markup=builder.as_markup(), ) await state.update_data(tg_id=tg_id) await state.set_state(Form.waiting_for_server_selection) @@ -120,7 +139,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any): "plan_price": plan_price, "duration_days": duration_days, "required_amount": max(0, plan_price - balance), - } + }, ) if balance < plan_price: @@ -130,8 +149,12 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any): await process_custom_amount_input(callback_query, session) else: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay")) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay") + ) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) await callback_query.message.answer( f"💳 Недостаточно средств. Для продолжения необходимо пополнить баланс на {required_amount}₽.", @@ -143,6 +166,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any): expiry_time = datetime.utcnow() + timedelta(days=duration_days) await create_key(tg_id, expiry_time, None, session, callback_query) + async def create_key( tg_id: int, expiry_time: datetime, @@ -162,7 +186,9 @@ async def create_key( ) if not existing_key: break - logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.") + logger.warning( + f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one." + ) client_id = str(uuid.uuid4()) email = key_name.lower() @@ -187,13 +213,23 @@ async def create_key( await asyncio.gather(*tasks) logger.info(f"Key created on cluster {least_loaded_cluster} for user {tg_id}.") - await store_key(tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster, session) + await store_key( + tg_id, + client_id, + email, + expiry_timestamp, + public_link, + least_loaded_cluster, + session, + ) except Exception as e: logger.error(f"Error while creating the key for user {tg_id} on cluster: {e}") if isinstance(message_or_query, Message): - await message_or_query.answer("❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова.") + await message_or_query.answer( + "❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова." + ) elif isinstance(message_or_query, CallbackQuery): await message_or_query.message.answer( "❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова." @@ -201,7 +237,7 @@ async def create_key( else: await bot.send_message( chat_id=tg_id, - text="❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова." + text="❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова.", ) return @@ -213,12 +249,14 @@ async def create_key( ) builder.row( InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{public_link}"), - InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}"), + InlineKeyboardButton( + text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}" + ), ) builder.row( InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"), InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"), - ) + ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) remaining_time = expiry_time - datetime.utcnow() @@ -228,9 +266,13 @@ async def create_key( if isinstance(message_or_query, Message): await message_or_query.answer(key_message, reply_markup=builder.as_markup()) elif isinstance(message_or_query, CallbackQuery): - await message_or_query.message.answer(key_message, reply_markup=builder.as_markup()) + await message_or_query.message.answer( + key_message, reply_markup=builder.as_markup() + ) else: - await bot.send_message(chat_id=tg_id, text=key_message, reply_markup=builder.as_markup()) + await bot.send_message( + chat_id=tg_id, text=key_message, reply_markup=builder.as_markup() + ) if state: await state.clear() diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 9236f667..0e8f409f 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,9 +1,9 @@ import asyncio +from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, TOTAL_GB from py3xui import AsyncApi from client import add_client, delete_client, extend_client_key -from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, TOTAL_GB from database import get_servers_from_db from logger import logger @@ -11,7 +11,7 @@ from logger import logger async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_timestamp): """ Создает ключ на всех серверах указанного кластера. - + :param cluster_id: ID кластера. :param tg_id: Telegram ID пользователя. :param client_id: Уникальный идентификатор клиента. diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 92d66f61..4644b0e1 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -7,7 +7,6 @@ from typing import Any from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import ( CONNECT_ANDROID, CONNECT_IOS, @@ -18,6 +17,7 @@ from config import ( RENEWAL_PLANS, TOTAL_GB, ) + from database import ( delete_key, get_balance, @@ -105,14 +105,10 @@ def build_keys_response(records): ) builder.row( - InlineKeyboardButton( - text="➕ Добавить подписку", callback_data="create_key" - ) + InlineKeyboardButton(text="➕ Добавить подписку", callback_data="create_key") ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) inline_keyboard = builder.as_markup() response_message = ( @@ -122,14 +118,18 @@ def build_keys_response(records): return inline_keyboard, response_message -async def send_with_optional_image(send_message, send_photo, image_path, text, keyboard): +async def send_with_optional_image( + send_message, send_photo, image_path, text, keyboard +): """ Отправляет сообщение с изображением, если файл существует. В противном случае отправляет только текст. """ if os.path.isfile(image_path): with open(image_path, "rb") as image_file: await send_photo( - photo=BufferedInputFile(image_file.read(), filename=os.path.basename(image_path)), + photo=BufferedInputFile( + image_file.read(), filename=os.path.basename(image_path) + ), caption=text, reply_markup=keyboard, ) @@ -140,8 +140,6 @@ async def send_with_optional_image(send_message, send_photo, image_path, text, k ) - - @router.callback_query(F.data.startswith("view_key|")) async def process_callback_view_key(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.message.chat.id @@ -166,9 +164,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session time_left = expiry_date - current_date if time_left.total_seconds() <= 0: - days_left_message = ( - "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0" - ) + days_left_message = "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0" else: total_seconds = int(time_left.total_seconds()) days = total_seconds // 86400 @@ -191,7 +187,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session builder.row( InlineKeyboardButton( text="🔄 Обновить подписку", - callback_data=f"update_subscription|{key_name}" + callback_data=f"update_subscription|{key_name}", ) ) @@ -203,9 +199,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session ) builder.row( - InlineKeyboardButton( - text=IMPORT_IOS, url=f"{CONNECT_IOS}{key}" - ), + InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{key}"), InlineKeyboardButton( text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key}" ), @@ -217,7 +211,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session ), InlineKeyboardButton( text=TV_BUTTON, callback_data=f"connect_tv|{key_name}" - ) + ), ) builder.row( diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index f60730dd..38ae40cc 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -4,8 +4,8 @@ from datetime import datetime import aiohttp import asyncpg from aiohttp import web - from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, TRANSITION_DATE_STR + from database import get_servers_from_db from logger import logger @@ -95,7 +95,9 @@ async def handle_old_subscription(request): status=400, ) - logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}") + logger.info( + f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}" + ) created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000) logger.info( @@ -129,9 +131,8 @@ async def handle_old_subscription(request): "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "profile-update-interval": "7", - "profile-title": "base64:" + base64.b64encode( - encoded_project_name.encode("utf-8") - ).decode("utf-8"), + "profile-title": "base64:" + + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), } logger.info(f"Возвращаем объединенные подписки для email: {email}") @@ -141,7 +142,6 @@ async def handle_old_subscription(request): await conn.close() - async def handle_new_subscription(request): email = request.match_info.get("email") tg_id = request.match_info.get("tg_id") @@ -205,9 +205,8 @@ async def handle_new_subscription(request): "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "profile-update-interval": "7", - "profile-title": "base64:" + base64.b64encode( - encoded_project_name.encode("utf-8") - ).decode("utf-8"), + "profile-title": "base64:" + + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), } logger.info(f"Возвращаем объединенные подписки для email: {email}") diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index af5fbb7f..ef9cc566 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -4,10 +4,17 @@ from datetime import datetime, timedelta from typing import Any import pytz +from config import ( + ADMIN_PASSWORD, + ADMIN_USERNAME, + LIMIT_IP, + PUBLIC_LINK, + TOTAL_GB, + TRIAL_TIME, +) from py3xui import AsyncApi from client import add_client -from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME from database import get_servers_from_db, store_key, use_trial from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email, get_least_loaded_cluster @@ -17,8 +24,7 @@ from logger import logger async def create_trial_key(tg_id: int, session: Any): try: trial_status = await session.fetchval( - "SELECT trial FROM connections WHERE tg_id = $1", - tg_id + "SELECT trial FROM connections WHERE tg_id = $1", tg_id ) if trial_status == 1: diff --git a/handlers/notifications.py b/handlers/notifications.py index f361ca0a..355eb199 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -5,8 +5,6 @@ import asyncpg from aiogram import Bot, Router, types from aiogram.exceptions import TelegramForbiddenError from aiogram.utils.keyboard import InlineKeyboardBuilder -from py3xui import AsyncApi - from config import ( ADMIN_PASSWORD, ADMIN_USERNAME, @@ -16,6 +14,8 @@ from config import ( TOTAL_GB, TRIAL_TIME, ) +from py3xui import AsyncApi + from database import ( add_blocked_user, add_notification, @@ -140,7 +140,11 @@ async def notify_10h_keys( for cluster_id in servers: await renew_key_in_cluster( - cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB + cluster_id, + email, + record["client_id"], + new_expiry_time, + TOTAL_GB, ) logger.info( f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}." @@ -169,7 +173,9 @@ async def notify_10h_keys( f"Уведомление об успешном продлении отправлено клиенту {tg_id}." ) except TelegramForbiddenError: - logger.warning(f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users.") + logger.warning( + f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users." + ) await add_blocked_user(tg_id, conn) except Exception as e: logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}") @@ -183,7 +189,9 @@ async def notify_10h_keys( keyboard.button(text="👤 Личный кабинет", callback_data="profile") keyboard.adjust(1) - await bot.send_message(tg_id, message, reply_markup=keyboard.as_markup()) + await bot.send_message( + tg_id, message, reply_markup=keyboard.as_markup() + ) logger.info(f"Уведомление отправлено пользователю {tg_id}.") await conn.execute( @@ -191,9 +199,12 @@ async def notify_10h_keys( record["client_id"], ) logger.info( - f"Обновлено поле notified для клиента {record['client_id']}.") + f"Обновлено поле notified для клиента {record['client_id']}." + ) except TelegramForbiddenError: - logger.warning(f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users.") + logger.warning( + f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users." + ) await add_blocked_user(tg_id, conn) except Exception as e: logger.debug( @@ -260,7 +271,11 @@ async def notify_24h_keys( for cluster_id in servers: await renew_key_in_cluster( - cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB + cluster_id, + email, + record["client_id"], + new_expiry_time, + TOTAL_GB, ) logger.info( f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}." @@ -290,7 +305,9 @@ async def notify_24h_keys( f"Уведомление об успешном продлении отправлено клиенту {tg_id}." ) except TelegramForbiddenError: - logger.warning(f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users.") + logger.warning( + f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users." + ) await add_blocked_user(tg_id, conn) except Exception as e: logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}") @@ -317,11 +334,11 @@ async def notify_24h_keys( ) keyboard = builder.as_markup() await bot.send_message(tg_id, message_24h, reply_markup=keyboard) - logger.info( - f"Уведомление за 24 часа отправлено пользователю {tg_id}." - ) + logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.") except TelegramForbiddenError: - logger.warning(f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users.") + logger.warning( + f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users." + ) await add_blocked_user(tg_id, conn) except Exception as e: logger.error( @@ -332,9 +349,7 @@ async def notify_24h_keys( "UPDATE keys SET notified_24h = TRUE WHERE client_id = $1", record["client_id"], ) - logger.info( - f"Обновлено поле notified_24h для клиента {record['client_id']}." - ) + logger.info(f"Обновлено поле notified_24h для клиента {record['client_id']}.") await asyncio.gather(*(process_record(record) for record in records_24h)) logger.info("Обработка всех уведомлений за 24 часа завершена.") @@ -389,14 +404,20 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): try: await bot.send_message(tg_id, message, reply_markup=keyboard) - logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.") + logger.info( + f"Отправлено уведомление неактивному пользователю {tg_id}." + ) await add_notification(tg_id, "inactive_trial", session=conn) except TelegramForbiddenError: - logger.warning(f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users.") + logger.warning( + f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users." + ) await add_blocked_user(tg_id, conn) except Exception as e: - logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления пользователю {tg_id}: {e}" + ) except Exception as e: logger.error(f"Ошибка при обработке пользователя {tg_id}: {e}") @@ -418,7 +439,9 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: current_time, ) - logger.info(f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает.") + logger.info( + f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает." + ) for record in expiring_keys: try: @@ -427,7 +450,6 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: logger.error(f"Ошибка при обработке подписки {record['client_id']}: {e}") - async def process_key(record, bot, conn): tg_id = record["tg_id"] client_id = record["client_id"] @@ -519,7 +541,6 @@ async def process_key(record, bot, conn): logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}") - async def check_online_users(): servers = await get_servers_from_db() diff --git a/handlers/pay.py b/handlers/pay.py index 24dd97a1..2ebfa491 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, @@ -54,9 +53,15 @@ async def handle_pay(callback_query: CallbackQuery): ) ) - builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon")) + builder.row( + InlineKeyboardButton( + text="🎟️ Активировать купон", callback_data="activate_coupon" + ) + ) if DONATIONS_ENABLE: - builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) + builder.row( + InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") + ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py index e1ccb3fc..ba0f040b 100644 --- a/handlers/payments/__init__.py +++ b/handlers/payments/__init__.py @@ -1,7 +1,6 @@ __all__ = ("router",) from aiogram import Router - from config import ( CRYPTO_BOT_ENABLE, ROBOKASSA_ENABLE, diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 996a8693..2c1de2e5 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -7,8 +7,6 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web -from robokassa import HashAlgorithm, Robokassa - from config import ( ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, @@ -16,6 +14,8 @@ from config import ( ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE, ) +from robokassa import HashAlgorithm, Robokassa + from database import ( add_connection, add_payment, diff --git a/handlers/profile.py b/handlers/profile.py index 991c7909..243f3794 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -5,10 +5,18 @@ from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS + from database import get_balance, get_key_count, get_referral_stats, get_trial -from handlers.buttons.profile import ADD_SUB, GIFTS, INSTRUCTIONS, INVITE, MAIN_MENU, MY_SUBS, PAYMENT +from handlers.buttons.profile import ( + ADD_SUB, + GIFTS, + INSTRUCTIONS, + INVITE, + MAIN_MENU, + MY_SUBS, + PAYMENT, +) from handlers.texts import get_referral_link, invite_message_send, profile_message_send router = Router() @@ -17,7 +25,9 @@ router = Router() @router.callback_query(F.data == "profile") @router.message(F.text == "/profile") async def process_callback_view_profile( - callback_query_or_message: types.Message | types.CallbackQuery, state: FSMContext, admin: bool + callback_query_or_message: types.Message | types.CallbackQuery, + state: FSMContext, + admin: bool, ): if isinstance(callback_query_or_message, types.CallbackQuery): chat_id = callback_query_or_message.message.chat.id @@ -38,7 +48,9 @@ async def process_callback_view_profile( try: trial_status = await get_trial(chat_id, conn) - profile_message = profile_message_send(username, chat_id, int(balance), key_count) + profile_message = profile_message_send( + username, chat_id, int(balance), key_count + ) if key_count == 0: profile_message += "\n
🔧 Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение
" @@ -47,15 +59,10 @@ async def process_callback_view_profile( builder = InlineKeyboardBuilder() - if trial_status == 0 or key_count == 0: - builder.row( - InlineKeyboardButton(text=ADD_SUB, callback_data="create_key") - ) + builder.row(InlineKeyboardButton(text=ADD_SUB, callback_data="create_key")) else: - builder.row( - InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys") - ) + builder.row(InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys")) builder.row( InlineKeyboardButton( diff --git a/handlers/start.py b/handlers/start.py index e0551e87..4a45e0dd 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -11,7 +11,6 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import ( CHANNEL_EXISTS, CHANNEL_URL, @@ -22,7 +21,14 @@ from config import ( DOWNLOAD_IOS, SUPPORT_CHAT_URL, ) -from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial + +from database import ( + add_connection, + add_referral, + check_connection_exists, + get_trial, + use_trial, +) from handlers.buttons.add_subscribe import ( DOWNLOAD_ANDROID_BUTTON, DOWNLOAD_IOS_BUTTON, @@ -58,10 +64,11 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin connection_exists = await check_connection_exists(message.chat.id) logger.info(f"Проверка существования подключения: {connection_exists}") - if not connection_exists: await add_connection(tg_id=message.chat.id, session=session) - logger.info(f"Пользователь {message.chat.id} успешно добавлен в базу данных.") + logger.info( + f"Пользователь {message.chat.id} успешно добавлен в базу данных." + ) if "gift_" in message.text: logger.info(f"Обнаружена ссылка на подарок: {message.text}") @@ -71,28 +78,41 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin recipient_tg_id = message.chat.id gift_info = await session.fetchrow( - "SELECT * FROM gifts WHERE gift_id = $1 AND is_used = FALSE", gift_id + "SELECT * FROM gifts WHERE gift_id = $1 AND is_used = FALSE", + gift_id, ) if gift_info is None: - logger.warning(f"Подарок с ID {gift_id} уже был использован или не существует.") - await message.answer("Этот подарок уже был использован или не существует.") + logger.warning( + f"Подарок с ID {gift_id} уже был использован или не существует." + ) + await message.answer( + "Этот подарок уже был использован или не существует." + ) return await show_start_menu(message, admin, session) - if gift_info['sender_tg_id'] == recipient_tg_id: + if gift_info["sender_tg_id"] == recipient_tg_id: logger.warning( f"Пользователь {recipient_tg_id} попытался активировать подарок, который был отправлен им самим." ) - await message.answer("❌ Вы не можете получить подарок от самого себя.") + await message.answer( + "❌ Вы не можете получить подарок от самого себя." + ) return await show_start_menu(message, admin, session) - selected_months = gift_info['selected_months'] - expiry_time = gift_info['expiry_time'] + selected_months = gift_info["selected_months"] + expiry_time = gift_info["expiry_time"] expiry_time_naive = expiry_time.replace(tzinfo=None) - logger.info(f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}.") + logger.info( + f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}." + ) - await create_key(recipient_tg_id, expiry_time_naive, state, session, message) - logger.info(f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев.") + await create_key( + recipient_tg_id, expiry_time_naive, state, session, message + ) + logger.info( + f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев." + ) await session.execute( "UPDATE gifts SET is_used = TRUE, recipient_tg_id = $1 WHERE gift_id = $2", @@ -103,7 +123,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin await message.answer( f"🎉 Ваш подарок на {selected_months} {'месяц' if selected_months == 1 else 'месяца' if selected_months in [2, 3, 4] else 'месяцев'} активирован!" ) - logger.info(f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}.") + logger.info( + f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}." + ) return elif "referral_" in message.text: @@ -114,16 +136,23 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin logger.info( f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом." ) - await message.answer("❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку.") + await message.answer( + "❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку." + ) return await show_start_menu(message, admin, session) if referrer_tg_id == message.chat.id: - logger.warning(f"Пользователь {message.chat.id} попытался стать рефералом самого себя.") - await message.answer("❌ Вы не можете быть рефералом самого себя.") + logger.warning( + f"Пользователь {message.chat.id} попытался стать рефералом самого себя." + ) + await message.answer( + "❌ Вы не можете быть рефералом самого себя." + ) return await show_start_menu(message, admin, session) existing_referral = await session.fetchrow( - "SELECT * FROM referrals WHERE referred_tg_id = $1", message.chat.id + "SELECT * FROM referrals WHERE referred_tg_id = $1", + message.chat.id, ) if existing_referral: @@ -131,7 +160,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin return await show_start_menu(message, admin, session) await add_referral(message.chat.id, referrer_tg_id, session) - logger.info(f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}") + logger.info( + f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}" + ) return await show_start_menu(message, admin, session) except (ValueError, IndexError) as e: @@ -139,12 +170,16 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin return else: - logger.info(f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка.") + logger.info( + f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка." + ) await show_start_menu(message, admin, session) except (ValueError, IndexError) as e: - logger.error(f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}") + logger.error( + f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}" + ) await message.answer("❌ Произошла ошибка. Пожалуйста, попробуйте снова.") else: await show_start_menu(message, admin, session) @@ -159,7 +194,9 @@ async def show_start_menu(message: Message, admin: bool, session: Any): builder = InlineKeyboardBuilder() if trial_status == 0: - builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")) + builder.row( + InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn") + ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) @@ -169,7 +206,9 @@ async def show_start_menu(message: Message, admin: bool, session: Any): ) if admin: - builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) + builder.row( + InlineKeyboardButton(text="🔧 Администратор", callback_data="admin") + ) builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn")) @@ -223,12 +262,8 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): ), ) builder.row( - InlineKeyboardButton( - text=PC_BUTTON, callback_data=f"connect_pc|{email}" - ), - InlineKeyboardButton( - text=TV_BUTTON, callback_data=f"connect_tv|{email}" - ) + InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"), + InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"), ) builder.row( InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") @@ -244,7 +279,9 @@ async def handle_about_vpn(callback_query: CallbackQuery): builder = InlineKeyboardBuilder() if DONATIONS_ENABLE: - builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) + builder.row( + InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") + ) builder.row( InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL), diff --git a/handlers/utils.py b/handlers/utils.py index e3725575..ea0a9c91 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -4,9 +4,9 @@ import re import aiohttp import asyncpg +from config import DATABASE_URL from bot import bot -from config import DATABASE_URL from database import get_servers_from_db from logger import logger @@ -14,10 +14,12 @@ from logger import logger async def get_usd_rate(): try: async with aiohttp.ClientSession() as session: - async with session.get('https://www.cbr-xml-daily.ru/daily_json.js') as response: + async with session.get( + "https://www.cbr-xml-daily.ru/daily_json.js" + ) as response: if response.status == 200: data = await response.text() - usd = float(json.loads(data)['Valute']['USD']['Value']) + usd = float(json.loads(data)["Valute"]["USD"]["Value"]) else: usd = float(100) # Default value if request fails except Exception as e: diff --git a/middlewares/admin.py b/middlewares/admin.py index bc21afe4..85819d32 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -3,7 +3,6 @@ from typing import Any from aiogram import BaseMiddleware from aiogram.types import TelegramObject - from config import ADMIN_ID diff --git a/middlewares/database.py b/middlewares/database.py index c9bf6906..c625fec3 100644 --- a/middlewares/database.py +++ b/middlewares/database.py @@ -4,7 +4,6 @@ from typing import Any 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 new file mode 100644 index 00000000..be6e64de --- /dev/null +++ b/middlewares/throttling.py @@ -0,0 +1,42 @@ +import asyncio + +from aiogram import Dispatcher, types +from aiogram.dispatcher import DEFAULT_RATE_LIMIT +from aiogram.dispatcher.handler import CancelHandler, current_handler +from aiogram.dispatcher.middlewares import BaseMiddleware +from aiogram.utils.exceptions import Throttled +from aiogram.utils.keyboard import InlineKeyboardBuilder + + +class ThrottlingMiddleware(BaseMiddleware): + def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix="antiflood_"): + self.rate_limit = limit + self.prefix = key_prefix + super(ThrottlingMiddleware, self).__init__() + + async def on_process_message(self, message: types.Message, data: dict): + handler = current_handler.get() + dispatcher = Dispatcher.get_current() + + if handler: + limit = getattr(handler, "throttling_rate_limit", self.rate_limit) + key = getattr(handler, "throttling_key", f"{self.prefix}_{handler.__name__}") + else: + limit = self.rate_limit + key = f"{self.prefix}_message" + + try: + await dispatcher.throttle(key, rate=limit) + except Throttled as t: + await self.message_throttled(message, t) + raise CancelHandler() + + async def message_throttled(self, message: types.Message, throttled: Throttled): + delta = throttled.rate - throttled.delta + + if throttled.exceeded_count <= 2: + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await message.reply("🚫 Слишком много запросов! Пожалуйста, не торопитесь!", reply_markup=builder.as_markup()) + + await asyncio.sleep(delta) diff --git a/servers.py b/servers.py index e5806cba..9b9efe2c 100644 --- a/servers.py +++ b/servers.py @@ -5,10 +5,10 @@ from datetime import datetime, timedelta import asyncpg from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder +from config import ADMIN_ID, DATABASE_URL, PING_TIME from ping3 import ping from bot import bot -from config import ADMIN_ID, DATABASE_URL, PING_TIME from database import get_servers_from_db from logger import logger @@ -104,8 +104,9 @@ async def notify_admin(server_name: str): current_time = datetime.now() last_notification_time = last_notification_times.get(server_name) - if last_notification_time and current_time - last_notification_time < timedelta( - minutes=3 + if ( + last_notification_time + and current_time - last_notification_time < timedelta(minutes=3) ): logger.info( f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут."