From 3fcb6adaad187895198ca4cf53775d8fa2d17f83 Mon Sep 17 00:00:00 2001 From: hteppl Date: Thu, 5 Dec 2024 07:59:49 +0300 Subject: [PATCH 01/28] Create schema.sql database template + optimize init_db --- assets/schema.sql | 87 ++++++++++++++++++++++++++ database.py | 152 +++++++--------------------------------------- 2 files changed, 108 insertions(+), 131 deletions(-) create mode 100644 assets/schema.sql diff --git a/assets/schema.sql b/assets/schema.sql new file mode 100644 index 00000000..4659ed3e --- /dev/null +++ b/assets/schema.sql @@ -0,0 +1,87 @@ +CREATE TABLE IF NOT EXISTS users +( + tg_id BIGINT PRIMARY KEY NOT NULL, + username TEXT, + first_name TEXT, + last_name TEXT, + language_code TEXT, + is_bot BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS connections +( + tg_id BIGINT PRIMARY KEY NOT NULL, + balance REAL NOT NULL DEFAULT 0.0, + trial INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS payments +( + id SERIAL PRIMARY KEY, + tg_id BIGINT NOT NULL, + amount REAL NOT NULL, + payment_system TEXT NOT NULL, + status TEXT DEFAULT 'success', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tg_id) REFERENCES users (tg_id) +); + +CREATE TABLE IF NOT EXISTS keys +( + tg_id BIGINT NOT NULL, + client_id TEXT NOT NULL, + email TEXT NOT NULL, + created_at BIGINT NOT NULL, + expiry_time BIGINT NOT NULL, + key TEXT NOT NULL, + server_id TEXT NOT NULL DEFAULT 'cluster1', + notified BOOLEAN NOT NULL DEFAULT FALSE, + notified_24h BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (tg_id, client_id) +); + +CREATE TABLE IF NOT EXISTS referrals +( + referred_tg_id BIGINT PRIMARY KEY NOT NULL, + referrer_tg_id BIGINT NOT NULL, + reward_issued BOOLEAN DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS coupons +( + id SERIAL PRIMARY KEY, + code TEXT UNIQUE NOT NULL, + amount INTEGER NOT NULL, + usage_limit INTEGER NOT NULL DEFAULT 1, + usage_count INTEGER NOT NULL DEFAULT 0, + is_used BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS coupon_usages +( + coupon_id INTEGER NOT NULL REFERENCES coupons (id) ON DELETE CASCADE, + user_id BIGINT NOT NULL, + used_at TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (coupon_id, user_id) +); + +CREATE TABLE IF NOT EXISTS notifications +( + tg_id BIGINT NOT NULL, + last_notification_time TIMESTAMP NOT NULL DEFAULT NOW(), + notification_type TEXT NOT NULL, + PRIMARY KEY (tg_id, notification_type) +); + +CREATE TABLE IF NOT EXISTS servers +( + id SERIAL PRIMARY KEY, + cluster_name TEXT NOT NULL, + server_name TEXT NOT NULL, + api_url TEXT NOT NULL, + subscription_url TEXT NOT NULL, + inbound_id TEXT NOT NULL, + UNIQUE (cluster_name, server_name) -- Уникальность по названию кластера и сервера +); \ No newline at end of file diff --git a/database.py b/database.py index 0dd381a0..3f9037d0 100644 --- a/database.py +++ b/database.py @@ -2,137 +2,27 @@ from datetime import datetime from typing import Any import asyncpg - from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES + from logger import logger -async def init_db(): +async def init_db(file_path: str = 'assets/schema.sql'): + with open(file_path, 'r') as file: + sql_content = file.read() + + # Split the file content into individual SQL statements and connect to the database + statements = [stmt.strip() for stmt in sql_content.split(';') if stmt.strip()] conn = await asyncpg.connect(DATABASE_URL) - # Таблица для хранения основной информации о пользователях из Telegram - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS users ( - tg_id BIGINT PRIMARY KEY NOT NULL, - username TEXT, - first_name TEXT, - last_name TEXT, - language_code TEXT, - is_bot BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP - ) - """ - ) - # Таблица для хранения информации о пользователях - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS connections ( - tg_id BIGINT PRIMARY KEY NOT NULL, - balance REAL NOT NULL DEFAULT 0.0, - trial INTEGER NOT NULL DEFAULT 0 - ) - """ - ) - - # Таблица для хранения информации о платежах - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS payments ( - id SERIAL PRIMARY KEY, - tg_id BIGINT NOT NULL, - amount REAL NOT NULL, - payment_system TEXT NOT NULL, - status TEXT DEFAULT 'success', - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (tg_id) REFERENCES users(tg_id) - ) - """ - ) - - # Таблица для хранения ключей - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS keys ( - tg_id BIGINT NOT NULL, - client_id TEXT NOT NULL, - email TEXT NOT NULL, - created_at BIGINT NOT NULL, - expiry_time BIGINT NOT NULL, - key TEXT NOT NULL, - server_id TEXT NOT NULL DEFAULT 'cluster1', - notified BOOLEAN NOT NULL DEFAULT FALSE, - notified_24h BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (tg_id, client_id) - ) - """ - ) - - # Таблица для хранения рефералов - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS referrals ( - referred_tg_id BIGINT PRIMARY KEY NOT NULL, - referrer_tg_id BIGINT NOT NULL, - reward_issued BOOLEAN DEFAULT FALSE - ) - """ - ) - - # Таблица для хранения купонов - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS coupons ( - id SERIAL PRIMARY KEY, - code TEXT UNIQUE NOT NULL, - amount INTEGER NOT NULL, - usage_limit INTEGER NOT NULL DEFAULT 1, - usage_count INTEGER NOT NULL DEFAULT 0, - is_used BOOLEAN NOT NULL DEFAULT FALSE - ) - """ - ) - - # Таблица для отслеживания использований купонов пользователями - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS coupon_usages ( - coupon_id INTEGER NOT NULL REFERENCES coupons(id) ON DELETE CASCADE, - user_id BIGINT NOT NULL, - used_at TIMESTAMP NOT NULL DEFAULT NOW(), - PRIMARY KEY (coupon_id, user_id) - ) - """ - ) - - # Таблица для отслеживания отправленных уведомлений - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS notifications ( - tg_id BIGINT NOT NULL, - last_notification_time TIMESTAMP NOT NULL DEFAULT NOW(), - notification_type TEXT NOT NULL, - PRIMARY KEY (tg_id, notification_type) - ) - """ - ) - - await conn.execute( - """ - CREATE TABLE IF NOT EXISTS servers ( - id SERIAL PRIMARY KEY, - cluster_name TEXT NOT NULL, - server_name TEXT NOT NULL, - api_url TEXT NOT NULL, - subscription_url TEXT NOT NULL, - inbound_id TEXT NOT NULL, - UNIQUE(cluster_name, server_name) -- Уникальность по названию кластера и сервера - ) - """ - ) - - await conn.close() + try: + for statement in statements: + await conn.execute(statement) + except Exception as e: + logger.error(f'Error while executing SQL statement: {e}') + finally: + logger.info('Tables created successfully') + await conn.close() async def check_unique_server_name(server_name: str) -> bool: @@ -1026,12 +916,12 @@ async def get_tg_id_by_client_id(client_id: str): async def upsert_user( - tg_id: int, - username: str = None, - first_name: str = None, - last_name: str = None, - language_code: str = None, - is_bot: bool = False, + tg_id: int, + username: str = None, + first_name: str = None, + last_name: str = None, + language_code: str = None, + is_bot: bool = False, ): """ Обновляет или вставляет информацию о пользователе в базу данных. From 69b1705fd574c783b36cdc0ef2a1ac683d105040 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 04:15:52 +0300 Subject: [PATCH 02/28] Create basic main router for handlers --- handlers/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 handlers/__init__.py diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 00000000..9bd25e15 --- /dev/null +++ b/handlers/__init__.py @@ -0,0 +1,21 @@ +__all__ = ('router',) + +from aiogram import Router + +from .coupons import router as coupons_router +from .donate import router as donate_router +from .notifications import router as notifications_router +from .pay import router as pay_router +from .profile import router as profile_router +from .start import router as start_router + +router = Router(name='handlers_main_router') + +router.include_routers( + start_router, + profile_router, + pay_router, + donate_router, + coupons_router, + notifications_router, +) From 56ff84f7a5e871818cd0dcd1cf2f57e0cfbfd348 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 04:17:50 +0300 Subject: [PATCH 03/28] Create and implement router for payments --- handlers/__init__.py | 3 +++ handlers/payments/__init__.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 handlers/payments/__init__.py diff --git a/handlers/__init__.py b/handlers/__init__.py index 9bd25e15..74da7118 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -6,6 +6,7 @@ from .coupons import router as coupons_router from .donate import router as donate_router from .notifications import router as notifications_router from .pay import router as pay_router +from .payments import router as payments_router from .profile import router as profile_router from .start import router as start_router @@ -18,4 +19,6 @@ router.include_routers( donate_router, coupons_router, notifications_router, + + payments_router, ) diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py new file mode 100644 index 00000000..969f4913 --- /dev/null +++ b/handlers/payments/__init__.py @@ -0,0 +1,19 @@ +__all__ = ('router',) + +from aiogram import Router + +from .cryprobot_pay import router as cryprobot_router +from .freekassa_pay import router as freekassa_router +from .robokassa_pay import router as robokassa_router +from .stars_pay import router as stars_router +from .yookassa_pay import router as yookassa_router + +router = Router(name='payments_main_router') + +router.include_routers( + cryprobot_router, + stars_router, + freekassa_router, + robokassa_router, + yookassa_router, +) From fe655a7ee5ed67ee145a1967d6b8af7b610dbc35 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 04:19:16 +0300 Subject: [PATCH 04/28] Create and implement router for keys --- handlers/__init__.py | 2 ++ handlers/keys/__init__.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 handlers/keys/__init__.py diff --git a/handlers/__init__.py b/handlers/__init__.py index 74da7118..435191c1 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -4,6 +4,7 @@ from aiogram import Router from .coupons import router as coupons_router from .donate import router as donate_router +from .keys import router as keys_router from .notifications import router as notifications_router from .pay import router as pay_router from .payments import router as payments_router @@ -21,4 +22,5 @@ router.include_routers( notifications_router, payments_router, + keys_router, ) diff --git a/handlers/keys/__init__.py b/handlers/keys/__init__.py new file mode 100644 index 00000000..45256dad --- /dev/null +++ b/handlers/keys/__init__.py @@ -0,0 +1,13 @@ +__all__ = ('router',) + +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( + keys_router, + management_router, +) From d664036f5e8d4792730bfbd77c8de78c198bdf96 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 04:20:09 +0300 Subject: [PATCH 05/28] Create and implement router for instructions --- handlers/__init__.py | 2 ++ handlers/instructions/__init__.py | 11 +++++++++++ 2 files changed, 13 insertions(+) create mode 100644 handlers/instructions/__init__.py diff --git a/handlers/__init__.py b/handlers/__init__.py index 435191c1..31349930 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -4,6 +4,7 @@ from aiogram import Router from .coupons import router as coupons_router from .donate import router as donate_router +from .instructions import router as instructions_router from .keys import router as keys_router from .notifications import router as notifications_router from .pay import router as pay_router @@ -23,4 +24,5 @@ router.include_routers( payments_router, keys_router, + instructions_router, ) diff --git a/handlers/instructions/__init__.py b/handlers/instructions/__init__.py new file mode 100644 index 00000000..b8121003 --- /dev/null +++ b/handlers/instructions/__init__.py @@ -0,0 +1,11 @@ +__all__ = ('router',) + +from aiogram import Router + +from .instructions import router as instructions_router + +router = Router(name='instructions_main_router') + +router.include_routers( + instructions_router, +) From 8dc478a606a766a8b9a5b7f511fad1f9432d560d Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 04:21:37 +0300 Subject: [PATCH 06/28] Create and implement router for admin --- handlers/__init__.py | 2 ++ handlers/admin/__init__.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 handlers/admin/__init__.py diff --git a/handlers/__init__.py b/handlers/__init__.py index 31349930..613d08f9 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -2,6 +2,7 @@ __all__ = ('router',) from aiogram import Router +from .admin import router as admin_router from .coupons import router as coupons_router from .donate import router as donate_router from .instructions import router as instructions_router @@ -25,4 +26,5 @@ router.include_routers( payments_router, keys_router, instructions_router, + admin_router, ) diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py new file mode 100644 index 00000000..8f83973d --- /dev/null +++ b/handlers/admin/__init__.py @@ -0,0 +1,17 @@ +__all__ = ('router',) + +from aiogram import Router + +from .admin_coupons import router as coupons_router +from .admin_panel import router as panel_router +from .admin_servers import router as servers_router +from .admin_user_editor import router as user_editor_router + +router = Router(name='admins_main_router') + +router.include_routers( + panel_router, + servers_router, + coupons_router, + user_editor_router, +) From 822d84524124a689bb02faf2c047c876676efa12 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 04:23:59 +0300 Subject: [PATCH 07/28] Include routers only if payment method enabled in the config --- handlers/payments/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py index 969f4913..231dd68a 100644 --- a/handlers/payments/__init__.py +++ b/handlers/payments/__init__.py @@ -2,6 +2,7 @@ __all__ = ('router',) from aiogram import Router +from config import ROBOKASSA_ENABLE, STARS_ENABLE, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, YOOKASSA_ENABLE from .cryprobot_pay import router as cryprobot_router from .freekassa_pay import router as freekassa_router from .robokassa_pay import router as robokassa_router @@ -10,10 +11,13 @@ from .yookassa_pay import router as yookassa_router router = Router(name='payments_main_router') -router.include_routers( - cryprobot_router, - stars_router, - freekassa_router, - robokassa_router, - yookassa_router, -) +if YOOKASSA_ENABLE: + router.include_router(yookassa_router) +if FREEKASSA_ENABLE: + router.include_router(freekassa_router) +if ROBOKASSA_ENABLE: + router.include_router(robokassa_router) +if CRYPTO_BOT_ENABLE: + router.include_router(cryprobot_router) +if STARS_ENABLE: + router.include_router(stars_router) From b02c716992c9b266082d3a25a65e8df13cd62a58 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 04:36:59 +0300 Subject: [PATCH 08/28] Implement global router --- bot.py | 35 ++--------------------------------- main.py | 3 ++- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/bot.py b/bot.py index abf95cb9..62c86a2a 100644 --- a/bot.py +++ b/bot.py @@ -1,12 +1,12 @@ import traceback -from aiogram import Bot, Dispatcher, Router +from aiogram import Bot, Dispatcher 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, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE +from config import API_TOKEN from logger import logger from middlewares.admin import AdminMiddleware from middlewares.database import DatabaseMiddleware @@ -17,37 +17,6 @@ from middlewares.user import UserMiddleware bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) -router = Router() - -from handlers import coupons, donate, notifications, pay, profile, start -from handlers.admin import admin_coupons, admin_panel, admin_servers, admin_user_editor -from handlers.instructions import instructions -from handlers.keys import key_management, keys -from handlers.payments import cryprobot_pay, freekassa_pay, robokassa_pay, stars_pay, yookassa_pay - -dp.include_router(admin_coupons.router) -dp.include_router(admin_panel.router) -dp.include_router(admin_user_editor.router) -dp.include_router(admin_servers.router) -dp.include_router(coupons.router) -dp.include_router(start.router) -dp.include_router(profile.router) -dp.include_router(keys.router) -dp.include_router(key_management.router) -dp.include_router(pay.router) -dp.include_router(notifications.router) -dp.include_router(instructions.router) -dp.include_router(donate.router) -if YOOKASSA_ENABLE: - dp.include_router(yookassa_pay.router) -if FREEKASSA_ENABLE: - dp.include_router(freekassa_pay.router) -if CRYPTO_BOT_ENABLE: - dp.include_router(cryprobot_pay.router) -if STARS_ENABLE: - dp.include_router(stars_pay.router) -if ROBOKASSA_ENABLE: - dp.include_router(robokassa_pay.router) dp.message.middleware(LoggingMiddleware()) dp.callback_query.middleware(LoggingMiddleware()) diff --git a/main.py b/main.py index 3f2eb0d2..af659ad3 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,8 @@ from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_applicati from aiohttp import web from backup import backup_database -from bot import bot, dp, router +from bot import bot, dp +from handlers import router from config import ( BACKUP_TIME, CRYPTO_BOT_ENABLE, From 0e9c128a15167d11bc52ac39102df052504061c3 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 04:57:20 +0300 Subject: [PATCH 09/28] Move util-related files into separated folder + refactor imports --- handlers/admin/admin_coupons.py | 2 +- handlers/admin/admin_panel.py | 2 +- handlers/admin/admin_servers.py | 4 ++-- handlers/admin/admin_user_editor.py | 2 +- handlers/coupons.py | 2 +- handlers/keys/key_management.py | 5 ++--- handlers/keys/key_utils.py | 4 ++-- handlers/keys/keys.py | 2 +- handlers/keys/subscriptions.py | 2 +- handlers/keys/trial_key.py | 4 ++-- handlers/notifications.py | 2 +- handlers/payments/cryprobot_pay.py | 2 +- handlers/payments/freekassa_pay.py | 2 +- handlers/payments/robokassa_pay.py | 2 +- handlers/payments/stars_pay.py | 2 +- handlers/payments/yookassa_pay.py | 2 +- handlers/profile.py | 2 +- handlers/start.py | 2 +- handlers/utils.py | 2 +- main.py | 6 +++--- backup.py => utils/backup.py | 0 client.py => utils/client.py | 0 database.py => utils/database.py | 0 servers.py => utils/servers.py | 0 24 files changed, 26 insertions(+), 27 deletions(-) rename backup.py => utils/backup.py (100%) rename client.py => utils/client.py (100%) rename database.py => utils/database.py (100%) rename servers.py => utils/servers.py (100%) diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 5ea3ca98..c7964978 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -6,7 +6,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from database import create_coupon, delete_coupon_from_db, get_all_coupons +from utils.database import create_coupon, delete_coupon_from_db, get_all_coupons from filters.admin import IsAdminFilter from logger import logger diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index b0adf30a..8afa4d6a 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -10,7 +10,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from backup import backup_database +from utils.backup import backup_database from bot import bot from filters.admin import IsAdminFilter from logger import logger diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index b9260c66..23bddaf6 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -6,9 +6,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder import asyncpg from py3xui import AsyncApi -from backup import create_backup_and_send_to_admins +from utils.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 utils.database import check_unique_server_name, get_servers_from_db from filters.admin import IsAdminFilter router = Router() diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index d579cb5f..23a7be47 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -9,7 +9,7 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import TOTAL_GB -from database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry +from utils.database import 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, delete_key_from_db, renew_key_in_cluster from handlers.utils import sanitize_key_name diff --git a/handlers/coupons.py b/handlers/coupons.py index ed487671..59809fb1 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -7,7 +7,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from database import update_balance +from utils.database import update_balance class CouponActivationState(StatesGroup): diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 9efe8ec1..b147ec7e 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -15,14 +15,13 @@ from config import ( DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, - RENEWAL_PLANS, RENEWAL_PRICES, SUPPORT_CHAT_URL, TRIAL_TIME, ) -from database import get_balance, get_trial, store_key, update_balance +from utils.database import get_balance, get_trial, store_key, update_balance from handlers.keys.key_utils import create_key_on_cluster -from handlers.texts import DISCOUNTS, KEY, NULL_BALANCE, key_message_success +from handlers.texts import DISCOUNTS, KEY, key_message_success from handlers.utils import generate_random_email, get_least_loaded_cluster from logger import logger diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 8d39c8b9..36754467 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -3,9 +3,9 @@ import asyncio import asyncpg from py3xui import AsyncApi -from client import add_client, delete_client, extend_client_key +from utils.client import add_client, delete_client, extend_client_key from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB -from database import get_servers_from_db +from utils.database import get_servers_from_db from logger import logger diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 236545b4..1ac41038 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -9,7 +9,7 @@ from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, RENEWAL_PLANS, TOTAL_GB -from database import delete_key, get_balance, get_servers_from_db, store_key, update_balance, update_key_expiry +from utils.database import delete_key, get_balance, get_servers_from_db, store_key, update_balance, update_key_expiry from handlers.keys.key_utils import ( delete_key_from_cluster, delete_key_from_db, diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 45b0d3fc..176186c9 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -6,7 +6,7 @@ from aiohttp import web import asyncpg from config import DATABASE_URL, TRANSITION_DATE_STR -from database import get_servers_from_db +from utils.database import get_servers_from_db from logger import logger diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 819d66da..a97f2219 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -5,9 +5,9 @@ import uuid from py3xui import AsyncApi -from client import add_client +from utils.client import add_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME -from database import get_servers_from_db, store_key, use_trial +from utils.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 diff --git a/handlers/notifications.py b/handlers/notifications.py index 7ac8ccf2..bbf6fe8e 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -7,7 +7,7 @@ import asyncpg from py3xui import AsyncApi from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, DEV_MODE, RENEWAL_PLANS, TOTAL_GB, TRIAL_TIME -from database import ( +from utils.database import ( add_notification, check_notification_time, delete_key, diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index 6265d4db..eeab0bfb 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -9,7 +9,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 7c423129..49b38855 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -13,7 +13,7 @@ from aiohttp import web import requests from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID -from database import add_payment, update_balance +from utils.database import add_payment, update_balance from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 9ae90a29..b0db9d1e 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -10,7 +10,7 @@ from aiohttp import web from robokassa import HashAlgorithm, Robokassa from config import ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index 698444b8..18b9fa07 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -7,7 +7,7 @@ from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder from config import RUB_TO_XTR -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index c1687107..b71a5bf3 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -10,7 +10,7 @@ from aiohttp import web from yookassa import Configuration, Payment from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/profile.py b/handlers/profile.py index 005aecf1..34c514ce 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -6,7 +6,7 @@ from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import NEWS_MESSAGE, RENEWAL_PLANS -from database import get_balance, get_key_count, get_referral_stats +from utils.database import get_balance, get_key_count, get_referral_stats from handlers.texts import get_referral_link, invite_message_send, profile_message_send router = Router() diff --git a/handlers/start.py b/handlers/start.py index 3c15d9bc..00a27dda 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -8,7 +8,7 @@ from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL -from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial +from utils.database import add_connection, add_referral, check_connection_exists, get_trial, use_trial from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn diff --git a/handlers/utils.py b/handlers/utils.py index 95871cf6..42709e76 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -6,7 +6,7 @@ import asyncpg from bot import bot from config import DATABASE_URL -from database import get_servers_from_db +from utils.database import get_servers_from_db from logger import logger diff --git a/main.py b/main.py index 3f2eb0d2..65850d49 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ import signal from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application from aiohttp import web -from backup import backup_database +from utils.backup import backup_database from bot import bot, dp, router from config import ( BACKUP_TIME, @@ -20,7 +20,7 @@ from config import ( WEBHOOK_URL, YOOKASSA_ENABLE, ) -from database import init_db +from utils.database import init_db from handlers.keys.subscriptions import handle_new_subscription, handle_old_subscription from handlers.notifications import notify_expiring_keys from handlers.payments.cryprobot_pay import cryptobot_webhook @@ -28,7 +28,7 @@ from handlers.payments.freekassa_pay import freekassa_webhook from handlers.payments.robokassa_pay import robokassa_webhook from handlers.payments.yookassa_pay import yookassa_webhook from logger import logger -from servers import check_servers, sync_servers_with_db +from utils.servers import check_servers, sync_servers_with_db async def periodic_notifications(): diff --git a/backup.py b/utils/backup.py similarity index 100% rename from backup.py rename to utils/backup.py diff --git a/client.py b/utils/client.py similarity index 100% rename from client.py rename to utils/client.py diff --git a/database.py b/utils/database.py similarity index 100% rename from database.py rename to utils/database.py diff --git a/servers.py b/utils/servers.py similarity index 100% rename from servers.py rename to utils/servers.py From 3e4d7972c7488958629ece834ae75dcf40ad4ebc Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 05:02:41 +0300 Subject: [PATCH 10/28] Move utils.py to the utils folder --- handlers/admin/admin_user_editor.py | 2 +- handlers/keys/key_management.py | 2 +- handlers/keys/keys.py | 2 +- handlers/keys/trial_key.py | 2 +- {handlers => utils}/utils.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename {handlers => utils}/utils.py (98%) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 23a7be47..8d868b73 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -12,7 +12,7 @@ from config import TOTAL_GB from utils.database import 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, delete_key_from_db, renew_key_in_cluster -from handlers.utils import sanitize_key_name +from utils.utils import sanitize_key_name from logger import logger router = Router() diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index b147ec7e..bc4396da 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -22,7 +22,7 @@ from config import ( from utils.database import get_balance, get_trial, store_key, update_balance from handlers.keys.key_utils import create_key_on_cluster from handlers.texts import DISCOUNTS, KEY, key_message_success -from handlers.utils import generate_random_email, get_least_loaded_cluster +from utils.utils import generate_random_email, get_least_loaded_cluster from logger import logger router = Router() diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 1ac41038..30ffd7b0 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -25,7 +25,7 @@ from handlers.texts import ( SUCCESS_RENEWAL_MSG, key_message, ) -from handlers.utils import get_least_loaded_cluster, handle_error +from utils.utils import get_least_loaded_cluster, handle_error from logger import logger locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index a97f2219..0ed8f7bb 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -9,7 +9,7 @@ from utils.client import add_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME from utils.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 +from utils.utils import generate_random_email, get_least_loaded_cluster async def create_trial_key(tg_id: int, session: Any): diff --git a/handlers/utils.py b/utils/utils.py similarity index 98% rename from handlers/utils.py rename to utils/utils.py index 42709e76..95871cf6 100644 --- a/handlers/utils.py +++ b/utils/utils.py @@ -6,7 +6,7 @@ import asyncpg from bot import bot from config import DATABASE_URL -from utils.database import get_servers_from_db +from database import get_servers_from_db from logger import logger From 9756d2e2b7104228345c994c3a31c080febeeb58 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 05:11:31 +0300 Subject: [PATCH 11/28] Move keys utils into utils folder --- handlers/admin/admin_user_editor.py | 2 +- handlers/keys/key_management.py | 2 +- handlers/keys/keys.py | 2 +- handlers/notifications.py | 2 +- handlers/start.py | 2 +- main.py | 2 +- {handlers => utils}/keys/key_utils.py | 0 {handlers => utils}/keys/subscriptions.py | 0 {handlers => utils}/keys/trial_key.py | 0 9 files changed, 6 insertions(+), 6 deletions(-) rename {handlers => utils}/keys/key_utils.py (100%) rename {handlers => utils}/keys/subscriptions.py (100%) rename {handlers => utils}/keys/trial_key.py (100%) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 8d868b73..d840a875 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -11,7 +11,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from config import TOTAL_GB from utils.database import 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, delete_key_from_db, renew_key_in_cluster +from utils.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster from utils.utils import sanitize_key_name from logger import logger diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index bc4396da..1c417788 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -20,7 +20,7 @@ from config import ( TRIAL_TIME, ) from utils.database import get_balance, get_trial, store_key, update_balance -from handlers.keys.key_utils import create_key_on_cluster +from utils.keys.key_utils import create_key_on_cluster from handlers.texts import DISCOUNTS, KEY, key_message_success from utils.utils import generate_random_email, get_least_loaded_cluster from logger import logger diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 30ffd7b0..e75c1c30 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -10,7 +10,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, RENEWAL_PLANS, TOTAL_GB from utils.database import delete_key, get_balance, get_servers_from_db, store_key, update_balance, update_key_expiry -from handlers.keys.key_utils import ( +from utils.keys.key_utils import ( delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster, diff --git a/handlers/notifications.py b/handlers/notifications.py index bbf6fe8e..68ae5909 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -16,7 +16,7 @@ from utils.database import ( update_balance, update_key_expiry, ) -from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster +from utils.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED from logger import logger diff --git a/handlers/start.py b/handlers/start.py index 00a27dda..3f678978 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -9,7 +9,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL from utils.database import add_connection, add_referral, check_connection_exists, get_trial, use_trial -from handlers.keys.trial_key import create_trial_key +from utils.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn router = Router() diff --git a/main.py b/main.py index 65850d49..92c9f411 100644 --- a/main.py +++ b/main.py @@ -21,7 +21,7 @@ from config import ( YOOKASSA_ENABLE, ) from utils.database import init_db -from handlers.keys.subscriptions import handle_new_subscription, handle_old_subscription +from utils.keys.subscriptions import handle_new_subscription, handle_old_subscription from handlers.notifications import notify_expiring_keys from handlers.payments.cryprobot_pay import cryptobot_webhook from handlers.payments.freekassa_pay import freekassa_webhook diff --git a/handlers/keys/key_utils.py b/utils/keys/key_utils.py similarity index 100% rename from handlers/keys/key_utils.py rename to utils/keys/key_utils.py diff --git a/handlers/keys/subscriptions.py b/utils/keys/subscriptions.py similarity index 100% rename from handlers/keys/subscriptions.py rename to utils/keys/subscriptions.py diff --git a/handlers/keys/trial_key.py b/utils/keys/trial_key.py similarity index 100% rename from handlers/keys/trial_key.py rename to utils/keys/trial_key.py From 94d335d950cb7c272dae985bfcc9de897c0cac5a Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 05:13:38 +0300 Subject: [PATCH 12/28] Move payments utils into utils folder --- handlers/payments/cryprobot_pay.py | 2 +- handlers/payments/freekassa_pay.py | 2 +- handlers/payments/robokassa_pay.py | 2 +- handlers/payments/stars_pay.py | 2 +- handlers/payments/yookassa_pay.py | 2 +- {handlers => utils}/payments/utils.py | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename {handlers => utils}/payments/utils.py (100%) diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index eeab0bfb..7d2188f4 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -10,7 +10,7 @@ from aiohttp import web from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance -from handlers.payments.utils import send_payment_success_notification +from utils.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 49b38855..6e1c33fe 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -14,7 +14,7 @@ import requests from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID from utils.database import add_payment, update_balance -from handlers.payments.utils import send_payment_success_notification +from utils.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS router = Router() diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index b0db9d1e..bfa09e2c 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -11,7 +11,7 @@ from robokassa import HashAlgorithm, Robokassa from config import ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance -from handlers.payments.utils import send_payment_success_notification +from utils.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index 18b9fa07..75135c5e 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -8,7 +8,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from config import RUB_TO_XTR from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance -from handlers.payments.utils import send_payment_success_notification +from utils.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index b71a5bf3..d0dff390 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -11,7 +11,7 @@ from yookassa import Configuration, Payment from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance -from handlers.payments.utils import send_payment_success_notification +from utils.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/utils.py b/utils/payments/utils.py similarity index 100% rename from handlers/payments/utils.py rename to utils/payments/utils.py From eee8d864260098dcbe3d681325fce689190a55d3 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 8 Dec 2024 05:32:23 +0300 Subject: [PATCH 13/28] Make utils as package + fix python unresolved imports --- middlewares/user.py | 2 +- utils/__init__.py | 0 utils/servers.py | 2 +- utils/utils.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 utils/__init__.py diff --git a/middlewares/user.py b/middlewares/user.py index 969020ef..ee130b16 100644 --- a/middlewares/user.py +++ b/middlewares/user.py @@ -3,7 +3,7 @@ from typing import Any, Awaitable, Callable, Dict from aiogram import BaseMiddleware from aiogram.types import TelegramObject, User -from database import upsert_user +from utils.database import upsert_user class UserMiddleware(BaseMiddleware): diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/utils/servers.py b/utils/servers.py index fbb81968..4a275bc9 100644 --- a/utils/servers.py +++ b/utils/servers.py @@ -9,7 +9,7 @@ from ping3 import ping from bot import bot from config import ADMIN_ID, DATABASE_URL -from database import get_servers_from_db +from utils.database import get_servers_from_db from logger import logger try: diff --git a/utils/utils.py b/utils/utils.py index 95871cf6..42709e76 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -6,7 +6,7 @@ import asyncpg from bot import bot from config import DATABASE_URL -from database import get_servers_from_db +from utils.database import get_servers_from_db from logger import logger From bc1ddc349a470e387df866c6f40be496e17fe970 Mon Sep 17 00:00:00 2001 From: Vladless Date: Tue, 10 Dec 2024 03:53:07 +0300 Subject: [PATCH 14/28] Revert "Merge pull request #112 from Vladless/hteppl-optimize-sources" This reverts commit 5f557fa2b217448bfe2f77bedd6576c792d8677f, reversing changes made to 4ea1f101ff364c6ef58cf9c50145fefe3c6afc3b. --- utils/backup.py => backup.py | 0 utils/client.py => client.py | 0 utils/database.py => database.py | 0 handlers/admin/admin_coupons.py | 2 +- handlers/admin/admin_panel.py | 2 +- handlers/admin/admin_servers.py | 4 ++-- handlers/admin/admin_user_editor.py | 6 +++--- handlers/coupons.py | 2 +- handlers/keys/key_management.py | 9 +++++---- {utils => handlers}/keys/key_utils.py | 4 ++-- handlers/keys/keys.py | 6 +++--- {utils => handlers}/keys/subscriptions.py | 2 +- {utils => handlers}/keys/trial_key.py | 6 +++--- handlers/notifications.py | 4 ++-- handlers/payments/cryprobot_pay.py | 4 ++-- handlers/payments/freekassa_pay.py | 4 ++-- handlers/payments/robokassa_pay.py | 4 ++-- handlers/payments/stars_pay.py | 4 ++-- {utils => handlers}/payments/utils.py | 0 handlers/payments/yookassa_pay.py | 4 ++-- handlers/profile.py | 2 +- handlers/start.py | 4 ++-- {utils => handlers}/utils.py | 2 +- main.py | 9 +++++---- middlewares/user.py | 2 +- utils/servers.py => servers.py | 2 +- utils/__init__.py | 0 27 files changed, 45 insertions(+), 43 deletions(-) rename utils/backup.py => backup.py (100%) rename utils/client.py => client.py (100%) rename utils/database.py => database.py (100%) rename {utils => handlers}/keys/key_utils.py (98%) rename {utils => handlers}/keys/subscriptions.py (99%) rename {utils => handlers}/keys/trial_key.py (91%) rename {utils => handlers}/payments/utils.py (100%) rename {utils => handlers}/utils.py (98%) rename utils/servers.py => servers.py (99%) delete mode 100644 utils/__init__.py diff --git a/utils/backup.py b/backup.py similarity index 100% rename from utils/backup.py rename to backup.py diff --git a/utils/client.py b/client.py similarity index 100% rename from utils/client.py rename to client.py diff --git a/utils/database.py b/database.py similarity index 100% rename from utils/database.py rename to database.py diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index c7964978..5ea3ca98 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -6,7 +6,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from utils.database import create_coupon, delete_coupon_from_db, get_all_coupons +from database import create_coupon, delete_coupon_from_db, get_all_coupons from filters.admin import IsAdminFilter from logger import logger diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 8afa4d6a..b0adf30a 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -10,7 +10,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from utils.backup import backup_database +from backup import backup_database from bot import bot from filters.admin import IsAdminFilter from logger import logger diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 23bddaf6..b9260c66 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -6,9 +6,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder import asyncpg from py3xui import AsyncApi -from utils.backup import create_backup_and_send_to_admins +from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL -from utils.database import check_unique_server_name, get_servers_from_db +from database import check_unique_server_name, get_servers_from_db from filters.admin import IsAdminFilter router = Router() diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index d840a875..d579cb5f 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -9,10 +9,10 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import TOTAL_GB -from utils.database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry +from database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry from filters.admin import IsAdminFilter -from utils.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster -from utils.utils import sanitize_key_name +from handlers.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster +from handlers.utils import sanitize_key_name from logger import logger router = Router() diff --git a/handlers/coupons.py b/handlers/coupons.py index 59809fb1..ed487671 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -7,7 +7,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from utils.database import update_balance +from database import update_balance class CouponActivationState(StatesGroup): diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 1c417788..9efe8ec1 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -15,14 +15,15 @@ from config import ( DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, + RENEWAL_PLANS, RENEWAL_PRICES, SUPPORT_CHAT_URL, TRIAL_TIME, ) -from utils.database import get_balance, get_trial, store_key, update_balance -from utils.keys.key_utils import create_key_on_cluster -from handlers.texts import DISCOUNTS, KEY, key_message_success -from utils.utils import generate_random_email, get_least_loaded_cluster +from database import get_balance, get_trial, store_key, update_balance +from handlers.keys.key_utils import create_key_on_cluster +from handlers.texts import DISCOUNTS, KEY, NULL_BALANCE, key_message_success +from handlers.utils import generate_random_email, get_least_loaded_cluster from logger import logger router = Router() diff --git a/utils/keys/key_utils.py b/handlers/keys/key_utils.py similarity index 98% rename from utils/keys/key_utils.py rename to handlers/keys/key_utils.py index 36754467..8d39c8b9 100644 --- a/utils/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -3,9 +3,9 @@ import asyncio import asyncpg from py3xui import AsyncApi -from utils.client import add_client, delete_client, extend_client_key +from client import add_client, delete_client, extend_client_key from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB -from utils.database import get_servers_from_db +from database import get_servers_from_db from logger import logger diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index e75c1c30..236545b4 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -9,8 +9,8 @@ from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, RENEWAL_PLANS, TOTAL_GB -from utils.database import delete_key, get_balance, get_servers_from_db, store_key, update_balance, update_key_expiry -from utils.keys.key_utils import ( +from database import delete_key, get_balance, get_servers_from_db, store_key, update_balance, update_key_expiry +from handlers.keys.key_utils import ( delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster, @@ -25,7 +25,7 @@ from handlers.texts import ( SUCCESS_RENEWAL_MSG, key_message, ) -from utils.utils import get_least_loaded_cluster, handle_error +from handlers.utils import get_least_loaded_cluster, handle_error from logger import logger locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") diff --git a/utils/keys/subscriptions.py b/handlers/keys/subscriptions.py similarity index 99% rename from utils/keys/subscriptions.py rename to handlers/keys/subscriptions.py index 176186c9..45b0d3fc 100644 --- a/utils/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -6,7 +6,7 @@ from aiohttp import web import asyncpg from config import DATABASE_URL, TRANSITION_DATE_STR -from utils.database import get_servers_from_db +from database import get_servers_from_db from logger import logger diff --git a/utils/keys/trial_key.py b/handlers/keys/trial_key.py similarity index 91% rename from utils/keys/trial_key.py rename to handlers/keys/trial_key.py index 0ed8f7bb..819d66da 100644 --- a/utils/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -5,11 +5,11 @@ import uuid from py3xui import AsyncApi -from utils.client import add_client +from client import add_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME -from utils.database import get_servers_from_db, store_key, use_trial +from database import get_servers_from_db, store_key, use_trial from handlers.texts import INSTRUCTIONS -from utils.utils import generate_random_email, get_least_loaded_cluster +from handlers.utils import generate_random_email, get_least_loaded_cluster async def create_trial_key(tg_id: int, session: Any): diff --git a/handlers/notifications.py b/handlers/notifications.py index 68ae5909..7ac8ccf2 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -7,7 +7,7 @@ import asyncpg from py3xui import AsyncApi from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, DEV_MODE, RENEWAL_PLANS, TOTAL_GB, TRIAL_TIME -from utils.database import ( +from database import ( add_notification, check_notification_time, delete_key, @@ -16,7 +16,7 @@ from utils.database import ( update_balance, update_key_expiry, ) -from utils.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster +from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED from logger import logger diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index 7d2188f4..6265d4db 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -9,8 +9,8 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT -from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance -from utils.payments.utils import send_payment_success_notification +from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 6e1c33fe..7c423129 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -13,8 +13,8 @@ from aiohttp import web import requests from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID -from utils.database import add_payment, update_balance -from utils.payments.utils import send_payment_success_notification +from database import add_payment, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS router = Router() diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index bfa09e2c..9ae90a29 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -10,8 +10,8 @@ from aiohttp import web from robokassa import HashAlgorithm, Robokassa from config import ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE -from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance -from utils.payments.utils import send_payment_success_notification +from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index 75135c5e..698444b8 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -7,8 +7,8 @@ from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder from config import RUB_TO_XTR -from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance -from utils.payments.utils import send_payment_success_notification +from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/utils/payments/utils.py b/handlers/payments/utils.py similarity index 100% rename from utils/payments/utils.py rename to handlers/payments/utils.py diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index d0dff390..c1687107 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -10,8 +10,8 @@ from aiohttp import web from yookassa import Configuration, Payment from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID -from utils.database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance -from utils.payments.utils import send_payment_success_notification +from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger diff --git a/handlers/profile.py b/handlers/profile.py index 34c514ce..005aecf1 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -6,7 +6,7 @@ from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import NEWS_MESSAGE, RENEWAL_PLANS -from utils.database import get_balance, get_key_count, get_referral_stats +from database import get_balance, get_key_count, get_referral_stats from handlers.texts import get_referral_link, invite_message_send, profile_message_send router = Router() diff --git a/handlers/start.py b/handlers/start.py index 3f678978..3c15d9bc 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -8,8 +8,8 @@ from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL -from utils.database import add_connection, add_referral, check_connection_exists, get_trial, use_trial -from utils.keys.trial_key import create_trial_key +from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial +from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn router = Router() diff --git a/utils/utils.py b/handlers/utils.py similarity index 98% rename from utils/utils.py rename to handlers/utils.py index 42709e76..95871cf6 100644 --- a/utils/utils.py +++ b/handlers/utils.py @@ -6,7 +6,7 @@ import asyncpg from bot import bot from config import DATABASE_URL -from utils.database import get_servers_from_db +from database import get_servers_from_db from logger import logger diff --git a/main.py b/main.py index 32d0a297..af659ad3 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,8 @@ import signal from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application from aiohttp import web -from utils.backup import backup_database + +from backup import backup_database from bot import bot, dp from handlers import router from config import ( @@ -20,15 +21,15 @@ from config import ( WEBHOOK_URL, YOOKASSA_ENABLE, ) -from utils.database import init_db -from utils.keys.subscriptions import handle_new_subscription, handle_old_subscription +from database import init_db +from handlers.keys.subscriptions import handle_new_subscription, handle_old_subscription from handlers.notifications import notify_expiring_keys from handlers.payments.cryprobot_pay import cryptobot_webhook from handlers.payments.freekassa_pay import freekassa_webhook from handlers.payments.robokassa_pay import robokassa_webhook from handlers.payments.yookassa_pay import yookassa_webhook from logger import logger -from utils.servers import check_servers, sync_servers_with_db +from servers import check_servers, sync_servers_with_db async def periodic_notifications(): diff --git a/middlewares/user.py b/middlewares/user.py index ee130b16..969020ef 100644 --- a/middlewares/user.py +++ b/middlewares/user.py @@ -3,7 +3,7 @@ from typing import Any, Awaitable, Callable, Dict from aiogram import BaseMiddleware from aiogram.types import TelegramObject, User -from utils.database import upsert_user +from database import upsert_user class UserMiddleware(BaseMiddleware): diff --git a/utils/servers.py b/servers.py similarity index 99% rename from utils/servers.py rename to servers.py index 4a275bc9..fbb81968 100644 --- a/utils/servers.py +++ b/servers.py @@ -9,7 +9,7 @@ from ping3 import ping from bot import bot from config import ADMIN_ID, DATABASE_URL -from utils.database import get_servers_from_db +from database import get_servers_from_db from logger import logger try: diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 From 145c716529bee351255b0b542efe099e357dfc11 Mon Sep 17 00:00:00 2001 From: Vladless Date: Tue, 10 Dec 2024 05:12:49 +0300 Subject: [PATCH 15/28] pic for tariffs --- handlers/profile.py | 26 +++++++++++++++++++++++--- img/tariffs.jpg | Bin 0 -> 37214 bytes 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 img/tariffs.jpg diff --git a/handlers/profile.py b/handlers/profile.py index 34c514ce..49c9cd2e 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -68,7 +68,11 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await callback_query.message.answer( + # Путь к изображению + image_path = os.path.join("img", "tariffs.jpg") # Убедитесь, что этот путь правильный + + # Формируем текст с тарифами + tariffs_message = ( "🚀 Доступные тарифы VPN:\n\n" + "\n".join( [ @@ -77,10 +81,26 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей" for months in sorted(RENEWAL_PLANS.keys(), key=int) ] - ), - reply_markup=builder.as_markup(), + ) ) + # Проверяем наличие файла изображения + if os.path.isfile(image_path): + # Если изображение существует, отправляем его + with open(image_path, "rb") as image_file: + await callback_query.message.answer_photo( + photo=BufferedInputFile(image_file.read(), filename="tariffs.jpg"), + caption=tariffs_message, + reply_markup=builder.as_markup(), + ) + else: + # Если изображения нет, просто отправляем текст + await callback_query.message.answer( + text=tariffs_message, + reply_markup=builder.as_markup(), + ) + + @router.callback_query(F.data == "invite") async def invite_handler(callback_query: types.CallbackQuery): diff --git a/img/tariffs.jpg b/img/tariffs.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1064f788e4c426561f4b6bc013f0757c77c69583 GIT binary patch literal 37214 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

%Mi|({L9?P<)Ew4y3CG113u1Yq689Fnc*rs8~ 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 literal 0 HcmV?d00001 From a81b09eb1ecdfdff0d9162a7b227112372b6a08f Mon Sep 17 00:00:00 2001 From: Vladless Date: Tue, 10 Dec 2024 05:13:54 +0300 Subject: [PATCH 16/28] pic for tariffs --- handlers/start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/start.py b/handlers/start.py index 3f678978..c258766a 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -116,4 +116,4 @@ async def handle_about_vpn(callback_query: CallbackQuery): ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start")) - await callback_query.message.answer(get_about_vpn("3.2.2-Release"), reply_markup=builder.as_markup()) + await callback_query.message.answer(get_about_vpn("3.2.21-Release"), reply_markup=builder.as_markup()) From 2b572da0892ebdbb6c32aa566cb44818c4565601 Mon Sep 17 00:00:00 2001 From: Vladless Date: Wed, 11 Dec 2024 15:41:45 +0300 Subject: [PATCH 17/28] rm wrench --- handlers/profile.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index baf787f8..80775e0e 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -27,7 +27,7 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta if key_count == 0: profile_message += "\n

🔧 Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение
" else: - profile_message += f"\n
🔧 {NEWS_MESSAGE}
" + profile_message += f"\n
 {NEWS_MESSAGE}
" builder = InlineKeyboardBuilder() builder.row( @@ -68,10 +68,8 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - # Путь к изображению - image_path = os.path.join("img", "tariffs.jpg") # Убедитесь, что этот путь правильный + image_path = os.path.join("img", "tariffs.jpg") - # Формируем текст с тарифами tariffs_message = ( "🚀 Доступные тарифы VPN:\n\n" + "\n".join( @@ -84,9 +82,7 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): ) ) - # Проверяем наличие файла изображения if os.path.isfile(image_path): - # Если изображение существует, отправляем его with open(image_path, "rb") as image_file: await callback_query.message.answer_photo( photo=BufferedInputFile(image_file.read(), filename="tariffs.jpg"), @@ -94,7 +90,6 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): reply_markup=builder.as_markup(), ) else: - # Если изображения нет, просто отправляем текст await callback_query.message.answer( text=tariffs_message, reply_markup=builder.as_markup(), From 69f7d05527c06a8331b713bced8534afccd4631c Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Fri, 13 Dec 2024 12:09:20 +0300 Subject: [PATCH 18/28] Migrate to ruff --- .flake8 | 6 - Makefile | 9 +- backup.py | 11 +- bot.py | 2 +- client.py | 12 +- database.py | 191 ++++++++++++++------- filters/admin.py | 4 +- handlers/__init__.py | 5 +- handlers/admin/__init__.py | 4 +- handlers/admin/admin_coupons.py | 28 +++- handlers/admin/admin_panel.py | 132 +++++++++++---- handlers/admin/admin_servers.py | 198 ++++++++++++++++------ handlers/admin/admin_user_editor.py | 229 +++++++++++++++++++------- handlers/coupons.py | 8 +- handlers/donate.py | 24 ++- handlers/instructions/__init__.py | 4 +- handlers/instructions/instructions.py | 16 +- handlers/keys/__init__.py | 4 +- handlers/keys/key_management.py | 103 +++++++++--- handlers/keys/key_utils.py | 22 ++- handlers/keys/keys.py | 171 ++++++++++++++----- handlers/keys/subscriptions.py | 40 +++-- handlers/keys/trial_key.py | 4 +- handlers/notifications.py | 119 +++++++++---- handlers/pay.py | 19 ++- handlers/payments/__init__.py | 12 +- handlers/payments/cryprobot_pay.py | 50 ++++-- handlers/payments/freekassa_pay.py | 38 +++-- handlers/payments/robokassa_pay.py | 48 ++++-- handlers/payments/stars_pay.py | 38 +++-- handlers/payments/utils.py | 4 +- handlers/payments/yookassa_pay.py | 37 +++-- handlers/profile.py | 30 ++-- handlers/start.py | 58 +++++-- handlers/utils.py | 11 +- logger.py | 2 +- main.py | 12 +- middlewares/admin.py | 10 +- middlewares/database.py | 10 +- middlewares/delete.py | 11 +- middlewares/logging.py | 9 +- middlewares/user.py | 7 +- pyproject.toml | 42 ++--- requirements.txt | 4 - servers.py | 62 +++++-- 45 files changed, 1335 insertions(+), 525 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b51e9062..00000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -max-line-length = 120 -ignore = E203, E266, E501, W503, F541, E704, W293, W291, E126, E121, E123, E128, E302, E131, E231, W292, E402, E261, E305, E701 -max-complexity = 15 -select = B, C, E, F, W, T4, B9 -exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.txt,config.py \ No newline at end of file diff --git a/Makefile b/Makefile index 2a739bd8..aef0b00f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ formatting: - @echo "Running black..." && black . - @echo "Running isort..." && isort . - @echo "Running flake8..." && flake8 --config .flake8 - # @echo "Running pylint..." && pylint . + @echo "Running Ruff..." && ruff check . --fix + @echo "Running Ruff format..." && ruff format . + +lint: + @echo "Running Ruff checks..." && ruff check . \ No newline at end of file diff --git a/backup.py b/backup.py index ccee564c..329d7a22 100644 --- a/backup.py +++ b/backup.py @@ -1,11 +1,10 @@ -from datetime import datetime import os import subprocess -from typing import Union +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 @@ -57,8 +56,10 @@ def _create_database_backup(): async def _send_backup_to_admin(bot, backup_file_path): try: with open(backup_file_path, "rb") as backup_file: - backup_input_file = BufferedInputFile(backup_file.read(), filename=os.path.basename(backup_file_path)) - admin_ids: Union[int, list[int]] = ADMIN_ID + backup_input_file = BufferedInputFile( + backup_file.read(), filename=os.path.basename(backup_file_path) + ) + admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): for id in admin_ids: await bot.send_document(id, backup_input_file) diff --git a/bot.py b/bot.py index 62c86a2a..8799cd3c 100644 --- a/bot.py +++ b/bot.py @@ -5,8 +5,8 @@ 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 diff --git a/client.py b/client.py index 660214e0..76451786 100644 --- a/client.py +++ b/client.py @@ -44,7 +44,9 @@ async def add_client( return {"status": "failed", "error": str(e)} -async def extend_client_key(xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int): +async def extend_client_key( + xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int +): """ Функция для обновления срока действия ключа клиента по email. """ @@ -60,7 +62,9 @@ async def extend_client_key(xui, inbound_id, email: str, new_expiry_time: int, c logger.warning(f"Ошибка: клиент {email} не имеет действительного ID.") return - logger.info(f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}") + logger.info( + f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}" + ) client.id = client_id client.expiry_time = new_expiry_time @@ -73,7 +77,9 @@ async def extend_client_key(xui, inbound_id, email: str, new_expiry_time: int, c await xui.client.update(client.id, client) await xui.client.reset_stats(inbound_id, email) - logger.info(f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}.") + logger.info( + f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}." + ) except Exception as e: logger.error(f"Ошибка при обновлении клиента с email {email}: {e}") diff --git a/database.py b/database.py index a51ceb17..0472eac5 100644 --- a/database.py +++ b/database.py @@ -7,21 +7,21 @@ from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES from logger import logger -async def init_db(file_path: str = 'assets/schema.sql'): - with open(file_path, 'r') as file: +async def init_db(file_path: str = "assets/schema.sql"): + with open(file_path) as file: sql_content = file.read() # Split the file content into individual SQL statements and connect to the database - statements = [stmt.strip() for stmt in sql_content.split(';') if stmt.strip()] + statements = [stmt.strip() for stmt in sql_content.split(";") if stmt.strip()] conn = await asyncpg.connect(DATABASE_URL) try: for statement in statements: await conn.execute(statement) except Exception as e: - logger.error(f'Error while executing SQL statement: {e}') + logger.error(f"Error while executing SQL statement: {e}") finally: - logger.info('Tables created successfully') + logger.info("Tables created successfully") await conn.close() @@ -34,14 +34,18 @@ async def check_unique_server_name(server_name: str) -> bool: """ conn = await asyncpg.connect(DATABASE_URL) - result = await conn.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name) + result = await conn.fetchrow( + "SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name + ) await conn.close() return result is None -async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any): +async def create_coupon( + coupon_code: str, amount: float, usage_limit: int, session: Any +): """ Создает новый купон в базе данных. @@ -180,7 +184,9 @@ async def restore_trial(tg_id: int, session: Any): logger.info(f"Триальный период успешно восстановлен для пользователя {tg_id}") return True except Exception as e: - logger.error(f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}" + ) return False @@ -212,7 +218,9 @@ async def use_trial(tg_id: int, session: Any): return False -async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None): +async def add_connection( + tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None +): """ Добавляет новое подключение для пользователя в базу данных. @@ -239,7 +247,9 @@ async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, sessi f"Успешно добавлено новое подключение для пользователя {tg_id} с балансом {balance} и статусом триала {trial}" ) except Exception as e: - logger.error(f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}") + logger.error( + f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}" + ) raise @@ -276,7 +286,15 @@ async def check_connection_exists(tg_id: int): await conn.close() -async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str, session: Any): +async def store_key( + tg_id: int, + client_id: str, + email: str, + expiry_time: int, + key: str, + server_id: str, + session: Any, +): """ Сохраняет информацию о ключе в базу данных. @@ -305,7 +323,9 @@ async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, ke key, server_id, ) - logger.info(f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}") + logger.info( + f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}" + ) except Exception as e: logger.error(f"Ошибка при сохранении ключа для пользователя {tg_id}: {e}") raise @@ -371,10 +391,14 @@ async def get_keys_by_server(tg_id: int, server_id: str): tg_id, server_id, ) - logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}") + logger.info( + f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}" + ) return records except Exception as e: - logger.error(f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}") + logger.error( + f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}" + ) raise finally: if conn: @@ -398,10 +422,14 @@ async def has_active_key(tg_id: int) -> bool: try: conn = await asyncpg.connect(DATABASE_URL) count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - logger.info(f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}") + logger.info( + f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}" + ) return count > 0 except Exception as e: - logger.error(f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}" + ) raise finally: if conn: @@ -424,7 +452,9 @@ async def get_balance(tg_id: int) -> float: conn = None try: conn = await asyncpg.connect(DATABASE_URL) - balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + balance = await conn.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) logger.info(f"Получен баланс для пользователя {tg_id}: {balance}") return balance if balance is not None else 0.0 except Exception as e: @@ -482,11 +512,15 @@ async def get_trial(tg_id: int, session: Any) -> int: int: Статус триала (0 - не использован, 1 - использован) """ try: - trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) + trial = await session.fetchval( + "SELECT trial FROM connections WHERE tg_id = $1", tg_id + ) logger.info(f"Получен статус триала для пользователя {tg_id}: {trial}") return trial if trial is not None else 0 except Exception as e: - logger.error(f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}" + ) return 0 @@ -510,7 +544,9 @@ async def get_key_count(tg_id: int) -> int: logger.info(f"Получено количество ключей для пользователя {tg_id}: {count}") return count if count is not None else 0 except Exception as e: - logger.error(f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}" + ) return 0 finally: if conn: @@ -550,7 +586,9 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): referred_tg_id, referrer_tg_id, ) - logger.info(f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}") + logger.info( + f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}" + ) except Exception as e: logger.error(f"Ошибка при добавлении реферала: {e}") raise @@ -584,7 +622,9 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): for level in range(1, MAX_REFERRAL_LEVELS + 1): if current_tg_id in visited_tg_ids: - logger.warning(f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение.") + logger.warning( + f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение." + ) break visited_tg_ids.add(current_tg_id) @@ -601,26 +641,30 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): if not referral: break - referrer_tg_id = referral['referrer_tg_id'] - referral_chain.append({'tg_id': referrer_tg_id, 'level': level}) + referrer_tg_id = referral["referrer_tg_id"] + referral_chain.append({"tg_id": referrer_tg_id, "level": level}) current_tg_id = referrer_tg_id for referral in referral_chain: - referrer_tg_id = referral['tg_id'] - level = referral['level'] + referrer_tg_id = referral["tg_id"] + level = referral["level"] bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0) bonus = amount * bonus_percent bonus = max(bonus, 0) if bonus > 0: - logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}") + logger.info( + f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}" + ) await update_balance(referrer_tg_id, bonus) except Exception as e: - logger.error(f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}") + logger.error( + f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}" + ) finally: if conn: await conn.close() @@ -696,7 +740,10 @@ async def get_referral_stats(referrer_tg_id: int): # Преобразование результатов в словарь referrals_by_level = { - record['level']: {'total': record['level_count'], 'active': record['active_level_count']} + record["level"]: { + "total": record["level_count"], + "active": record["active_level_count"], + } for record in referrals_by_level_records } logger.debug(f"Получена статистика рефералов по уровням: {referrals_by_level}") @@ -714,7 +761,9 @@ async def get_referral_stats(referrer_tg_id: int): """, referrer_tg_id, ) - logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}") + logger.debug( + f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}" + ) return { "total_referrals": total_referrals, @@ -724,7 +773,9 @@ async def get_referral_stats(referrer_tg_id: int): } except Exception as e: - logger.error(f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}") + logger.error( + f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}" + ) raise finally: if conn: @@ -746,7 +797,9 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}") + logger.info( + f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}" + ) await conn.execute( """ @@ -760,7 +813,9 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): logger.info(f"Успешно обновлено время истечения ключа для клиента {client_id}") except Exception as e: - logger.error(f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}") + logger.error( + f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}" + ) raise finally: if conn: @@ -781,7 +836,9 @@ async def delete_key(client_id: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для удаления ключа клиента {client_id}") + logger.info( + f"Установлено подключение к базе данных для удаления ключа клиента {client_id}" + ) await conn.execute( """ @@ -815,7 +872,9 @@ async def add_balance_to_client(client_id: str, amount: float): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}") + logger.info( + f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}" + ) await conn.execute( """ @@ -853,7 +912,9 @@ async def get_client_id_by_email(email: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для поиска client_id по email: {email}") + logger.info( + f"Установлено подключение к базе данных для поиска client_id по email: {email}" + ) client_id = await conn.fetchval( """ @@ -894,9 +955,13 @@ async def get_tg_id_by_client_id(client_id: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}") + logger.info( + f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}" + ) - result = await conn.fetchrow("SELECT tg_id FROM keys WHERE client_id = $1", client_id) + result = await conn.fetchrow( + "SELECT tg_id FROM keys WHERE client_id = $1", client_id + ) if result: logger.info(f"Найден Telegram ID для client_id: {client_id}") @@ -915,12 +980,12 @@ async def get_tg_id_by_client_id(client_id: str): async def upsert_user( - tg_id: int, - username: str = None, - first_name: str = None, - last_name: str = None, - language_code: str = None, - is_bot: bool = False, + tg_id: int, + username: str = None, + first_name: str = None, + last_name: str = None, + language_code: str = None, + is_bot: bool = False, ): """ Обновляет или вставляет информацию о пользователе в базу данных. @@ -939,7 +1004,9 @@ async def upsert_user( conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}") + logger.info( + f"Установлено подключение к базе данных для обновления пользователя {tg_id}" + ) await conn.execute( """ @@ -986,7 +1053,9 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}") + logger.info( + f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}" + ) await conn.execute( """ @@ -997,7 +1066,9 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): amount, payment_system, ) - logger.info(f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}") + logger.info( + f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}" + ) except Exception as e: logger.error(f"Ошибка при добавлении платежа для пользователя {tg_id}: {e}") raise @@ -1030,13 +1101,19 @@ async def add_notification(tg_id: int, notification_type: str, session: Any): tg_id, notification_type, ) - logger.info(f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}") + logger.info( + f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}" + ) except Exception as e: - logger.error(f"Ошибка при добавлении notification для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при добавлении notification для пользователя {tg_id}: {e}" + ) raise -async def check_notification_time(tg_id: int, notification_type: str, hours: int = 12, session: Any = None) -> bool: +async def check_notification_time( + tg_id: int, notification_type: str, hours: int = 12, session: Any = None +) -> bool: """ Проверяет, прошло ли указанное количество часов с момента последнего уведомления. @@ -1081,7 +1158,9 @@ async def check_notification_time(tg_id: int, notification_type: str, hours: int return can_notify except Exception as e: - logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}" + ) return False finally: @@ -1103,16 +1182,16 @@ async def get_servers_from_db(): servers = {} for row in result: - cluster_name = row['cluster_name'] + cluster_name = row["cluster_name"] 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'], + "server_name": row["server_name"], + "api_url": row["api_url"], + "subscription_url": row["subscription_url"], + "inbound_id": row["inbound_id"], } ) diff --git a/filters/admin.py b/filters/admin.py index b902294b..d940b31f 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -1,15 +1,13 @@ -from typing import Union from aiogram.filters import BaseFilter from aiogram.types import Message - from config import ADMIN_ID class IsAdminFilter(BaseFilter): async def __call__(self, message: Message) -> bool: try: - admin_ids: Union[int, list[int]] = ADMIN_ID + admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): return message.from_user.id in admin_ids return message.from_user.id == admin_ids diff --git a/handlers/__init__.py b/handlers/__init__.py index 613d08f9..f984092c 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -1,4 +1,4 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router @@ -13,7 +13,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 = Router(name="handlers_main_router") router.include_routers( start_router, @@ -22,7 +22,6 @@ router.include_routers( donate_router, coupons_router, notifications_router, - payments_router, keys_router, instructions_router, diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 8f83973d..39109027 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -1,4 +1,4 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router @@ -7,7 +7,7 @@ from .admin_panel import router as panel_router from .admin_servers import router as servers_router from .admin_user_editor import router as user_editor_router -router = Router(name='admins_main_router') +router = Router(name="admins_main_router") router.include_routers( panel_router, diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 5ea3ca98..27e3071e 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -19,13 +19,19 @@ router = Router() @router.callback_query(F.data == "coupons_editor", IsAdminFilter()) -async def show_coupon_management_menu(callback_query: types.CallbackQuery, state: FSMContext): +async def show_coupon_management_menu( + callback_query: types.CallbackQuery, state: FSMContext +): await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon")) + builder.row( + InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon") + ) builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🛠 Меню управления купонами:", reply_markup=builder.as_markup() + ) @router.callback_query(F.data == "coupons", IsAdminFilter()) @@ -35,7 +41,9 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): if not coupons: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) await callback_query.message.answer( "❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙", @@ -61,8 +69,12 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): ) ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - await callback_query.message.answer(coupon_list, reply_markup=builder.as_markup()) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) + await callback_query.message.answer( + coupon_list, reply_markup=builder.as_markup() + ) except Exception as e: logger.error(f"Ошибка при получении списка купонов: {e}") @@ -141,7 +153,9 @@ async def process_coupon_data(message: types.Message, state: FSMContext, session ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) await message.answer(result_message, reply_markup=builder.as_markup()) await state.clear() diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index b0adf30a..47865ad8 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -1,6 +1,6 @@ +import subprocess from datetime import datetime from io import BytesIO -import subprocess from typing import Any from aiogram import F, Router, types @@ -35,13 +35,35 @@ async def handle_admin_message(message: types.Message, state: FSMContext): await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="user_stats")) - builder.row(InlineKeyboardButton(text="👥 Управление пользователями", callback_data="user_editor")) - builder.row(InlineKeyboardButton(text="🖥️ Управление серверами", callback_data="servers_editor")) - builder.row(InlineKeyboardButton(text="🎟️ Управление купонами", callback_data="coupons_editor")) - builder.row(InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")) - builder.row(InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")) - builder.row(InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")) + builder.row( + InlineKeyboardButton( + text="📊 Статистика пользователей", callback_data="user_stats" + ) + ) + builder.row( + InlineKeyboardButton( + text="👥 Управление пользователями", callback_data="user_editor" + ) + ) + builder.row( + InlineKeyboardButton( + text="🖥️ Управление серверами", callback_data="servers_editor" + ) + ) + builder.row( + InlineKeyboardButton( + text="🎟️ Управление купонами", callback_data="coupons_editor" + ) + ) + builder.row( + InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls") + ) + builder.row( + InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups") + ) + builder.row( + InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot") + ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer("🤖 Панель администратора", reply_markup=builder.as_markup()) @@ -59,7 +81,9 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): total_payments_week = await session.fetchval( "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)" ) - total_payments_all_time = await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments") + total_payments_all_time = await session.fetchval( + "SELECT COALESCE(SUM(amount), 0) FROM payments" + ) active_keys = await session.fetchval( "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", @@ -83,12 +107,27 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats")) - builder.row(InlineKeyboardButton(text="📥 Выгрузить пользователей в CSV", callback_data="export_users_csv")) - builder.row(InlineKeyboardButton(text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv")) - builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) + builder.row( + InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats") + ) + builder.row( + InlineKeyboardButton( + text="📥 Выгрузить пользователей в CSV", + callback_data="export_users_csv", + ) + ) + builder.row( + InlineKeyboardButton( + text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv" + ) + ) + builder.row( + InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin") + ) - await callback_query.message.answer(stats_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + stats_message, reply_markup=builder.as_markup() + ) except Exception as e: logger.error(f"Error in user_stats_menu: {e}") @@ -115,7 +154,9 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): ) if not users: - await callback_query.message.answer("📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup() + ) return csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" # Заголовки CSV @@ -128,14 +169,17 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv") await callback_query.message.answer_document( - file, caption="📥 Экспорт пользователей в CSV", reply_markup=builder.as_markup() + file, + caption="📥 Экспорт пользователей в CSV", + reply_markup=builder.as_markup(), ) file_name.close() except Exception as e: logger.error(f"Ошибка при экспорте пользователей в CSV: {e}") await callback_query.message.answer( - "❗ Произошла ошибка при экспорте пользователей.", reply_markup=builder.as_markup() + "❗ Произошла ошибка при экспорте пользователей.", + reply_markup=builder.as_markup(), ) @@ -161,7 +205,9 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): ) if not payments: - await callback_query.message.answer("📭 Нет платежей для экспорта.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "📭 Нет платежей для экспорта.", reply_markup=builder.as_markup() + ) return csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV @@ -181,7 +227,8 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): except Exception as e: logger.error(f"Ошибка при экспорте платежей в CSV: {e}") await callback_query.message.answer( - "❗ Произошла ошибка при экспорте платежей.", reply_markup=builder.as_markup() + "❗ Произошла ошибка при экспорте платежей.", + reply_markup=builder.as_markup(), ) @@ -197,7 +244,9 @@ async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): @router.message(UserEditorState.waiting_for_message, IsAdminFilter()) -async def process_message_to_all(message: types.Message, state: FSMContext, session: Any): +async def process_message_to_all( + message: types.Message, state: FSMContext, session: Any +): text_message = message.text try: @@ -214,7 +263,9 @@ async def process_message_to_all(message: types.Message, state: FSMContext, sess success_count += 1 except Exception as e: error_count += 1 - logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}") + logger.error( + f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}" + ) await message.answer( f"📤 Рассылка завершена:\n" @@ -230,9 +281,13 @@ async def process_message_to_all(message: types.Message, state: FSMContext, sess @router.callback_query(F.data == "backups", IsAdminFilter()) async def handle_backup(callback_query: CallbackQuery, state: FSMContext): - await callback_query.message.answer("💾 Инициализация резервного копирования базы данных...") + await callback_query.message.answer( + "💾 Инициализация резервного копирования базы данных..." + ) await backup_database() - await callback_query.message.answer("✅ Резервная копия успешно создана и отправлена администратору.") + await callback_query.message.answer( + "✅ Резервная копия успешно создана и отправлена администратору." + ) @router.callback_query(F.data == "restart_bot", IsAdminFilter()) @@ -240,7 +295,9 @@ async def handle_restart(callback_query: CallbackQuery, state: FSMContext): await state.set_state(UserEditorState.waiting_for_restart_confirmation) builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton(text="✅ Да, перезапустить", callback_data="confirm_restart"), + InlineKeyboardButton( + text="✅ Да, перезапустить", callback_data="confirm_restart" + ), InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"), ) builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) @@ -266,12 +323,17 @@ async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext): text=True, ) await state.clear() - await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup() + ) except subprocess.CalledProcessError: - await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup() + ) except Exception as e: await callback_query.message.answer( - f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", reply_markup=builder.as_markup() + f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", + reply_markup=builder.as_markup(), ) @@ -284,7 +346,17 @@ async def user_editor_menu(callback_query: CallbackQuery): callback_data="search_by_key_name", ) ) - builder.row(InlineKeyboardButton(text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id")) - builder.row(InlineKeyboardButton(text="🌐 Поиск по Username", callback_data="search_by_username")) + builder.row( + InlineKeyboardButton( + text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id" + ) + ) + builder.row( + InlineKeyboardButton( + text="🌐 Поиск по Username", callback_data="search_by_username" + ) + ) builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin")) - await callback_query.message.answer("👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup() + ) diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index b9260c66..8a030119 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -1,13 +1,13 @@ +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 InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg +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 @@ -29,9 +29,15 @@ async def handle_servers_editor(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() for cluster_name, cluster_servers in servers.items(): - builder.row(InlineKeyboardButton(text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}")) + builder.row( + InlineKeyboardButton( + text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}" + ) + ) - builder.row(InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster")) + builder.row( + InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster") + ) builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin")) await callback_query.message.answer( @@ -64,7 +70,11 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): if cluster_name == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔧 Управление кластерами", callback_data="servers_editor" + ) + ) await message.answer( "Процесс создания кластера отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -78,7 +88,9 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): await state.update_data(cluster_name=cluster_name) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await message.answer( f"Введите имя сервера для кластера {cluster_name}:\n\n" @@ -97,7 +109,11 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): if server_name == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔧 Управление кластерами", callback_data="servers_editor" + ) + ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -110,15 +126,19 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): server_unique = await check_unique_server_name(server_name) if not server_unique: - await message.answer("❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.") + await message.answer( + "❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя." + ) return user_data = await state.get_data() - cluster_name = user_data.get('cluster_name') + cluster_name = user_data.get("cluster_name") await state.update_data(server_name=server_name) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await message.answer( f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -138,7 +158,11 @@ async def handle_api_url_input(message: types.Message, state: FSMContext): if api_url == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔧 Управление кластерами", callback_data="servers_editor" + ) + ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -152,15 +176,17 @@ async def handle_api_url_input(message: types.Message, state: FSMContext): ) return - api_url = api_url.rstrip('/') + api_url = api_url.rstrip("/") user_data = await state.get_data() - cluster_name = user_data.get('cluster_name') - server_name = user_data.get('server_name') + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") await state.update_data(api_url=api_url) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await message.answer( f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -181,7 +207,11 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex if subscription_url == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔧 Управление кластерами", callback_data="servers_editor" + ) + ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -195,15 +225,17 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex ) return - subscription_url = subscription_url.rstrip('/') + subscription_url = subscription_url.rstrip("/") user_data = await state.get_data() - cluster_name = user_data.get('cluster_name') - server_name = user_data.get('server_name') + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") await state.update_data(subscription_url=subscription_url) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await message.answer( f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -219,14 +251,16 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): inbound_id = message.text.strip() if not inbound_id.isdigit(): - await message.answer("❌ inbound_id должен быть числовым значением. Попробуйте снова.") + await message.answer( + "❌ inbound_id должен быть числовым значением. Попробуйте снова." + ) return user_data = await state.get_data() - cluster_name = user_data.get('cluster_name') - server_name = user_data.get('server_name') - api_url = user_data.get('api_url') - subscription_url = user_data.get('subscription_url') + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") + api_url = user_data.get("api_url") + subscription_url = user_data.get("subscription_url") conn = await asyncpg.connect(DATABASE_URL) await conn.execute( @@ -243,10 +277,15 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): await conn.close() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад к кластерам", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔙 Назад к кластерам", callback_data="servers_editor" + ) + ) await message.answer( - f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", reply_markup=builder.as_markup() + f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", + reply_markup=builder.as_markup(), ) await state.clear() @@ -264,22 +303,40 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC for server in cluster_servers: builder.row( InlineKeyboardButton( - text=f"🌍 {server['server_name']}", callback_data=f"manage_server|{server['server_name']}" + text=f"🌍 {server['server_name']}", + callback_data=f"manage_server|{server['server_name']}", ) ) - builder.row(InlineKeyboardButton(text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}")) - builder.row( - InlineKeyboardButton(text="🌐 Доступность серверов", callback_data=f"server_availability|{cluster_name}") + InlineKeyboardButton( + text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}" + ) ) - builder.row(InlineKeyboardButton(text="💾 Создать бэкап кластера", callback_data=f"backup_cluster|{cluster_name}")) + builder.row( + InlineKeyboardButton( + text="🌐 Доступность серверов", + callback_data=f"server_availability|{cluster_name}", + ) + ) - builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="💾 Создать бэкап кластера", + callback_data=f"backup_cluster|{cluster_name}", + ) + ) + + builder.row( + InlineKeyboardButton( + text="🔙 Назад в управление кластерами", callback_data="servers_editor" + ) + ) await callback_query.message.answer( - f"🔧 Управление серверами для кластера {cluster_name}", reply_markup=builder.as_markup() + f"🔧 Управление серверами для кластера {cluster_name}", + reply_markup=builder.as_markup(), ) @@ -299,24 +356,36 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery): "Это может занять до 1 минуты, пожалуйста, подождите..." ) - availability_message = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" + availability_message = ( + f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" + ) for server in cluster_servers: - xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) + xui = AsyncApi( + server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD + ) try: await xui.login() online_users = len(await xui.client.online()) - availability_message += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" + availability_message += ( + f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" + ) except Exception as e: availability_message += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" + ) + ) - await in_progress_message.edit_text(availability_message, reply_markup=builder.as_markup()) + await in_progress_message.edit_text( + availability_message, reply_markup=builder.as_markup() + ) await callback_query.answer() @@ -330,19 +399,29 @@ async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMCo server = None cluster_name = None for cluster, cluster_servers in servers.items(): - server = next((s for s in cluster_servers if s['server_name'] == server_name), None) + server = next( + (s for s in cluster_servers if s["server_name"] == server_name), None + ) if server: cluster_name = cluster break if server: - api_url = server['api_url'] - subscription_url = server['subscription_url'] - inbound_id = server['inbound_id'] + api_url = server["api_url"] + subscription_url = server["subscription_url"] + inbound_id = server["inbound_id"] builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"delete_server|{server_name}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) + builder.row( + InlineKeyboardButton( + text="🗑️ Удалить", callback_data=f"delete_server|{server_name}" + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" + ) + ) await callback_query.message.answer( f"🔧 Информация о сервере {server_name}:\n\n" @@ -362,17 +441,24 @@ async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMCo builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_server|{server_name}"), - InlineKeyboardButton(text="❌ Нет", callback_data=f"manage_server|{server_name}"), + InlineKeyboardButton( + text="✅ Да", callback_data=f"confirm_delete_server|{server_name}" + ), + InlineKeyboardButton( + text="❌ Нет", callback_data=f"manage_server|{server_name}" + ), ) await callback_query.message.answer( - f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", reply_markup=builder.as_markup() + f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", + reply_markup=builder.as_markup(), ) @router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) -async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext): +async def handle_confirm_delete_server( + callback_query: types.CallbackQuery, state: FSMContext +): server_name = callback_query.data.split("|")[1] conn = await asyncpg.connect(DATABASE_URL) @@ -385,9 +471,15 @@ async def handle_confirm_delete_server(callback_query: types.CallbackQuery, stat await conn.close() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔙 Назад в управление кластерами", callback_data="servers_editor" + ) + ) - await callback_query.message.answer(f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup() + ) @router.callback_query(F.data.startswith("add_server|"), IsAdminFilter()) @@ -397,7 +489,9 @@ async def handle_add_server(callback_query: types.CallbackQuery, state: FSMConte await state.update_data(cluster_name=cluster_name) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await callback_query.message.answer( f"Введите имя сервера для кластера {cluster_name}:\n\n" diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index d579cb5f..8a89d875 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -7,11 +7,20 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import TOTAL_GB -from database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry + +from database import ( + 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, delete_key_from_db, renew_key_in_cluster +from handlers.keys.key_utils import ( + delete_key_from_cluster, + delete_key_from_db, + renew_key_in_cluster, +) from handlers.utils import sanitize_key_name from logger import logger @@ -31,7 +40,9 @@ class UserEditorState(StatesGroup): async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup() + ) await state.set_state(UserEditorState.waiting_for_tg_id) @@ -39,39 +50,59 @@ async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): async def prompt_username(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Введите Username клиента:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔍 Введите Username клиента:", reply_markup=builder.as_markup() + ) await state.set_state(UserEditorState.waiting_for_username) @router.message(UserEditorState.waiting_for_username, IsAdminFilter()) -async def handle_username_input(message: types.Message, state: FSMContext, session: Any): +async def handle_username_input( + message: types.Message, state: FSMContext, session: Any +): username = message.text.strip().lstrip("@") - user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username) + user_record = await session.fetchrow( + "SELECT tg_id FROM users WHERE username = $1", username + ) if not user_record: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("🔍 Пользователь с указанным username не найден. 🚫", reply_markup=builder.as_markup()) + await message.answer( + "🔍 Пользователь с указанным username не найден. 🚫", + reply_markup=builder.as_markup(), + ) await state.clear() return tg_id = user_record["tg_id"] - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + username = await session.fetchval( + "SELECT username FROM users WHERE tg_id = $1", tg_id + ) + balance = await session.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + referral_count = await session.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) if balance is None: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("🚫 Пользователь с указанным tg_id не найден. 🔍", reply_markup=builder.as_markup()) + await message.answer( + "🚫 Пользователь с указанным tg_id не найден. 🔍", + reply_markup=builder.as_markup(), + ) await state.clear() return builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) + builder.row( + InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") + ) builder.row( InlineKeyboardButton( @@ -104,22 +135,33 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi @router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter()) async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any): tg_id = int(message.text) - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + username = await session.fetchval( + "SELECT username FROM users WHERE tg_id = $1", tg_id + ) + balance = await session.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + referral_count = await session.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) if balance is None: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("❌ Пользователь с указанным tg_id не найден. 🔍", reply_markup=builder.as_markup()) + await message.answer( + "❌ Пользователь с указанным tg_id не найден. 🔍", + reply_markup=builder.as_markup(), + ) await state.clear() return builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) + builder.row( + InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") + ) builder.row( InlineKeyboardButton( @@ -156,9 +198,15 @@ async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any await restore_trial(tg_id, session) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin")) + builder.row( + InlineKeyboardButton( + text="🔙 Назад в меню администратора", callback_data="admin" + ) + ) - await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "✅ Триал успешно восстановлен.", reply_markup=builder.as_markup() + ) @router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter()) @@ -167,17 +215,22 @@ async def process_balance_change(callback_query: CallbackQuery, state: FSMContex await state.update_data(tg_id=tg_id) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("💸 Введите новую сумму баланса:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "💸 Введите новую сумму баланса:", reply_markup=builder.as_markup() + ) await state.set_state(UserEditorState.waiting_for_new_balance) @router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter()) -async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any): +async def handle_new_balance_input( + message: types.Message, state: FSMContext, session: Any +): if not message.text.isdigit() or int(message.text) < 0: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await message.answer( - "❌ Пожалуйста, введите корректную сумму для изменения баланса.", reply_markup=builder.as_markup() + "❌ Пожалуйста, введите корректную сумму для изменения баланса.", + reply_markup=builder.as_markup(), ) return @@ -222,11 +275,13 @@ async def get_key_details(email, session): cluster_name = "Неизвестный кластер" for cluster_name, cluster_servers in servers.items(): - if any(server['inbound_id'] == record['server_id'] for server in cluster_servers): + if any( + server["inbound_id"] == record["server_id"] for server in cluster_servers + ): cluster_name = cluster_name break - expiry_date = datetime.utcfromtimestamp(record['expiry_time'] / 1000) + expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000) current_date = datetime.utcnow() time_left = expiry_date - current_date @@ -239,12 +294,12 @@ async def get_key_details(email, session): days_left_message = f"Осталось часов: {hours_left}" return { - 'key': record['key'], - 'expiry_date': expiry_date.strftime("%d %B %Y года"), - 'days_left_message': days_left_message, - 'server_name': cluster_name, - 'balance': record['balance'], - 'tg_id': record['tg_id'], + "key": record["key"], + "expiry_date": expiry_date.strftime("%d %B %Y года"), + "days_left_message": days_left_message, + "server_name": cluster_name, + "balance": record["balance"], + "tg_id": record["tg_id"], } @@ -257,7 +312,8 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await callback_query.message.answer( - "🔍 Информация о ключе не найдена. 🚫", reply_markup=builder.as_markup() + "🔍 Информация о ключе не найдена. 🚫", + reply_markup=builder.as_markup(), ) return @@ -289,19 +345,25 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): ) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + response_message, reply_markup=builder.as_markup() + ) @router.callback_query(F.data == "search_by_key_name", IsAdminFilter()) async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("🔑 Введите имя ключа:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔑 Введите имя ключа:", reply_markup=builder.as_markup() + ) await state.set_state(UserEditorState.waiting_for_key_name) @router.message(UserEditorState.waiting_for_key_name, IsAdminFilter()) -async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any): +async def handle_key_name_input( + message: types.Message, state: FSMContext, session: Any +): key_name = sanitize_key_name(message.text) key_details = await get_key_details(key_name, session) @@ -358,34 +420,52 @@ async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext) @router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter()) -async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any): +async def handle_expiry_time_input( + message: types.Message, state: FSMContext, session: Any +): user_data = await state.get_data() email = user_data.get("email") if not email: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup()) + await message.answer( + "📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup() + ) await state.clear() return try: expiry_time_str = message.text - expiry_time = int(datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000) + expiry_time = int( + datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 + ) client_id = await get_client_id_by_email(email) if client_id is None: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer(f"🚫 Клиент с email {email} не найден. 🔍", reply_markup=builder.as_markup()) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") + ) + await message.answer( + f"🚫 Клиент с email {email} не найден. 🔍", + reply_markup=builder.as_markup(), + ) await state.clear() return - record = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id) + record = await session.fetchrow( + "SELECT server_id FROM keys WHERE client_id = $1", client_id + ) if not record: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("🚫 Клиент не найден в базе данных. 🔍", reply_markup=builder.as_markup()) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") + ) + await message.answer( + "🚫 Клиент не найден в базе данных. 🔍", + reply_markup=builder.as_markup(), + ) await state.clear() return @@ -412,9 +492,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se await update_key_expiry(client_id, expiry_time) - response_message = ( - f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." - ) + response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) @@ -423,7 +501,8 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await message.answer( - "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", reply_markup=builder.as_markup() + "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", + reply_markup=builder.as_markup(), ) except Exception as e: logger.error(e) @@ -431,14 +510,20 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se @router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter()) -async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any): +async def process_callback_delete_key( + callback_query: types.CallbackQuery, session: Any +): email = callback_query.data.split("|")[1] - client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email) + client_id = await session.fetchval( + "SELECT client_id FROM keys WHERE email = $1", email + ) if client_id is None: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup() + ) return builder = InlineKeyboardBuilder() @@ -448,7 +533,9 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery, sessi callback_data=f"confirm_delete_admin|{client_id}", ) ) - builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor")) + builder.row( + types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor") + ) await callback_query.message.answer( "❓ Вы уверены, что хотите удалить ключ?", reply_markup=builder.as_markup(), @@ -456,9 +543,13 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery, sessi @router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter()) -async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): +async def process_callback_confirm_delete( + callback_query: types.CallbackQuery, session: Any +): client_id = callback_query.data.split("|")[1] - record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id) + record = await session.fetchrow( + "SELECT email FROM keys WHERE client_id = $1", client_id + ) if record: email = record["email"] @@ -472,32 +563,48 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s tasks = [] for cluster_name, cluster_servers in clusters.items(): for server in cluster_servers: - tasks.append(delete_key_from_cluster(cluster_name, email, client_id)) + tasks.append( + delete_key_from_cluster(cluster_name, email, client_id) + ) await asyncio.gather(*tasks) await delete_key_from_servers(email, client_id) await delete_key_from_db(client_id, session) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + response_message, reply_markup=builder.as_markup() + ) else: response_message = "🚫 Ключ не найден или уже удален." builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + response_message, reply_markup=builder.as_markup() + ) @router.callback_query(F.data.startswith("user_info|"), IsAdminFilter()) -async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def handle_user_info( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = int(callback_query.data.split("|")[1]) - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + username = await session.fetchval( + "SELECT username FROM users WHERE tg_id = $1", tg_id + ) + balance = await session.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + referral_count = await session.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) + builder.row( + InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") + ) builder.row( InlineKeyboardButton( diff --git a/handlers/coupons.py b/handlers/coupons.py index ed487671..15b3aa55 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -18,7 +18,9 @@ router = Router() @router.callback_query(F.data == "activate_coupon") -async def handle_activate_coupon(callback_query: types.CallbackQuery, state: FSMContext): +async def handle_activate_coupon( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) @@ -38,7 +40,9 @@ async def process_coupon_code(message: types.Message, state: FSMContext, session builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await message.answer(activation_result, reply_markup=builder.as_markup(), parse_mode="HTML") + await message.answer( + activation_result, reply_markup=builder.as_markup(), parse_mode="HTML" + ) await state.clear() diff --git a/handlers/donate.py b/handlers/donate.py index d441463a..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 @@ -22,7 +22,11 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext) await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) + builder.row( + InlineKeyboardButton( + text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot" + ) + ) builder.row( InlineKeyboardButton( text="💰 Ввести сумму доната", @@ -40,10 +44,14 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext) @router.callback_query(F.data == "enter_custom_donate_amount") -async def process_enter_donate_amount(callback_query: types.CallbackQuery, state: FSMContext): +async def process_enter_donate_amount( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate")) - await callback_query.message.answer(f"💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup() + ) await state.set_state(DonateState.entering_donate_amount) @@ -52,7 +60,9 @@ async def process_donate_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount // RUB_TO_XTR <= 0: - await message.answer(f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:") + await message.answer( + f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:" + ) return await state.update_data(amount=amount) @@ -89,7 +99,9 @@ async def on_successful_donate(message: types.Message, state: FSMContext): try: amount = float(message.successful_payment.invoice_payload.split("_")[0]) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) await message.answer( text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖", reply_markup=builder.as_markup(), diff --git a/handlers/instructions/__init__.py b/handlers/instructions/__init__.py index b8121003..06350a7c 100644 --- a/handlers/instructions/__init__.py +++ b/handlers/instructions/__init__.py @@ -1,10 +1,10 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router from .instructions import router as instructions_router -router = Router(name='instructions_main_router') +router = Router(name="instructions_main_router") router.include_routers( instructions_router, diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 44d954c4..b8745d92 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -4,8 +4,8 @@ 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_IOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL + from handlers.texts import INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE router = Router() @@ -49,7 +49,9 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): ) if not record: - await callback_query.message.answer("❌ Ключ не найден. Проверьте имя ключа. 🔍") + await callback_query.message.answer( + "❌ Ключ не найден. Проверьте имя ключа. 🔍" + ) return key = record["key"] @@ -57,8 +59,14 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): instruction_message = f"{key_message}{INSTRUCTION_PC}" builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}")) - builder.row(InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_IOS}{key}")) + builder.row( + InlineKeyboardButton( + text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}" + ) + ) + builder.row( + InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_IOS}{key}") + ) builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) diff --git a/handlers/keys/__init__.py b/handlers/keys/__init__.py index 45256dad..064024e6 100644 --- a/handlers/keys/__init__.py +++ b/handlers/keys/__init__.py @@ -1,11 +1,11 @@ -__all__ = ('router',) +__all__ = ("router",) 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 = Router(name="keys_main_router") router.include_routers( keys_router, diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 9efe8ec1..1ef37a36 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -1,28 +1,27 @@ import asyncio -from datetime import datetime, timedelta -from typing import Any, Union import uuid +from datetime import datetime, timedelta +from typing import Any from aiogram import F, Router 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 config import ( CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, - RENEWAL_PLANS, RENEWAL_PRICES, SUPPORT_CHAT_URL, TRIAL_TIME, ) + from database import get_balance, get_trial, store_key, update_balance from handlers.keys.key_utils import create_key_on_cluster -from handlers.texts import DISCOUNTS, KEY, NULL_BALANCE, key_message_success +from handlers.texts import DISCOUNTS, KEY, key_message_success from handlers.utils import generate_random_email, get_least_loaded_cluster from logger import logger @@ -37,7 +36,9 @@ class Form(StatesGroup): @router.callback_query(F.data == "create_key") -async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext, session: Any): +async def process_callback_create_key( + callback_query: CallbackQuery, state: FSMContext, session: Any +): server_id = "все сервера" await state.update_data(selected_server_id=server_id) await select_server(callback_query, state, session) @@ -48,9 +49,14 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext, sessio if trial_status == 1: builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton(text="✅ Да, подключить новое устройство", callback_data="confirm_create_new_key") + InlineKeyboardButton( + text="✅ Да, подключить новое устройство", + callback_data="confirm_create_new_key", + ) + ) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") ) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( text=KEY, @@ -58,22 +64,31 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext, sessio ) await state.update_data(creating_new_key=True) else: - await handle_key_creation(callback_query.message.chat.id, state, session, callback_query) + await handle_key_creation( + callback_query.message.chat.id, state, session, callback_query + ) @router.callback_query(F.data == "confirm_create_new_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: Union[Message, CallbackQuery] + tg_id: int, + state: FSMContext, + session: Any, + message_or_query: Message | CallbackQuery, ): """Создание ключа с учётом выбора тарифного плана.""" current_time = datetime.utcnow() @@ -83,7 +98,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() @@ -100,21 +117,27 @@ 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) @router.callback_query(F.data.startswith("select_plan_")) -async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, session: Any): +async def select_tariff_plan( + callback_query: CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id plan_id = callback_query.data.split("_")[-1] plan_price = RENEWAL_PRICES.get(plan_id) @@ -128,8 +151,12 @@ async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, s balance = await get_balance(tg_id) if balance < plan_price: 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( "💳 Недостаточно средств для создания подписки. Пополните баланс в личном кабинете.", reply_markup=builder.as_markup(), @@ -145,7 +172,11 @@ async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, s async def create_key( - tg_id: int, expiry_time: datetime, state: FSMContext, session: Any, message_or_query: Union[Message, CallbackQuery] + tg_id: int, + expiry_time: datetime, + state: FSMContext, + session: Any, + message_or_query: Message | CallbackQuery, ): """Создаёт ключ с заданным сроком действия.""" while True: @@ -159,7 +190,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() @@ -184,11 +217,21 @@ 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}") - await message_or_query.message.answer("❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова.") + await message_or_query.message.answer( + "❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова." + ) return builder = InlineKeyboardBuilder() @@ -198,10 +241,18 @@ async def create_key( InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), ) builder.row( - InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{public_link}"), - InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{public_link}"), + InlineKeyboardButton( + text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{public_link}" + ), + InlineKeyboardButton( + text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{public_link}" + ), + ) + builder.row( + InlineKeyboardButton( + text="💻 Windows/Linux", callback_data=f"connect_pc|{email}" + ) ) - builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{email}")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) remaining_time = expiry_time - datetime.utcnow() diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 8d39c8b9..a66e1bec 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,10 +1,10 @@ import asyncio import asyncpg +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB from py3xui import AsyncApi from client import add_client, delete_client, extend_client_key -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB from database import get_servers_from_db from logger import logger @@ -33,7 +33,9 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time continue conn = await asyncpg.connect(DATABASE_URL) - existing_key = await conn.fetchrow("SELECT 1 FROM keys WHERE email = $1", email) + existing_key = await conn.fetchrow( + "SELECT 1 FROM keys WHERE email = $1", email + ) if existing_key: raise ValueError(f"Email {email} уже существует в базе данных.") @@ -98,7 +100,9 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to await asyncio.gather(*tasks) except Exception as e: - logger.error(f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}") + logger.error( + f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}" + ) raise e @@ -145,7 +149,9 @@ async def delete_key_from_cluster(cluster_id, email, client_id): await asyncio.gather(*tasks) except Exception as e: - logger.error(f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}") + logger.error( + f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}" + ) raise e @@ -189,8 +195,12 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id await asyncio.gather(*tasks) - logger.info(f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}") + logger.info( + f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}" + ) except Exception as e: - logger.error(f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}") + logger.error( + f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}" + ) raise e diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 236545b4..48d5e7d8 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -1,15 +1,30 @@ import asyncio -from datetime import datetime, timedelta import locale import os +from datetime import datetime, timedelta 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, + DOWNLOAD_ANDROID, + DOWNLOAD_IOS, + PUBLIC_LINK, + RENEWAL_PLANS, + TOTAL_GB, +) -from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, RENEWAL_PLANS, TOTAL_GB -from database import delete_key, get_balance, get_servers_from_db, store_key, update_balance, update_key_expiry +from database import ( + delete_key, + get_balance, + get_servers_from_db, + store_key, + update_balance, + update_key_expiry, +) from handlers.keys.key_utils import ( delete_key_from_cluster, delete_key_from_db, @@ -48,20 +63,29 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio builder = InlineKeyboardBuilder() for record in records: key_name = record["email"] - builder.row(InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}")) + builder.row( + InlineKeyboardButton( + text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}" + ) + ) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) inline_keyboard = builder.as_markup() response_message = ( - "🔑 Список ваших устройств\n\n" "👇 Выберите устройство для управления подпиской:" + "🔑 Список ваших устройств\n\n" + "👇 Выберите устройство для управления подпиской:" ) image_path = os.path.join("img", "pic_keys.jpg") if os.path.isfile(image_path): with open(image_path, "rb") as image_file: await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), + photo=BufferedInputFile( + image_file.read(), filename="pic_keys.jpg" + ), caption=response_message, reply_markup=inline_keyboard, ) @@ -74,8 +98,14 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio else: response_message = NO_KEYS builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_key")) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton( + text="➕ Создать подписку", callback_data="create_key" + ) + ) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) keyboard = builder.as_markup() @@ -84,7 +114,9 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio if os.path.isfile(image_path): with open(image_path, "rb") as image_file: await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), + photo=BufferedInputFile( + image_file.read(), filename="pic_keys.jpg" + ), caption=response_message, reply_markup=keyboard, ) @@ -121,7 +153,9 @@ 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" + days_left_message = ( + "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0" + ) elif time_left.days > 0: days_left_message = f"Осталось дней: {time_left.days}" else: @@ -129,33 +163,54 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session days_left_message = f"Осталось часов: {hours_left}" formatted_expiry_date = expiry_date.strftime("%d %B %Y года") - response_message = key_message(key, formatted_expiry_date, days_left_message, server_name) + response_message = key_message( + key, formatted_expiry_date, days_left_message, server_name + ) builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS), - InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), + InlineKeyboardButton( + text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID + ), ) builder.row( - InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{key}"), - InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{key}"), + InlineKeyboardButton( + text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{key}" + ), + InlineKeyboardButton( + text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{key}" + ), ) - builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{key_name}")) + builder.row( + InlineKeyboardButton( + text="💻 Windows/Linux", callback_data=f"connect_pc|{key_name}" + ) + ) builder.row( - InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"), - InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}"), + InlineKeyboardButton( + text="⏳ Продлить", callback_data=f"renew_key|{key_name}" + ), + InlineKeyboardButton( + text="❌ Удалить", callback_data=f"delete_key|{key_name}" + ), ) if not key.startswith(PUBLIC_LINK): builder.row( - InlineKeyboardButton(text="🔄 Обновить подписку", callback_data=f"update_subscription|{key_name}") + InlineKeyboardButton( + text="🔄 Обновить подписку", + callback_data=f"update_subscription|{key_name}", + ) ) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) keyboard = builder.as_markup() @@ -184,7 +239,9 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session @router.callback_query(F.data.startswith("update_subscription|")) -async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any): +async def process_callback_update_subscription( + callback_query: types.CallbackQuery, session: Any +): tg_id = callback_query.message.chat.id email = callback_query.data.split("|")[1] try: @@ -244,7 +301,9 @@ async def process_callback_update_subscription(callback_query: types.CallbackQue else: await callback_query.message.answer("Ключ не найден в базе данных.") except Exception as e: - await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}") + await handle_error( + tg_id, callback_query, f"Ошибка при обновлении подписки: {e}" + ) @router.callback_query(F.data.startswith("delete_key|")) @@ -259,7 +318,11 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery): callback_data=f"confirm_delete|{client_id}", ) ], - [types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys")], + [ + types.InlineKeyboardButton( + text="❌ Нет, отменить", callback_data="view_keys" + ) + ], ] ) @@ -319,14 +382,18 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio callback_data=f"renew_plan|12|{client_id}", ) ) - back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys") + back_button = InlineKeyboardButton( + text="🔙 Назад", callback_data="view_keys" + ) builder.row(back_button) balance = await get_balance(tg_id) response_message = PLAN_SELECTION_MSG.format( balance=balance, - expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"), + expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime( + "%Y-%m-%d %H:%M:%S" + ), ) await callback_query.message.answer( @@ -340,15 +407,21 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio @router.callback_query(F.data.startswith("confirm_delete|")) -async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): +async def process_callback_confirm_delete( + callback_query: types.CallbackQuery, session: Any +): email = callback_query.data.split("|")[1] try: - record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) + record = await session.fetchrow( + "SELECT client_id FROM keys WHERE email = $1", email + ) if record: client_id = record["client_id"] response_message = "Ключ успешно удален." - back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") + back_button = types.InlineKeyboardButton( + text="Назад", callback_data="view_keys" + ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await delete_key(client_id) @@ -363,7 +436,9 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s try: tasks = [] for cluster_id, cluster in servers.items(): - tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) + tasks.append( + delete_key_from_cluster(cluster_id, email, client_id) + ) await asyncio.gather(*tasks) @@ -376,7 +451,9 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s else: response_message = "Ключ не найден или уже удален." - back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") + back_button = types.InlineKeyboardButton( + text="Назад", callback_data="view_keys" + ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await callback_query.message.answer( @@ -388,7 +465,9 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s @router.callback_query(F.data.startswith("renew_plan|")) -async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any): +async def process_callback_renew_plan( + callback_query: types.CallbackQuery, session: Any +): tg_id = callback_query.message.chat.id plan, client_id = ( callback_query.data.split("|")[1], @@ -411,17 +490,27 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi current_time = datetime.utcnow().timestamp() * 1000 if expiry_time <= current_time: - new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000) + new_expiry_time = int( + current_time + timedelta(days=days_to_extend).total_seconds() * 1000 + ) else: - new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000) + new_expiry_time = int( + expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000 + ) cost = RENEWAL_PLANS[plan]["price"] balance = await get_balance(tg_id) if balance < cost: 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( INSUFFICIENT_FUNDS_MSG, @@ -429,11 +518,17 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi ) return - response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]["months"]) + response_message = SUCCESS_RENEWAL_MSG.format( + months=RENEWAL_PLANS[plan]["months"] + ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + response_message, reply_markup=builder.as_markup() + ) servers = await get_servers_from_db() diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 45b0d3fc..0868d84f 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -2,10 +2,10 @@ import base64 from datetime import datetime import aiohttp -from aiohttp import web import asyncpg - +from aiohttp import web from config import DATABASE_URL, TRANSITION_DATE_STR + from database import get_servers_from_db from logger import logger @@ -20,7 +20,9 @@ async def fetch_url_content(url, tg_id): logger.info(f"Успешно получен контент с {url} для tg_id: {tg_id}") return base64.b64decode(content).decode("utf-8").split("\n") else: - logger.error(f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}") + logger.error( + f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}" + ) return [] except Exception as e: logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}") @@ -29,7 +31,9 @@ async def fetch_url_content(url, tg_id): async def combine_unique_lines(urls, tg_id, query_string): all_lines = [] - logger.info(f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}") + logger.info( + f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}" + ) urls_with_query = [f"{url}?{query_string}" for url in urls] logger.info(f"Составлены URL-адреса: {urls_with_query}") @@ -39,7 +43,9 @@ async def combine_unique_lines(urls, tg_id, query_string): all_lines.extend(lines) all_lines = list(set(filter(None, all_lines))) - logger.info(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}") + logger.info( + f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}" + ) return all_lines @@ -50,7 +56,9 @@ transition_timestamp_ms = int(transition_date.timestamp() * 1000) transition_timestamp_ms_adjusted = transition_timestamp_ms - (3 * 60 * 60 * 1000) -logger.info(f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}") +logger.info( + f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}" +) async def handle_old_subscription(request): @@ -67,7 +75,9 @@ async def handle_old_subscription(request): conn = await asyncpg.connect(DATABASE_URL) try: - key_info = await conn.fetchrow("SELECT created_at FROM keys WHERE email = $1", email) + key_info = await conn.fetchrow( + "SELECT created_at FROM keys WHERE email = $1", email + ) if not key_info: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -80,7 +90,9 @@ async def handle_old_subscription(request): logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}") created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000) - logger.info(f"Время создания клиента в формате datetime (UTC): {created_at_datetime}") + logger.info( + f"Время создания клиента в формате datetime (UTC): {created_at_datetime}" + ) if created_at_ms >= transition_timestamp_ms_adjusted: logger.info(f"Клиент с email {email} является новым.") @@ -99,7 +111,9 @@ async def handle_old_subscription(request): combined_subscriptions = await combine_unique_lines(urls, email, "") - base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8") + base64_encoded = base64.b64encode( + "\n".join(combined_subscriptions).encode("utf-8") + ).decode("utf-8") headers = { "Content-Type": "text/plain; charset=utf-8", @@ -130,7 +144,9 @@ async def handle_new_subscription(request): conn = await asyncpg.connect(DATABASE_URL) try: - client_data = await conn.fetchrow("SELECT tg_id FROM keys WHERE email = $1", email) + client_data = await conn.fetchrow( + "SELECT tg_id FROM keys WHERE email = $1", email + ) if not client_data: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -163,7 +179,9 @@ async def handle_new_subscription(request): combined_subscriptions = await combine_unique_lines(urls, tg_id, query_string) - base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8") + base64_encoded = base64.b64encode( + "\n".join(combined_subscriptions).encode("utf-8") + ).decode("utf-8") headers = { "Content-Type": "text/plain; charset=utf-8", diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 819d66da..fa0f9745 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -1,12 +1,12 @@ import asyncio +import uuid from datetime import datetime, timedelta from typing import Any -import uuid +from config import ADMIN_PASSWORD, ADMIN_USERNAME, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME from py3xui import AsyncApi from client import add_client -from config import ADMIN_PASSWORD, ADMIN_USERNAME, 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 diff --git a/handlers/notifications.py b/handlers/notifications.py index 7ac8ccf2..c16832f8 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -1,12 +1,20 @@ import asyncio from datetime import datetime, timedelta +import asyncpg from aiogram import Bot, Router, types from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg +from config import ( + ADMIN_PASSWORD, + ADMIN_USERNAME, + DATABASE_URL, + DEV_MODE, + RENEWAL_PLANS, + TOTAL_GB, + TRIAL_TIME, +) from py3xui import AsyncApi -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, DEV_MODE, RENEWAL_PLANS, TOTAL_GB, TRIAL_TIME from database import ( add_notification, check_notification_time, @@ -30,8 +38,12 @@ async def notify_expiring_keys(bot: Bot): logger.info("Подключение к базе данных успешно.") current_time = int(datetime.utcnow().timestamp() * 1000) - threshold_time_10h = int((datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000) - threshold_time_24h = int((datetime.utcnow() + timedelta(days=1)).timestamp() * 1000) + threshold_time_10h = int( + (datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000 + ) + threshold_time_24h = int( + (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000 + ) logger.info("Начало обработки уведомлений.") @@ -60,10 +72,14 @@ async def is_bot_blocked(bot: Bot, chat_id: int) -> bool: try: member = await bot.get_chat_member(chat_id, bot.id) blocked = member.status == "left" - logger.info(f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}") + logger.info( + f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}" + ) return blocked except Exception as e: - logger.warning(f"Не удалось проверить статус бота для пользователя {chat_id}: {e}") + logger.warning( + f"Не удалось проверить статус бота для пользователя {chat_id}: {e}" + ) return False @@ -110,7 +126,9 @@ async def notify_10h_keys( if not await is_bot_blocked(bot, tg_id) and not DEV_MODE: try: keyboard = InlineKeyboardBuilder() - keyboard.button(text="🔄 Продлить VPN", callback_data=f'renew_key|{email}') + keyboard.button( + text="🔄 Продлить VPN", callback_data=f"renew_key|{email}" + ) keyboard.button(text="💳 Пополнить баланс", callback_data="pay") keyboard.button(text="👤 Личный кабинет", callback_data="profile") keyboard.adjust(1) @@ -118,7 +136,9 @@ async def notify_10h_keys( await bot.send_message(tg_id, message, reply_markup=keyboard) logger.info(f"Уведомление отправлено пользователю {tg_id}.") except Exception as e: - logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления пользователю {tg_id}: {e}" + ) continue await conn.execute( @@ -177,7 +197,7 @@ async def notify_24h_keys( builder.row( types.InlineKeyboardButton( text="🔄 Продлить VPN", - callback_data=f'renew_key|{email}', + callback_data=f"renew_key|{email}", ) ) builder.row( @@ -196,14 +216,18 @@ async def notify_24h_keys( await bot.send_message(tg_id, message_24h, reply_markup=keyboard) logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.") except Exception as e: - logger.error(f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}" + ) continue await conn.execute( "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.sleep(1) @@ -225,18 +249,27 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): logger.info(f"Найдено {len(inactive_trial_users)} неактивных пользователей.") for user in inactive_trial_users: - tg_id = user['tg_id'] - username = user.get('username', 'Пользователь') + tg_id = user["tg_id"] + username = user.get("username", "Пользователь") try: - can_notify = await check_notification_time(tg_id, 'inactive_trial', hours=24, session=conn) + can_notify = await check_notification_time( + tg_id, "inactive_trial", hours=24, session=conn + ) if can_notify and not await is_bot_blocked(bot, tg_id): builder = InlineKeyboardBuilder() builder.row( - types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key") + types.InlineKeyboardButton( + text="🚀 Активировать пробный период", + callback_data="create_key", + ) + ) + builder.row( + types.InlineKeyboardButton( + text="👤 Личный кабинет", callback_data="profile" + ) ) - builder.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) keyboard = builder.as_markup() message = ( @@ -249,10 +282,12 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): await bot.send_message(tg_id, message, reply_markup=keyboard) logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.") - await add_notification(tg_id, 'inactive_trial', session=conn) + await add_notification(tg_id, "inactive_trial", session=conn) except Exception as e: - logger.error(f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}" + ) await asyncio.sleep(1) @@ -286,20 +321,32 @@ async def process_key(record, bot, conn): f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}" ) keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]] + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="👤 Личный кабинет", callback_data="profile" + ) + ] + ] ) try: if balance >= RENEWAL_PLANS["1"]["price"]: await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"]) - new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000) + new_expiry_time = int( + (datetime.utcnow() + timedelta(days=30)).timestamp() * 1000 + ) await update_key_expiry(client_id, new_expiry_time) servers = await get_servers_from_db() for cluster_id in servers: - await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB) - logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.") + await renew_key_in_cluster( + cluster_id, email, client_id, new_expiry_time, TOTAL_GB + ) + logger.info( + f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}." + ) await conn.execute( """ @@ -309,10 +356,14 @@ async def process_key(record, bot, conn): """, client_id, ) - logger.info(f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}.") + logger.info( + f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}." + ) try: await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard) - logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.") + logger.info( + f"Уведомление об успешном продлении отправлено клиенту {tg_id}." + ) except Exception as e: logger.error(f"Ошибка при отправке уведомления клиенту {tg_id}: {e}") @@ -320,10 +371,16 @@ async def process_key(record, bot, conn): message_expired = "Ваша подписка истекла и была удалена. Получите новую через личный кабинет" try: - await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard) - logger.info(f"Уведомление об истечении подписки и удалении ключа отправлено пользователю {tg_id}.") + await bot.send_message( + tg_id, text=message_expired, reply_markup=keyboard + ) + logger.info( + f"Уведомление об истечении подписки и удалении ключа отправлено пользователю {tg_id}." + ) except Exception as e: - logger.error(f"Ошибка при отправке уведомления об истечении подписки пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления об истечении подписки пользователю {tg_id}: {e}" + ) servers = await get_servers_from_db() @@ -343,7 +400,9 @@ async def check_online_users(): for cluster_id, cluster in servers.items(): for server_id, server in enumerate(cluster): - xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) + xui = AsyncApi( + server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD + ) await xui.login() try: online_users = len(await xui.client.online()) @@ -351,4 +410,6 @@ async def check_online_users(): f"Сервер '{server['server_name']}' доступен, текущее количество активных пользователей: {online_users}." ) except Exception as e: - logger.error(f"Не удалось проверить пользователей на сервере {server_id}: {e}") + logger.error( + f"Не удалось проверить пользователей на сервере {server_id}: {e}" + ) diff --git a/handlers/pay.py b/handlers/pay.py index bd709cd0..a2ef2e01 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -1,8 +1,13 @@ from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - -from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE +from config import ( + CRYPTO_BOT_ENABLE, + FREEKASSA_ENABLE, + ROBOKASSA_ENABLE, + STARS_ENABLE, + YOOKASSA_ENABLE, +) router = Router() @@ -46,8 +51,14 @@ async def handle_pay(callback_query: CallbackQuery): callback_data="pay_robokassa", ) ) - builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon")) - builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) + builder.row( + InlineKeyboardButton( + text="🎟️ Активировать купон", callback_data="activate_coupon" + ) + ) + builder.row( + InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") + ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py index 231dd68a..2b7063b5 100644 --- a/handlers/payments/__init__.py +++ b/handlers/payments/__init__.py @@ -1,15 +1,21 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router +from config import ( + CRYPTO_BOT_ENABLE, + FREEKASSA_ENABLE, + ROBOKASSA_ENABLE, + STARS_ENABLE, + YOOKASSA_ENABLE, +) -from config import ROBOKASSA_ENABLE, STARS_ENABLE, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, YOOKASSA_ENABLE from .cryprobot_pay import router as cryprobot_router from .freekassa_pay import router as freekassa_router from .robokassa_pay import router as robokassa_router from .stars_pay import router as stars_router from .yookassa_pay import router as yookassa_router -router = Router(name='payments_main_router') +router = Router(name="payments_main_router") if YOOKASSA_ENABLE: router.include_router(yookassa_router) diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index 6265d4db..a70d8758 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -7,9 +7,15 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web - from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance + +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -27,7 +33,9 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_cryptobot") -async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def process_callback_pay_cryptobot( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -59,7 +67,12 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st if key_count == 0: exists = await check_connection_exists(callback_query.message.chat.id) if not exists: - await add_connection(tg_id=callback_query.message.chat.id, balance=0.0, trial=0, session=session) + await add_connection( + tg_id=callback_query.message.chat.id, + balance=0.0, + trial=0, + session=session, + ) await callback_query.message.answer( "Выберите сумму пополнения:", reply_markup=builder.as_markup(), @@ -68,7 +81,9 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st @router.callback_query(F.data.startswith("crypto_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): data = callback_query.data.split("|", 1) if len(data) != 2: @@ -95,7 +110,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F if hasattr(invoice, "bot_invoice_url"): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)) + builder.row( + InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url) + ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")) await callback_query.message.answer( text=f"Вы выбрали пополнение на {amount} рублей.", @@ -115,7 +132,9 @@ async def cryptobot_webhook(request): await process_crypto_payment(data["payload"]) return web.Response(status=200) else: - logger.warning(f"Неподдерживаемый тип обновления: {data.get('update_type')}") + logger.warning( + f"Неподдерживаемый тип обновления: {data.get('update_type')}" + ) return web.Response(status=400) except Exception as e: logger.error(f"Ошибка обработки вебхука: {e}") @@ -140,8 +159,9 @@ async def process_crypto_payment(payload): @router.callback_query(F.data == "enter_custom_amount_crypto") -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_cryptobot")) @@ -158,11 +178,15 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount // RUB_TO_USDT <= 0: - await message.answer(f"Сумма должна быть больше {RUB_TO_USDT}. Пожалуйста, введите сумму еще раз:") + await message.answer( + f"Сумма должна быть больше {RUB_TO_USDT}. Пожалуйста, введите сумму еще раз:" + ) return await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_crypto) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_crypto + ) try: invoice = await crypto.create_invoice( asset="USDT", @@ -173,7 +197,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) if hasattr(invoice, "bot_invoice_url"): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)) + builder.row( + InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url) + ) builder.row( InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"), ) diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 7c423129..52b50116 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -4,15 +4,15 @@ import logging import time import uuid +import requests from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web -import requests - from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID + from database import add_payment, update_balance from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS @@ -49,7 +49,9 @@ async def create_payment(user_id, amount, email, ip): params["signature"] = generate_signature(params, FREEKASSA_API_KEY) try: - response = requests.post("https://api.freekassa.com/v1/orders/create", json=params) + response = requests.post( + "https://api.freekassa.com/v1/orders/create", json=params + ) response_data = response.json() logging.debug(f"Ответ от FreeKassa при создании платежа: {response_data}") @@ -83,7 +85,9 @@ async def freekassa_webhook(request): @router.callback_query(lambda c: c.data == "pay_freekassa") -async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext): +async def process_callback_pay_freekassa( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -121,7 +125,9 @@ async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, st @router.callback_query(F.data.startswith("freekassa_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): data = callback_query.data.split("|", 1) amount_str = data[1] try: @@ -132,12 +138,18 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F user_email = f"{callback_query.message.chat.id}@solo.net" user_ip = callback_query.message.chat.id - payment_url = await create_payment(callback_query.message.chat.id, amount, user_email, user_ip) + payment_url = await create_payment( + callback_query.message.chat.id, amount, user_email, user_ip + ) if payment_url: confirm_keyboard = InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton(text=f"Оплатить {amount} рублей", url=payment_url)], + [ + InlineKeyboardButton( + text=f"Оплатить {amount} рублей", url=payment_url + ) + ], [InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")], ] ) @@ -153,7 +165,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F @router.callback_query(F.data == "enter_custom_amount_freekassa") -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): await callback_query.message.answer(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount_freekassa) @@ -163,7 +177,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount <= 0: - await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:") + await message.answer( + "Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:" + ) return user_email = f"{message.chat.id}@solo.net" @@ -171,7 +187,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) payment_url = await create_payment(message.chat.id, amount, user_email, user_ip) if payment_url: - keyboard = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]]) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]] + ) await message.answer( f"Вы выбрали оплату на {amount} рублей. Перейдите по ссылке для завершения оплаты:", reply_markup=keyboard, diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 9ae90a29..2c1de2e5 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -7,10 +7,22 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web +from config import ( + ROBOKASSA_ENABLE, + ROBOKASSA_LOGIN, + ROBOKASSA_PASSWORD1, + ROBOKASSA_PASSWORD2, + ROBOKASSA_TEST_MODE, +) from robokassa import HashAlgorithm, Robokassa -from config import ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -37,7 +49,9 @@ if ROBOKASSA_ENABLE: def generate_payment_link(amount, inv_id, description, tg_id): """Генерация ссылки на оплату.""" - logger.debug(f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}") + logger.debug( + f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}" + ) payment_link = robokassa._payment.link.generate_by_script( out_sum=amount, inv_id=inv_id, @@ -49,7 +63,9 @@ def generate_payment_link(amount, inv_id, description, tg_id): @router.callback_query(F.data == "pay_robokassa") -async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def process_callback_pay_robokassa( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} initiated Robokassa payment.") @@ -98,7 +114,9 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st @router.callback_query(F.data.startswith("robokassa_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): logger.info(f"Получены данные callback_data: {callback_query.data}") data = callback_query.data.split("|") @@ -152,7 +170,9 @@ async def robokassa_webhook(request): shp_id = params.get("shp_id") signature_value = params.get("SignatureValue") - logger.info(f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}") + logger.info( + f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}" + ) if not check_payment_signature(params): logger.error("Неверная подпись или данные запроса.") @@ -191,7 +211,9 @@ def check_payment_signature(params): logger.info(f"Signature string before hashing: {signature_string}") - expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() + expected_signature = ( + hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() + ) logger.info(f"Expected signature: {expected_signature}") logger.info(f"Received signature: {signature_value}") @@ -200,7 +222,9 @@ def check_payment_signature(params): @router.callback_query(F.data == "enter_custom_amount_robokassa") -async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_custom_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} chose to enter a custom amount.") @@ -212,7 +236,9 @@ async def process_custom_amount_selection(callback_query: types.CallbackQuery, s reply_markup=builder.as_markup(), ) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_robokassa + ) @router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) @@ -246,4 +272,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext): await state.clear() except ValueError as e: logger.error(f"Некорректная сумма от пользователя {tg_id}: {e}") - await message.answer(text="Введите корректную сумму в рублях (целое положительное число).") + await message.answer( + text="Введите корректную сумму в рублях (целое положительное число)." + ) diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index 698444b8..f993d3f9 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -5,9 +5,15 @@ 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 database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance + +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -22,11 +28,17 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_stars") -async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def process_callback_pay_stars( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) + builder.row( + InlineKeyboardButton( + text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot" + ) + ) for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -76,7 +88,9 @@ async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: @router.callback_query(F.data.startswith("stars_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): data = callback_query.data.split("|", 1) if len(data) != 2: @@ -104,7 +118,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_stars) try: - builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="Пополнить", pay=True), @@ -128,8 +141,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F @router.callback_query(F.data == "enter_custom_amount_stars") -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_stars")) @@ -146,11 +160,15 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount // RUB_TO_XTR <= 0: - await message.answer(f"Сумма должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:") + await message.answer( + f"Сумма должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:" + ) return await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_stars) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_stars + ) try: builder = InlineKeyboardBuilder() builder.row( diff --git a/handlers/payments/utils.py b/handlers/payments/utils.py index 21015090..430c12f0 100644 --- a/handlers/payments/utils.py +++ b/handlers/payments/utils.py @@ -8,7 +8,9 @@ from logger import logger async def send_payment_success_notification(user_id: int, amount: float): try: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) await bot.send_message( chat_id=user_id, text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index c1687107..62760122 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -1,5 +1,5 @@ -from typing import Any import uuid +from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -7,10 +7,16 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web +from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID from yookassa import Configuration, Payment -from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -31,7 +37,9 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_yookassa") -async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def process_callback_pay_yookassa( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() @@ -79,7 +87,9 @@ async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, sta @router.callback_query(F.data.startswith("yookassa_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): data = callback_query.data.split("|", 1) if len(data) != 2: @@ -92,7 +102,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F return await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_yookassa) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_yookassa + ) # state_data = await state.get_data() customer_name = callback_query.from_user.full_name @@ -167,8 +179,9 @@ async def yookassa_webhook(request): @router.callback_query(F.data == "enter_custom_amount_yookassa") -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_yookassa")) @@ -185,11 +198,15 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount <= 0: - await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:") + await message.answer( + "Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:" + ) return await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_yookassa) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_yookassa + ) try: payment = Payment.create( diff --git a/handlers/profile.py b/handlers/profile.py index 80775e0e..49477276 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -4,8 +4,8 @@ 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 NEWS_MESSAGE, RENEWAL_PLANS + from database import get_balance, get_key_count, get_referral_stats from handlers.texts import get_referral_link, invite_message_send, profile_message_send @@ -13,7 +13,9 @@ router = Router() @router.callback_query(F.data == "profile") -async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext, admin: bool): +async def process_callback_view_profile( + callback_query: types.CallbackQuery, state: FSMContext, admin: bool +): chat_id = callback_query.message.chat.id username = callback_query.from_user.full_name image_path = os.path.join("img", "pic.jpg") @@ -46,7 +48,9 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta ) builder.row(InlineKeyboardButton(text="💡 Тарифы", callback_data="view_tariffs")) if admin: - builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) + builder.row( + InlineKeyboardButton(text="🔧 Администратор", callback_data="admin") + ) builder.row(InlineKeyboardButton(text="⬅️ Главное меню", callback_data="start")) if os.path.isfile(image_path): @@ -68,18 +72,15 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - image_path = os.path.join("img", "tariffs.jpg") + 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) + ] ) if os.path.isfile(image_path): @@ -96,7 +97,6 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): ) - @router.callback_query(F.data == "invite") async def invite_handler(callback_query: types.CallbackQuery): chat_id = callback_query.message.chat.id diff --git a/handlers/start.py b/handlers/start.py index 93df5511..04d0847d 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -4,11 +4,29 @@ from typing import Any from aiogram import F, Router from aiogram.filters import Command from aiogram.fsm.context import FSMContext -from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message +from aiogram.types import ( + BufferedInputFile, + CallbackQuery, + InlineKeyboardButton, + Message, +) from aiogram.utils.keyboard import InlineKeyboardBuilder +from config import ( + CHANNEL_URL, + CONNECT_ANDROID, + CONNECT_IOS, + DOWNLOAD_ANDROID, + DOWNLOAD_IOS, + SUPPORT_CHAT_URL, +) -from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, 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.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn @@ -16,7 +34,9 @@ router = Router() @router.callback_query(F.data == "start") -async def handle_start_callback_query(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool): +async def handle_start_callback_query( + callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool +): await start_command(callback_query.message, state, session, admin) @@ -37,7 +57,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin 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")) builder.row( @@ -46,7 +68,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin ) 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")) if os.path.isfile(image_path): @@ -98,16 +122,26 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): url=f'{CONNECT_ANDROID}{trial_key_info["key"]}', ), ) - builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{email}")) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton( + text="💻 Windows/Linux", callback_data=f"connect_pc|{email}" + ) + ) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) - await callback_query.message.answer(key_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + key_message, reply_markup=builder.as_markup() + ) @router.callback_query(F.data == "about_vpn") async def handle_about_vpn(callback_query: CallbackQuery): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) + builder.row( + InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") + ) builder.row( InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL), ) @@ -116,4 +150,6 @@ async def handle_about_vpn(callback_query: CallbackQuery): ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start")) - await callback_query.message.answer(get_about_vpn("3.2.21-Release"), reply_markup=builder.as_markup()) + await callback_query.message.answer( + get_about_vpn("3.2.21-Release"), reply_markup=builder.as_markup() + ) diff --git a/handlers/utils.py b/handlers/utils.py index 95871cf6..97ba02e9 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -1,11 +1,10 @@ import random import re -from typing import Optional 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 @@ -68,7 +67,9 @@ async def get_least_loaded_cluster() -> str: return least_loaded_cluster -async def handle_error(tg_id: int, callback_query: Optional[object] = None, message: str = "") -> None: +async def handle_error( + tg_id: int, callback_query: object | None = None, message: str = "" +) -> None: """ Обрабатывает ошибку, отправляя сообщение пользователю. @@ -80,7 +81,9 @@ async def handle_error(tg_id: int, callback_query: Optional[object] = None, mess try: if callback_query and hasattr(callback_query, "message"): try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception as delete_error: logger.warning(f"Не удалось удалить сообщение: {delete_error}") diff --git a/logger.py b/logger.py index d12aace2..7130c5e9 100644 --- a/logger.py +++ b/logger.py @@ -1,7 +1,7 @@ -from datetime import timedelta import logging import os import sys +from datetime import timedelta from loguru import logger diff --git a/main.py b/main.py index af659ad3..90f8bfd1 100644 --- a/main.py +++ b/main.py @@ -3,10 +3,6 @@ import signal from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application from aiohttp import web - -from backup import backup_database -from bot import bot, dp -from handlers import router from config import ( BACKUP_TIME, CRYPTO_BOT_ENABLE, @@ -21,7 +17,11 @@ from config import ( WEBHOOK_URL, YOOKASSA_ENABLE, ) + +from backup import backup_database +from bot import bot, dp from database import init_db +from handlers import router from handlers.keys.subscriptions import handle_new_subscription, handle_old_subscription from handlers.notifications import notify_expiring_keys from handlers.payments.cryprobot_pay import cryptobot_webhook @@ -108,7 +108,9 @@ async def main(): loop = asyncio.get_event_loop() for sig in (signal.SIGINT, signal.SIGTERM): - loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown_site(site))) + loop.add_signal_handler( + sig, lambda: asyncio.create_task(shutdown_site(site)) + ) try: await asyncio.Event().wait() diff --git a/middlewares/admin.py b/middlewares/admin.py index 57aa0692..85819d32 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -1,24 +1,24 @@ -from typing import Any, Awaitable, Callable, Dict, Union +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware from aiogram.types import TelegramObject - from config import ADMIN_ID class AdminMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: data["admin"] = self._check_admin_access(event) return await handler(event, data) def _check_admin_access(self, event: TelegramObject) -> bool: try: - admin_ids: Union[int, list[int]] = ADMIN_ID + admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): return event.from_user.id in admin_ids return event.from_user.id == admin_ids diff --git a/middlewares/database.py b/middlewares/database.py index b1687aa6..c625fec3 100644 --- a/middlewares/database.py +++ b/middlewares/database.py @@ -1,18 +1,18 @@ -from typing import Any, Awaitable, Callable, Dict +from collections.abc import Awaitable, Callable +from typing import Any +import asyncpg from aiogram import BaseMiddleware from aiogram.types import TelegramObject -import asyncpg - from config import DATABASE_URL class DatabaseMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: conn = await asyncpg.connect(DATABASE_URL) try: diff --git a/middlewares/delete.py b/middlewares/delete.py index 356f8831..7804b81c 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, Dict +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, Message, TelegramObject @@ -7,15 +8,17 @@ from aiogram.types import CallbackQuery, Message, TelegramObject class DeleteMessageMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: if isinstance(event, (Message, CallbackQuery)): if isinstance(event, Message): if not event.text.startswith("/start"): try: - await event.bot.delete_message(event.chat.id, event.message_id - 1) + await event.bot.delete_message( + event.chat.id, event.message_id - 1 + ) except Exception: pass await event.delete() diff --git a/middlewares/logging.py b/middlewares/logging.py index fbe31bae..1d0c1b38 100644 --- a/middlewares/logging.py +++ b/middlewares/logging.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, Dict, Optional +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, Message, TelegramObject @@ -9,9 +10,9 @@ from logger import logger class LoggingMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: user_info = self._extract_user_info(event) @@ -23,7 +24,7 @@ class LoggingMiddleware(BaseMiddleware): ) return await handler(event, data) - def _extract_user_info(self, event: TelegramObject) -> Dict[str, Optional[str]]: + def _extract_user_info(self, event: TelegramObject) -> dict[str, str | None]: user_id = None username = None action = None diff --git a/middlewares/user.py b/middlewares/user.py index 969020ef..6d276e75 100644 --- a/middlewares/user.py +++ b/middlewares/user.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, Dict +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware from aiogram.types import TelegramObject, User @@ -9,9 +10,9 @@ from database import upsert_user class UserMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: if user := data.get("event_from_user"): await self._process_user(user) diff --git a/pyproject.toml b/pyproject.toml index fb4c1abc..1ea6e345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,27 +1,17 @@ -[tool.black] -line-length = 120 # Совпадает с flake8 -target-version = ['py39','py310','py311'] # Укажите версию Python вашего проекта -skip-string-normalization = true # Отключает нормализацию кавычек -include = '\.pyi?$' # Включает Python-файлы -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist -)/ -''' # Исключает системные папки +[tool.ruff] +line-length = 120 +target-version = "py310" -[tool.isort] -profile = "black" # Устанавливает совместимость с black -line_length = 120 -multi_line_output = 3 # Формат многострочных импортов -include_trailing_comma = true # Совместимость с black -force_sort_within_sections = true # Сортировка внутри секций -sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] -skip_gitignore = true # Учитывать .gitignore +[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'] + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" + +[tool.darker] +src = ["."] +revision = "HEAD" +diff = false +check = false \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 77ed916d..7a5d0a6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,8 +30,4 @@ aiocryptopay py3xui sqlalchemy robokassa -flake8 -black -isort -pylint ping3 \ No newline at end of file diff --git a/servers.py b/servers.py index fbb81968..8dde3f5c 100644 --- a/servers.py +++ b/servers.py @@ -1,14 +1,14 @@ import asyncio -from datetime import datetime, timedelta import re +from datetime import datetime, timedelta +import asyncpg from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg +from config import ADMIN_ID, DATABASE_URL from ping3 import ping from bot import bot -from config import ADMIN_ID, DATABASE_URL from database import get_servers_from_db from logger import logger @@ -16,7 +16,9 @@ try: from config import CLUSTERS except ImportError: CLUSTERS = None - logger.warning("Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!") + logger.warning( + "Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!" + ) async def sync_servers_with_db(): @@ -25,7 +27,9 @@ async def sync_servers_with_db(): Если CLUSTERS не найден, синхронизация не будет выполнена. """ if CLUSTERS is None: - logger.info("Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена.") + logger.info( + "Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена." + ) return try: @@ -55,14 +59,18 @@ async def sync_servers_with_db(): server_info["SUBSCRIPTION"], server_info["INBOUND_ID"], ) - logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных.") + logger.info( + f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных." + ) else: - logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} уже существует.") + logger.info( + f"Сервер {server_info['name']} из кластера {cluster_name} уже существует." + ) except Exception as e: logger.error(f"Ошибка при синхронизации серверов: {e}") finally: - if 'conn' in locals(): + if "conn" in locals(): await conn.close() @@ -96,14 +104,24 @@ 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): - logger.info(f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут.") + if last_notification_time and current_time - last_notification_time < timedelta( + minutes=3 + ): + logger.info( + f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут." + ) return - logger.info(f"Отправка уведомлений администратору о недоступности сервера {server_name}...") + logger.info( + f"Отправка уведомлений администратору о недоступности сервера {server_name}..." + ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Управление сервером", callback_data=f"manage_server|{server_name}")) + builder.row( + InlineKeyboardButton( + text="Управление сервером", callback_data=f"manage_server|{server_name}" + ) + ) for admin_id in ADMIN_ID: await bot.send_message( @@ -116,7 +134,9 @@ async def notify_admin(server_name: str): parse_mode="HTML", reply_markup=builder.as_markup(), ) - logger.info(f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}.") + logger.info( + f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}." + ) last_notification_times[server_name] = current_time except Exception as e: @@ -140,7 +160,9 @@ async def check_servers(): server_name = server["server_name"] server_host = extract_host(original_api_url) - logger.debug(f"Проверка доступности сервера '{server_name}' с хостом {server_host}") + logger.debug( + f"Проверка доступности сервера '{server_name}' с хостом {server_host}" + ) is_online = await ping_server(server_host) @@ -148,12 +170,18 @@ async def check_servers(): last_ping_times[server_name] = current_time else: last_ping_time = last_ping_times.get(server_name) - if last_ping_time and current_time - last_ping_time > timedelta(minutes=3): - logger.warning(f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление.") + if last_ping_time and current_time - last_ping_time > timedelta( + minutes=3 + ): + logger.warning( + f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление." + ) await notify_admin(server_name) elif not last_ping_time: last_ping_times[server_name] = current_time - logger.info(f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован.") + logger.info( + f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован." + ) logger.info("Завершена проверка всех серверов.") await asyncio.sleep(30) From 1e912487afbda244036b4c3c8bac921b5a117007 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Fri, 13 Dec 2024 12:13:50 +0300 Subject: [PATCH 19/28] Fix requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a5d0a6f..72b819da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,4 +30,5 @@ aiocryptopay py3xui sqlalchemy robokassa -ping3 \ No newline at end of file +ping3 +ruff \ No newline at end of file From e5da607c9d271738c33910606f81ada09a235956 Mon Sep 17 00:00:00 2001 From: Vladless Date: Fri, 13 Dec 2024 23:01:58 +0300 Subject: [PATCH 20/28] sub_name/delete user --- database.py | 21 +++++--- handlers/__init__.py | 1 - handlers/admin/admin_user_editor.py | 74 ++++++++++++++++++++++------- handlers/keys/subscriptions.py | 35 +++++++++----- handlers/profile.py | 20 ++++---- 5 files changed, 102 insertions(+), 49 deletions(-) diff --git a/database.py b/database.py index a51ceb17..ad91821f 100644 --- a/database.py +++ b/database.py @@ -915,12 +915,12 @@ async def get_tg_id_by_client_id(client_id: str): async def upsert_user( - tg_id: int, - username: str = None, - first_name: str = None, - last_name: str = None, - language_code: str = None, - is_bot: bool = False, + tg_id: int, + username: str = None, + first_name: str = None, + last_name: str = None, + language_code: str = None, + is_bot: bool = False, ): """ Обновляет или вставляет информацию о пользователе в базу данных. @@ -1117,3 +1117,12 @@ async def get_servers_from_db(): ) return servers + + +async def delete_user_data(session: Any, tg_id: int): + await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id) + 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) + await session.execute("DELETE FROM keys WHERE tg_id = $1", tg_id) + await session.execute("DELETE FROM referrals WHERE referrer_tg_id = $1", tg_id) diff --git a/handlers/__init__.py b/handlers/__init__.py index 613d08f9..5037c650 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -22,7 +22,6 @@ router.include_routers( donate_router, coupons_router, notifications_router, - payments_router, keys_router, instructions_router, diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index d579cb5f..7a9bd856 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -5,11 +5,11 @@ from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, InlineKeyboardButton +from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from config import TOTAL_GB -from database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry +from database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry, delete_user_data from filters.admin import IsAdminFilter from handlers.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster from handlers.utils import sanitize_key_name @@ -86,7 +86,7 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi 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="user_editor")) user_info = ( @@ -499,20 +499,9 @@ async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContex for (email,) in key_records: builder.row(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"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="user_editor")) user_info = ( @@ -523,5 +512,56 @@ async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContex f"👥 Количество рефералов: {referral_count}\n" f"🔑 Ключи (для редактирования нажмите на ключ):" ) + await callback_query.message.answer(user_info, reply_markup=builder.as_markup()) await state.set_state(UserEditorState.displaying_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): + 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="user_editor")], + ], + ) + + await callback_query.message.answer( + f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", reply_markup=confirmation_markup + ) + + +@router.callback_query(F.data.startswith("delete_user_"), IsAdminFilter()) +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) + + async def delete_keys_from_servers(): + try: + tasks = [] + for email, client_id in key_records: + servers = await get_servers_from_db() + for cluster_id, cluster in servers.items(): + 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}") + + await delete_keys_from_servers() + + try: + await delete_user_data(session, tg_id) + + 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) + except Exception as e: + logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}") + await callback_query.message.answer( + f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." + ) diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 45b0d3fc..0f735199 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -8,6 +8,9 @@ import asyncpg from config import DATABASE_URL, TRANSITION_DATE_STR from database import get_servers_from_db from logger import logger +import urllib.parse + +from config import PROJECT_NAME, NEWS_MESSAGE async def fetch_url_content(url, tg_id): @@ -67,7 +70,7 @@ async def handle_old_subscription(request): conn = await asyncpg.connect(DATABASE_URL) try: - key_info = await conn.fetchrow("SELECT created_at FROM keys WHERE email = $1", email) + key_info = await conn.fetchrow("SELECT created_at, cluster_name FROM keys WHERE email = $1", email) if not key_info: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -77,7 +80,8 @@ async def handle_old_subscription(request): ) created_at_ms = key_info["created_at"] - logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}") + cluster_name = key_info["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(f"Время создания клиента в формате datetime (UTC): {created_at_datetime}") @@ -90,22 +94,24 @@ async def handle_old_subscription(request): ) servers = await get_servers_from_db() + cluster_servers = servers.get(cluster_name, []) + logger.info(f"Сервера в кластере: {cluster_servers}") urls = [] - for cluster_name, cluster_servers in servers.items(): - for server in cluster_servers: - server_subscription_url = f"{server['subscription_url']}/{email}" - urls.append(server_subscription_url) + for server in cluster_servers: + server_subscription_url = f"{server['subscription_url']}/{email}" + urls.append(server_subscription_url) combined_subscriptions = await combine_unique_lines(urls, email, "") base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8") + encoded_project_name = urllib.parse.quote(f"{PROJECT_NAME}\n{NEWS_MESSAGE}") headers = { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "profile-update-interval": "7", - "profile-title": email, + "profile-title": encoded_project_name, } logger.info(f"Возвращаем объединенные подписки для email: {email}") @@ -130,7 +136,7 @@ async def handle_new_subscription(request): conn = await asyncpg.connect(DATABASE_URL) try: - client_data = await conn.fetchrow("SELECT tg_id FROM keys WHERE email = $1", email) + client_data = await conn.fetchrow("SELECT tg_id, server_id FROM keys WHERE email = $1", email) if not client_data: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -140,6 +146,7 @@ async def handle_new_subscription(request): ) stored_tg_id = client_data["tg_id"] + cluster_name = client_data["server_id"] if str(tg_id) != str(stored_tg_id): logger.warning(f"Неверный tg_id для клиента с email {email}.") @@ -147,16 +154,17 @@ async def handle_new_subscription(request): text="❌ Неверные данные. Получите свой ключ в боте.", status=403, ) + finally: await conn.close() servers = await get_servers_from_db() + cluster_servers = servers.get(cluster_name, []) urls = [] - for cluster_name, cluster_servers in servers.items(): - for server in cluster_servers: - server_subscription_url = f"{server['subscription_url']}/{email}" - urls.append(server_subscription_url) + for server in cluster_servers: + server_subscription_url = f"{server['subscription_url']}/{email}" + urls.append(server_subscription_url) query_string = request.query_string logger.info(f"Извлечен query string: {query_string}") @@ -165,11 +173,12 @@ async def handle_new_subscription(request): base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8") + encoded_project_name = urllib.parse.quote(f"{PROJECT_NAME}\n{NEWS_MESSAGE}") headers = { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "profile-update-interval": "7", - "profile-title": email, + "profile-title": encoded_project_name, } logger.info(f"Возвращаем объединенные подписки для email: {email}") diff --git a/handlers/profile.py b/handlers/profile.py index 80775e0e..3a43d47f 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -68,18 +68,15 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - image_path = os.path.join("img", "tariffs.jpg") + 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) + ] ) if os.path.isfile(image_path): @@ -96,7 +93,6 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): ) - @router.callback_query(F.data == "invite") async def invite_handler(callback_query: types.CallbackQuery): chat_id = callback_query.message.chat.id From 2750517be42bdac092e6540d8c6655cc1173fff2 Mon Sep 17 00:00:00 2001 From: Vladless Date: Sat, 14 Dec 2024 00:37:20 +0300 Subject: [PATCH 21/28] vat_code --- handlers/payments/yookassa_pay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index c1687107..01618660 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -120,7 +120,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F "description": "Пополнение баланса", "quantity": "1.00", "amount": {"value": str(amount), "currency": "RUB"}, - "vat_code": 6, + "vat_code": 1, } ], }, @@ -215,7 +215,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) "value": str(amount), "currency": "RUB", }, - "vat_code": 6, + "vat_code": 1, } ], }, From f821241edbb41eed45492b2548bb16f15863d61e Mon Sep 17 00:00:00 2001 From: Vladless Date: Sat, 14 Dec 2024 17:39:39 +0300 Subject: [PATCH 22/28] fix middleware --- middlewares/delete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middlewares/delete.py b/middlewares/delete.py index 7804b81c..846ed89a 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -14,7 +14,7 @@ class DeleteMessageMiddleware(BaseMiddleware): ) -> Any: if isinstance(event, (Message, CallbackQuery)): if isinstance(event, Message): - if not event.text.startswith("/start"): + if not event.text or not event.text.startswith("/start"): try: await event.bot.delete_message( event.chat.id, event.message_id - 1 From ca87286898729f00eb318e9468a4d9e8aacef232 Mon Sep 17 00:00:00 2001 From: Vladless Date: Sat, 14 Dec 2024 18:46:43 +0300 Subject: [PATCH 23/28] sub_name --- handlers/keys/subscriptions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 4073dc71..a6a2c15b 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -10,7 +10,7 @@ from database import get_servers_from_db from logger import logger import urllib.parse -from config import PROJECT_NAME, NEWS_MESSAGE +from config import PROJECT_NAME, SUB_MESSAGE async def fetch_url_content(url, tg_id): @@ -122,7 +122,7 @@ async def handle_old_subscription(request): "\n".join(combined_subscriptions).encode("utf-8") ).decode("utf-8") - encoded_project_name = urllib.parse.quote(f"{PROJECT_NAME}\n{NEWS_MESSAGE}") + encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}" headers = { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", @@ -154,10 +154,9 @@ async def handle_new_subscription(request): try: client_data = await conn.fetchrow( - "SELECT tg_id FROM keys WHERE email = $1", email + "SELECT tg_id, server_id FROM keys WHERE email = $1", email ) - if not client_data: logger.warning(f"Клиент с email {email} не найден в базе.") return web.Response( @@ -195,7 +194,8 @@ async def handle_new_subscription(request): "\n".join(combined_subscriptions).encode("utf-8") ).decode("utf-8") - encoded_project_name = urllib.parse.quote(f"{PROJECT_NAME}\n{NEWS_MESSAGE}") + encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}" + headers = { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", From 7677ae91932966ab372239186d38c745d07648fd Mon Sep 17 00:00:00 2001 From: TrackLine Date: Sat, 14 Dec 2024 19:49:54 +0400 Subject: [PATCH 24/28] Add Yoomoney pay service --- handlers/pay.py | 8 ++ handlers/payments/__init__.py | 4 + handlers/payments/yoomoney_pay.py | 206 ++++++++++++++++++++++++++++++ main.py | 4 + 4 files changed, 222 insertions(+) create mode 100644 handlers/payments/yoomoney_pay.py diff --git a/handlers/pay.py b/handlers/pay.py index a2ef2e01..debe9a3f 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -7,6 +7,7 @@ from config import ( ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE, + YOOMONEY_ENABLE ) router = Router() @@ -23,6 +24,13 @@ async def handle_pay(callback_query: CallbackQuery): callback_data="pay_yookassa", ) ) + if YOOMONEY_ENABLE: + builder.row( + InlineKeyboardButton( + text="💳 ЮМани: оплата по карте", + callback_data="pay_yoomoney", + ) + ) if FREEKASSA_ENABLE: builder.row( InlineKeyboardButton( diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py index 2b7063b5..aabe25a2 100644 --- a/handlers/payments/__init__.py +++ b/handlers/payments/__init__.py @@ -7,6 +7,7 @@ from config import ( ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE, + YOOMONEY_ENABLE, ) from .cryprobot_pay import router as cryprobot_router @@ -14,11 +15,14 @@ from .freekassa_pay import router as freekassa_router from .robokassa_pay import router as robokassa_router from .stars_pay import router as stars_router from .yookassa_pay import router as yookassa_router +from .yoomoney_pay import router as yoomoney_router router = Router(name="payments_main_router") if YOOKASSA_ENABLE: router.include_router(yookassa_router) +if YOOMONEY_ENABLE: + router.include_router(yoomoney_router) if FREEKASSA_ENABLE: router.include_router(freekassa_router) if ROBOKASSA_ENABLE: diff --git a/handlers/payments/yoomoney_pay.py b/handlers/payments/yoomoney_pay.py new file mode 100644 index 00000000..f3957640 --- /dev/null +++ b/handlers/payments/yoomoney_pay.py @@ -0,0 +1,206 @@ +import uuid +from typing import Any + +from aiogram import F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiohttp import web +from config import YOOMONEY_ENABLE, YOOMONEY_ID, YOOMONEY_SECRET_KEY + +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) +from handlers.payments.utils import send_payment_success_notification +from handlers.texts import PAYMENT_OPTIONS +from logger import logger +import hashlib + +router = Router() + +if YOOMONEY_ENABLE: + logger.debug(f"Account ID: {YOOMONEY_ID}") + + +class ReplenishBalanceState(StatesGroup): + choosing_amount_yoomoney = State() + waiting_for_payment_confirmation_yoomoney = State() + entering_custom_amount_yoomoney = State() + + +@router.callback_query(F.data == "pay_yoomoney") +async def process_callback_pay_yoomoney( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): + tg_id = callback_query.message.chat.id + + builder = InlineKeyboardBuilder() + + for i in range(0, len(PAYMENT_OPTIONS), 2): + if i + 1 < len(PAYMENT_OPTIONS): + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=f'yoomoney_{PAYMENT_OPTIONS[i]["callback_data"]}', + ), + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i + 1]["text"], + callback_data=f'yoomoney_{PAYMENT_OPTIONS[i + 1]["callback_data"]}', + ), + ) + else: + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=f'yoomoney_{PAYMENT_OPTIONS[i]["callback_data"]}', + ) + ) + builder.row( + InlineKeyboardButton( + text="💰 Ввести свою сумму", + callback_data="enter_custom_amount_yoomoney", + ) + ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")) + + key_count = await get_key_count(tg_id) + + if key_count == 0: + exists = await check_connection_exists(tg_id) + if not exists: + await add_connection(tg_id, balance=0.0, trial=0, session=session) + + await callback_query.message.answer( + text="Выберите сумму пополнения:", + reply_markup=builder.as_markup(), + ) + + await state.set_state(ReplenishBalanceState.choosing_amount_yoomoney) + + +@router.callback_query(F.data.startswith("yoomoney_amount")) +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): + data = callback_query.data.split("|", 1) + + if len(data) != 2: + return + + amount_str = data[1] + try: + amount = int(amount_str) + except ValueError: + return + + await state.update_data(amount=amount) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_yoomoney + ) + + # state_data = await state.get_data() + #customer_name = callback_query.from_user.full_name + customer_id = callback_query.message.chat.id + account_id = YOOMONEY_ID + payment_url = f"https://yoomoney.ru/quickpay/confirm.xml?receiver={account_id}&quickpay-form=shop&targets=&sum={amount}&paymentType=PC&comment=Пополнение баланса&label={customer_id}" + + confirm_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Пополнить", url=payment_url)], + [InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")], + ] + ) + + await callback_query.message.answer( + text=f"Вы выбрали пополнение на {amount} рублей.", + reply_markup=confirm_keyboard, + ) + + +async def yoomoney_webhook(request: web.Request): + data = await request.post() + logger.debug(f"Webhook event received: {data}") + + user_id_str = data.get("label") + amount_str = data.get("withdraw_amount") + notification_secret = YOOMONEY_SECRET_KEY + sha1_hash = data.get("sha1_hash") + + # Строим строку для хэширования + string_to_hash = f"{data.get('notification_type')}&{data.get('operation_id')}&{data.get('amount')}&{data.get('currency')}&{data.get('datetime')}&{data.get('sender')}&{data.get('codepro')}&{notification_secret}&{user_id_str}" + + # Высчитываем хэш + calculated_hash = hashlib.sha1(string_to_hash.encode('utf-8')).hexdigest() + + # Проверяем хэш + if calculated_hash != sha1_hash: + logger.error("Проверка хэша не пройдена") + return web.Response(status=400) + + try: + user_id = int(user_id_str) + amount = float(amount_str) + logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}") + await add_payment(user_id, amount, "yoomoney") + await update_balance(user_id, amount) + await send_payment_success_notification(user_id, amount) + except ValueError as e: + logger.error(f"Ошибка конвертации user_id или amount: {e}") + return web.Response(status=400) + + return web.Response(status=200) + + +@router.callback_query(F.data == "enter_custom_amount_yoomoney") +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_yoomoney")) + + await callback_query.message.answer( + "Пожалуйста, введите сумму пополнения.", + reply_markup=builder.as_markup(), + ) + + await state.set_state(ReplenishBalanceState.entering_custom_amount_yoomoney) + + +@router.message(ReplenishBalanceState.entering_custom_amount_yoomoney) +async def process_custom_amount_input(message: types.Message, state: FSMContext): + if message.text.isdigit(): + amount = int(message.text) + if amount <= 0: + await message.answer( + "Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:" + ) + return + + await state.update_data(amount=amount) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_yoomoney + ) + + customer_id = message.chat.id + account_id = YOOMONEY_ID + payment_url = f"https://yoomoney.ru/quickpay/confirm.xml?receiver={account_id}&quickpay-form=shop&targets=&sum={amount}&paymentType=PC&comment=Пополнение баланса&label={customer_id}" + + confirm_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Пополнить", url=payment_url)], + [InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")], + ] + ) + + await message.answer( + text=f"Вы выбрали пополнение на {amount} рублей.", + reply_markup=confirm_keyboard, + ) + + else: + await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:") diff --git a/main.py b/main.py index 90f8bfd1..4b5e542a 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,7 @@ from config import ( WEBHOOK_PATH, WEBHOOK_URL, YOOKASSA_ENABLE, + YOOMONEY_ENABLE, ) from backup import backup_database @@ -28,6 +29,7 @@ from handlers.payments.cryprobot_pay import cryptobot_webhook from handlers.payments.freekassa_pay import freekassa_webhook from handlers.payments.robokassa_pay import robokassa_webhook from handlers.payments.yookassa_pay import yookassa_webhook +from handlers.payments.yoomoney_pay import yoomoney_webhook from logger import logger from servers import check_servers, sync_servers_with_db @@ -85,6 +87,8 @@ async def main(): app.on_shutdown.append(on_shutdown) if YOOKASSA_ENABLE: app.router.add_post("/yookassa/webhook", yookassa_webhook) + if YOOMONEY_ENABLE: + app.router.add_post("/yoomoney/webhook", yoomoney_webhook) if FREEKASSA_ENABLE: app.router.add_post("/freekassa/webhook", freekassa_webhook) if CRYPTO_BOT_ENABLE: From 831472ab51ca63ee7cdc7b6efc082b09012d7876 Mon Sep 17 00:00:00 2001 From: Vladislav Lisitsyn <114032530+Vladless@users.noreply.github.com> Date: Sat, 14 Dec 2024 21:24:42 +0300 Subject: [PATCH 25/28] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 24b4bfcf..897b1c87 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ SoloBot реализует множество функций, включая: * через Юкасса (самозанятость и ИП) * через freekassa (Физические Лица) * Криптовалюта (Thanks [izzzzzi](https://github.com/izzzzzi)) - * Звезды Telegram + * Звезды Telegram + * Юмани (физические лица) (Thanks [TrackLine](https://github.com/TrackLine)) - Периодические **бэкапы базы данных клиентов**. - Уведомления: - Произвольные сообщения через админку. From f1358b36b72fc996c6526bb1a97f2f11fafd80f0 Mon Sep 17 00:00:00 2001 From: Vladless Date: Sat, 14 Dec 2024 22:17:35 +0300 Subject: [PATCH 26/28] add import --- handlers/admin/admin_user_editor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 7d52d3ac..7bea7151 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -14,6 +14,7 @@ from database import ( get_servers_from_db, restore_trial, update_key_expiry, + delete_user_data ) from filters.admin import IsAdminFilter From fd3d5d8de6ad9d3c6b67f77d602bf09b3923e33a Mon Sep 17 00:00:00 2001 From: Vladless Date: Sat, 14 Dec 2024 22:18:46 +0300 Subject: [PATCH 27/28] add import --- handlers/admin/admin_user_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 7bea7151..3e00bed1 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -171,7 +171,7 @@ 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="🔄 Восстановить пробник", From de6a79a634878782c5ffd5f9c99fc165a2a09076 Mon Sep 17 00:00:00 2001 From: Vladless Date: Sat, 14 Dec 2024 22:29:03 +0300 Subject: [PATCH 28/28] remove gifts --- database.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/database.py b/database.py index 2a293fd0..d689070f 100644 --- a/database.py +++ b/database.py @@ -1199,7 +1199,13 @@ async def get_servers_from_db(): async def delete_user_data(session: Any, tg_id: int): - await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id) + + 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 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)