Merge pull request #95 from izzzzzi/main

Add admin middleware
This commit is contained in:
Vladislav Lisitsyn
2024-11-18 12:54:05 +03:00
committed by GitHub
33 changed files with 336 additions and 461 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[flake8]
max-line-length = 250
ignore = E203, E266, E501, W503, F541, E704, W293, W291, E126, E121, E123, E128, E302, E131, E231, W292, E402, E261, E305
ignore = E203, E266, E501, W503, F541, E704, W293, W291, E126, E121, E123, E128, E302, E131, E231, W292, E402, E261, E305, E701
max-complexity = 25
select = B, C, E, F, W, T4, B9
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.txt
+2 -1
View File
@@ -50,4 +50,5 @@ handlers/texts.py
Thumbs.db
nginx.conf
scripts/nginx.sh
scripts
models.py
+27 -7
View File
@@ -1,6 +1,7 @@
import os
import subprocess
from datetime import datetime
from typing import Union
from aiogram.types import BufferedInputFile
@@ -11,6 +12,15 @@ from logger import logger
async def backup_database():
from bot import bot
try:
if backup_file_path := _create_database_backup():
await _send_backup_to_admin(bot, backup_file_path)
_cleanup_old_backups()
except Exception as e:
logger.error(f"Ошибка при создании или отправке бэкапа: {e}")
def _create_database_backup():
USER = DB_USER
HOST = "localhost"
BACKUP_DIR = BACK_DIR
@@ -25,25 +35,37 @@ async def backup_database():
check=True,
)
logger.info(f"Бэкап базы данных создан: {BACKUP_FILE}")
return BACKUP_FILE
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при создании бэкапа базы данных: {e}")
return
return None
finally:
del os.environ["PGPASSWORD"]
async def _send_backup_to_admin(bot, backup_file_path):
try:
with open(BACKUP_FILE, "rb") as backup_file:
with open(backup_file_path, "rb") as backup_file:
backup_input_file = BufferedInputFile(
backup_file.read(), filename=os.path.basename(BACKUP_FILE)
backup_file.read(), filename=os.path.basename(backup_file_path)
)
await bot.send_document(ADMIN_ID, backup_input_file)
admin_ids: Union[int, list[int]] = ADMIN_ID
if isinstance(admin_ids, list):
for id in admin_ids:
await bot.send_document(id, backup_input_file)
else:
await bot.send_document(admin_ids, backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
except Exception as e:
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
def _cleanup_old_backups():
try:
subprocess.run(
[
"find",
BACKUP_DIR,
BACK_DIR,
"-type",
"f",
"-name",
@@ -60,5 +82,3 @@ async def backup_database():
logger.info("Старые бэкапы удалены.")
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
del os.environ["PGPASSWORD"]
+6 -3
View File
@@ -2,8 +2,9 @@ from aiogram import Bot, Dispatcher, Router
from aiogram.fsm.storage.memory import MemoryStorage
from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE
from middlewares.database import DatabaseMiddleware
from middlewares.admin import AdminMiddleware
from middlewares.logging import LoggingMiddleware
from middlewares.user import UserMiddleware
bot = Bot(token=API_TOKEN)
storage = MemoryStorage()
@@ -44,6 +45,8 @@ if ROBOKASSA_ENABLE:
dp.message.middleware(LoggingMiddleware())
dp.callback_query.middleware(LoggingMiddleware())
dp.message.middleware(AdminMiddleware())
dp.callback_query.middleware(AdminMiddleware())
dp.message.middleware(DatabaseMiddleware())
dp.callback_query.middleware(DatabaseMiddleware())
dp.message.middleware(UserMiddleware())
dp.callback_query.middleware(UserMiddleware())
+71
View File
@@ -9,6 +9,22 @@ from logger import logger
async def init_db():
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(
"""
@@ -420,3 +436,58 @@ async def get_tg_id_by_client_id(client_id: str):
return result["tg_id"] if result else None
finally:
await conn.close()
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,
):
"""
Создает или обновляет информацию о пользователе в базе данных.
Args:
tg_id (int): Уникальный идентификатор пользователя в Telegram
username (str, optional): Никнейм пользователя
first_name (str, optional): Имя пользователя
last_name (str, optional): Фамилия пользователя
language_code (str, optional): Код языка пользователя
is_bot (bool, optional): Флаг, указывающий является ли пользователь ботом
"""
conn = await asyncpg.connect(DATABASE_URL)
try:
await conn.execute(
"""
INSERT INTO users (tg_id, username, first_name, last_name, language_code, is_bot, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (tg_id) DO UPDATE
SET
username = COALESCE(EXCLUDED.username, users.username),
first_name = COALESCE(EXCLUDED.first_name, users.first_name),
last_name = COALESCE(EXCLUDED.last_name, users.last_name),
language_code = COALESCE(EXCLUDED.language_code, users.language_code),
is_bot = EXCLUDED.is_bot,
updated_at = CURRENT_TIMESTAMP
""",
tg_id,
username,
first_name,
last_name,
language_code,
is_bot,
)
# Создаем запись в connections, если ее еще нет
await conn.execute(
"""
INSERT INTO connections (tg_id, balance, trial)
VALUES ($1, 0.0, 0)
ON CONFLICT (tg_id) DO NOTHING
""",
tg_id,
)
finally:
await conn.close()
+11 -5
View File
@@ -1,3 +1,5 @@
from typing import Union
from aiogram.filters import BaseFilter
from aiogram.types import Message
@@ -6,9 +8,13 @@ from config import ADMIN_ID
class IsAdminFilter(BaseFilter):
async def __call__(self, message: Message) -> bool:
if isinstance(ADMIN_ID, list):
return message.from_user.id in ADMIN_ID
elif isinstance(ADMIN_ID, int):
return message.from_user.id == ADMIN_ID
else:
try:
admin_ids: Union[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
except Exception:
return False
+13 -6
View File
@@ -5,6 +5,7 @@ from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from database import create_coupon, delete_coupon_from_db, get_all_coupons
from filters.admin import IsAdminFilter
from logger import logger
@@ -15,7 +16,7 @@ class AdminCouponsState(StatesGroup):
router = Router()
@router.callback_query(F.data == "coupons_editor")
@router.callback_query(F.data == "coupons_editor", IsAdminFilter())
async def show_coupon_management_menu(callback_query: types.CallbackQuery):
try:
await callback_query.message.delete()
@@ -38,7 +39,7 @@ async def show_coupon_management_menu(callback_query: types.CallbackQuery):
await callback_query.answer()
@router.callback_query(F.data == "coupons")
@router.callback_query(F.data == "coupons", IsAdminFilter())
async def show_coupon_list(callback_query: types.CallbackQuery):
try:
try:
@@ -99,7 +100,7 @@ async def show_coupon_list(callback_query: types.CallbackQuery):
await callback_query.answer()
@router.callback_query(F.data.startswith("delete_coupon_"))
@router.callback_query(F.data.startswith("delete_coupon_"), IsAdminFilter())
async def handle_delete_coupon(callback_query: types.CallbackQuery):
coupon_code = callback_query.data[len("delete_coupon_") :]
@@ -127,24 +128,29 @@ async def handle_delete_coupon(callback_query: types.CallbackQuery):
await callback_query.answer()
@router.callback_query(F.data == "create_coupon")
@router.callback_query(F.data == "create_coupon", IsAdminFilter())
async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
markup = builder.as_markup()
await callback_query.message.answer(
"<b>Введите данные для создания купона в формате:</b>\n\n"
"<i>код</i> <i>сумма</i> <i>лимит</i>\n\n"
"Пример: <b>'COUPON1 50 5'</b>\n\n",
parse_mode="HTML",
reply_markup=markup,
)
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
await callback_query.answer()
@router.message(AdminCouponsState.waiting_for_coupon_data)
@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter())
async def process_coupon_data(message: types.Message, state: FSMContext):
text = message.text.strip()
@@ -202,6 +208,7 @@ async def process_coupon_data(message: types.Message, state: FSMContext):
@router.callback_query(F.data == "back_to_coupons_menu")
async def back_to_coupons_menu(callback_query: types.CallbackQuery):
async def back_to_coupons_menu(callback_query: types.CallbackQuery, state: FSMContext):
"""Возвращаем пользователя в меню управления купонами"""
await state.clear()
await show_coupon_management_menu(callback_query)
+10 -2
View File
@@ -23,8 +23,13 @@ class UserEditorState(StatesGroup):
displaying_user_info = State()
@router.message(Command("admin"), IsAdminFilter())
async def handle_admin_command(message: types.Message):
@router.callback_query(F.data == "admin", IsAdminFilter())
async def handle_admin_callback_query(callback_query: CallbackQuery):
await handle_admin_message(callback_query.message)
@router.message(Command("admin"), F.data == "admin", IsAdminFilter())
async def handle_admin_message(message: types.Message):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
@@ -50,6 +55,9 @@ async def handle_admin_command(message: types.Message):
builder.row(
InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")
)
builder.row(
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")
)
await bot.send_message(
message.chat.id, "🤖 Панель администратора", reply_markup=builder.as_markup()
)
+4 -4
View File
@@ -8,10 +8,10 @@ router = Router()
@router.message(Command("start"))
async def handle_start(message: types.Message, state: FSMContext):
await start_command(message)
async def handle_start(message: types.Message, state: FSMContext, admin: bool = False):
await start_command(message, admin)
@router.message(Command("menu"))
async def handle_menu(message: types.Message, state: FSMContext):
await start_command(message)
async def handle_menu(message: types.Message, state: FSMContext, admin: bool = False):
await start_command(message, admin)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

+1 -1
View File
@@ -16,7 +16,7 @@ async def send_instructions(callback_query: types.CallbackQuery):
instructions_message = INSTRUCTIONS
image_path = os.path.join(os.path.dirname(__file__), "instructions.jpg")
image_path = os.path.join("img", "instructions.jpg")
if not os.path.isfile(image_path):
await callback_query.message.answer("Файл изображения не найден.")
+4 -1
View File
@@ -9,7 +9,7 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
from bot import bot, dp
from config import CONNECT_ANDROID, CONNECT_IOS, DATABASE_URL, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK
from config import CONNECT_ANDROID, CONNECT_IOS, DATABASE_URL, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, SUPPORT_CHAT_URL
from database import add_connection, get_balance, store_key, update_balance
from handlers.instructions.instructions import send_instructions
from handlers.keys.key_utils import create_key_on_cluster
@@ -202,6 +202,8 @@ async def handle_key_name_input(message: Message, state: FSMContext):
logger.info(f"Generated public link for the key: {public_link}")
button_support = InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL)
button_profile = InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
@@ -221,6 +223,7 @@ async def handle_key_name_input(message: Message, state: FSMContext):
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[button_support],
[button_download_ios, button_download_android],
[button_iphone, button_android],
[button_profile],
+3 -3
View File
@@ -56,7 +56,7 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery):
"<i>👇 Выберите устройство для управления подпиской:</i>"
)
image_path = os.path.join(os.path.dirname(__file__), "pic_keys.jpg")
image_path = os.path.join("img", "pic_keys.jpg")
try:
await bot.delete_message(
@@ -104,7 +104,7 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery):
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
image_path = os.path.join(os.path.dirname(__file__), "pic_keys.jpg")
image_path = os.path.join("img", "pic_keys.jpg")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
@@ -231,7 +231,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery):
keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
image_path = os.path.join(os.path.dirname(__file__), "pic_view.jpg")
image_path = os.path.join("img", "pic_view.jpg")
if not os.path.isfile(image_path):
await bot.send_message(tg_id, "Файл изображения не найден.")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+2 -2
View File
@@ -72,8 +72,8 @@ async def handle_pay(callback_query: CallbackQuery):
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery):
async def handle_back_to_menu(callback_query: CallbackQuery, admin: bool = False):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
await send_welcome_message(callback_query.from_user.id, trial_status)
await send_welcome_message(callback_query.from_user.id, trial_status, admin)
await callback_query.answer()
+10 -9
View File
@@ -10,7 +10,7 @@ from loguru import logger
from robokassa import HashAlgorithm, Robokassa
from bot import bot
from config import ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE
from config import ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.texts import PAYMENT_OPTIONS
@@ -22,15 +22,16 @@ class ReplenishBalanceState(StatesGroup):
waiting_for_payment_confirmation_robokassa = State()
robokassa = Robokassa(
merchant_login=ROBOKASSA_LOGIN,
password1=ROBOKASSA_PASSWORD1,
password2=ROBOKASSA_PASSWORD2,
algorithm=HashAlgorithm.md5,
is_test=ROBOKASSA_TEST_MODE,
)
if ROBOKASSA_ENABLE:
robokassa = Robokassa(
merchant_login=ROBOKASSA_LOGIN,
password1=ROBOKASSA_PASSWORD1,
password2=ROBOKASSA_PASSWORD2,
algorithm=HashAlgorithm.md5,
is_test=ROBOKASSA_TEST_MODE,
)
logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN)
logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN)
def generate_payment_link(amount, inv_id, description):
+5 -5
View File
@@ -51,6 +51,11 @@ async def process_callback_pay_stars(
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
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,11 +81,6 @@ async def process_callback_pay_stars(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_stars"
)
)
builder.row(
InlineKeyboardButton(
text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
key_count = await get_key_count(tg_id)
+11 -5
View File
@@ -15,12 +15,12 @@ router = Router()
async def process_callback_view_profile(
callback_query: types.CallbackQuery, state: FSMContext
callback_query: types.CallbackQuery, state: FSMContext, admin: bool
):
chat_id = callback_query.from_user.id
username = callback_query.from_user.full_name
image_path = os.path.join(os.path.dirname(__file__), "pic.jpg")
image_path = os.path.join("img", "pic.jpg")
try:
key_count = await get_key_count(chat_id)
@@ -52,6 +52,10 @@ async def process_callback_view_profile(
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
)
if admin:
builder.row(
InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")
)
builder.row(
InlineKeyboardButton(text="⬅️ Главное меню", callback_data="back_to_menu")
)
@@ -95,7 +99,7 @@ async def invite_handler(callback_query: types.CallbackQuery):
invite_message = invite_message_send(referral_link, referral_stats)
image_path = os.path.join(os.path.dirname(__file__), "pic_invite.jpg")
image_path = os.path.join("img", "pic_invite.jpg")
builder = InlineKeyboardBuilder()
builder.row(
@@ -138,6 +142,8 @@ async def invite_handler(callback_query: types.CallbackQuery):
@router.callback_query(F.data == "view_profile")
async def view_profile_handler(callback_query: types.CallbackQuery, state: FSMContext):
async def view_profile_handler(
callback_query: types.CallbackQuery, state: FSMContext, admin: bool = False
):
await state.clear()
await process_callback_view_profile(callback_query, state)
await process_callback_view_profile(callback_query, state, admin)
+10 -8
View File
@@ -1,7 +1,6 @@
import os
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -15,8 +14,8 @@ from logger import logger
router = Router()
async def send_welcome_message(chat_id: int, trial_status: int):
image_path = os.path.join(os.path.dirname(__file__), "pic.jpg")
async def send_welcome_message(chat_id: int, trial_status: int, admin: bool):
image_path = os.path.join("img", "pic.jpg")
builder = InlineKeyboardBuilder()
if trial_status == 0:
@@ -26,6 +25,10 @@ async def send_welcome_message(chat_id: int, trial_status: int):
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
)
if admin:
builder.row(
InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")
)
builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
)
@@ -52,8 +55,7 @@ async def send_welcome_message(chat_id: int, trial_status: int):
)
@router.message(Command("start"))
async def start_command(message: Message):
async def start_command(message: Message, admin: bool = False):
logger.info(f"Received start command with text: {message.text}")
if "referral_" in message.text:
referrer_tg_id = int(message.text.split("referral_")[1])
@@ -66,7 +68,7 @@ async def start_command(message: Message):
await message.answer("Вы уже зарегистрированы в системе!")
trial_status = await get_trial(message.from_user.id)
await send_welcome_message(message.chat.id, trial_status)
await send_welcome_message(message.chat.id, trial_status, admin)
@router.callback_query(F.data == "connect_vpn")
@@ -150,8 +152,8 @@ async def handle_about_vpn(callback_query: CallbackQuery):
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery):
async def handle_back_to_menu(callback_query: CallbackQuery, admin: bool = False):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
await send_welcome_message(callback_query.from_user.id, trial_status)
await send_welcome_message(callback_query.from_user.id, trial_status, admin)
await callback_query.answer()
+60 -37
View File
@@ -1,5 +1,6 @@
import random
import re
from typing import Optional
import asyncpg
@@ -9,35 +10,48 @@ from logger import logger
def sanitize_key_name(key_name: str) -> str:
"""
Очищает название ключа, оставляя только допустимые символы.
Args:
key_name (str): Исходное название ключа.
Returns:
str: Очищенное название ключа в нижнем регистре.
"""
return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
def generate_random_email():
"""Генерирует случайный набор символов."""
random_string = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=6))
return random_string
async def get_least_loaded_cluster():
def generate_random_email(length: int = 6) -> str:
"""
Функция для получения кластера с наименьшей загрузкой (по количеству ключей).
Возвращает идентификатор кластера с наименьшей загрузкой или первый кластер из конфигурации,
если загруженность не определяется. В случае отсутствия кластеров с номером, возвращает 'cluster1'.
Генерирует случайный email с заданной длиной.
Args:
length (int, optional): Длина случайной строки. По умолчанию 6.
Returns:
str: Сгенерированная случайная строка.
"""
cluster_loads = {}
return "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=length))
conn = await asyncpg.connect(DATABASE_URL)
try:
keys = await conn.fetch("SELECT * FROM keys")
for key in keys:
cluster_id = key["server_id"]
if re.match(r"^cluster\d+$", cluster_id):
if cluster_id not in cluster_loads:
cluster_loads[cluster_id] = 0
cluster_loads[cluster_id] += 1
finally:
await conn.close()
async def get_least_loaded_cluster() -> str:
"""
Определяет кластер с наименьшей загрузкой.
Returns:
str: Идентификатор наименее загруженного кластера.
"""
cluster_loads: dict[str, int] = {}
async with asyncpg.create_pool(DATABASE_URL) as pool:
async with pool.acquire() as conn:
keys = await conn.fetch("SELECT * FROM keys")
for key in keys:
cluster_id = key["server_id"]
if re.match(r"^cluster\d+$", cluster_id):
cluster_loads[cluster_id] = cluster_loads.get(cluster_id, 0) + 1
logger.info(f"Cluster loads: {cluster_loads}")
@@ -51,29 +65,38 @@ async def get_least_loaded_cluster():
logger.info(f"Available clusters from config: {available_clusters}")
if available_clusters:
logger.info(
f"Returning the first available cluster: {available_clusters[0]}"
)
return available_clusters[0]
else:
logger.warning("No valid clusters found in config, returning 'cluster1'.")
return "cluster1"
selected_cluster = available_clusters[0]
logger.info(f"Returning the first available cluster: {selected_cluster}")
return selected_cluster
logger.warning("No valid clusters found in config, returning 'cluster1'.")
return "cluster1"
least_loaded_cluster = min(cluster_loads, key=cluster_loads.get)
logger.info(f"Least loaded cluster selected: {least_loaded_cluster}")
return least_loaded_cluster
async def handle_error(tg_id, callback_query, message):
async def handle_error(
tg_id: int, callback_query: Optional[object] = None, message: str = ""
) -> None:
"""
Обрабатывает ошибку, отправляя сообщение пользователю.
Args:
tg_id (int): Идентификатор пользователя в Telegram.
callback_query (Optional[object], optional): Объект запроса обратного вызова. По умолчанию None.
message (str, optional): Текст сообщения об ошибке. По умолчанию пустая строка.
"""
try:
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
if callback_query and hasattr(callback_query, "message"):
try:
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}")
await bot.send_message(tg_id, message, parse_mode="HTML")

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+29
View File
@@ -0,0 +1,29 @@
from typing import Any, Awaitable, Callable, Dict,Union
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import ADMIN_ID
from logger import logger
class AdminMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
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
if isinstance(admin_ids, list):
return event.from_user.id in admin_ids
return event.from_user.id == admin_ids
except Exception as e:
logger.error(f"Ошибка проверки администратора: {e}")
return False
+7 -6
View File
@@ -14,9 +14,10 @@ class DatabaseMiddleware(BaseMiddleware):
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
session = await asyncpg.connect(DATABASE_URL)
data["session"] = session
try:
return await handler(event, data)
finally:
await session.close()
async with await asyncpg.create_pool(DATABASE_URL) as pool:
async with pool.acquire() as session:
data["session"] = session
try:
return await handler(event, data)
finally:
await pool.release(session)
+19 -12
View File
@@ -1,4 +1,4 @@
from typing import Any, Awaitable, Callable, Dict
from typing import Any, Awaitable, Callable, Dict, Optional
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
@@ -13,23 +13,30 @@ class LoggingMiddleware(BaseMiddleware):
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
user_info = self._extract_user_info(event)
logger.info(
f"Активность пользователя - "
f"ID пользователя: {user_info['user_id']}, "
f"Имя пользователя: {user_info['username']}, "
f"Действие: {user_info['action']}"
)
return await handler(event, data)
def _extract_user_info(self, event: TelegramObject) -> Dict[str, Optional[str]]:
user_id = None
username = None
action = None
if isinstance(event, Message):
user_id = event.from_user.id
username = event.from_user.username
user = event.from_user
user_id = user.id
username = user.username
action = f"Сообщение: {event.text}"
elif isinstance(event, CallbackQuery):
user_id = event.from_user.id
username = event.from_user.username
user = event.from_user
user_id = user.id
username = user.username
action = f"Обратный вызов: {event.data}"
logger.info(
f"Активность пользователя - "
f"ID пользователя: {user_id}, "
f"Имя пользователя: {username}, "
f"Действие: {action}"
)
return await handler(event, data)
return {"user_id": user_id, "username": username, "action": action}
+28
View File
@@ -0,0 +1,28 @@
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, User
from database import upsert_user
class UserMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
if user := data.get("event_from_user"):
await self._process_user(user)
return await handler(event, data)
async def _process_user(self, user: User) -> None:
await upsert_user(
tg_id=user.id,
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
language_code=user.language_code,
is_bot=user.is_bot,
)
-70
View File
@@ -1,70 +0,0 @@
from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class Connection(Base):
"""
Модель для таблицы connections
"""
__tablename__ = "connections"
tg_id = Column(BigInteger, primary_key=True, nullable=False)
balance = Column(Float, nullable=False, default=0.0)
trial = Column(BigInteger, nullable=False, default=0)
# Связь с ключами и рефералами
keys = relationship("Key", back_populates="connection")
referrals_received = relationship(
"Referral", foreign_keys="Referral.referred_tg_id", back_populates="referred"
)
referrals_sent = relationship(
"Referral", foreign_keys="Referral.referrer_tg_id", back_populates="referrer"
)
class Key(Base):
"""
Модель для таблицы keys
"""
__tablename__ = "keys"
tg_id = Column(
BigInteger, ForeignKey("connections.tg_id"), primary_key=True, nullable=False
)
client_id = Column(Text, primary_key=True, nullable=False)
email = Column(Text, nullable=False)
created_at = Column(BigInteger, nullable=False)
expiry_time = Column(BigInteger, nullable=False)
key = Column(Text, nullable=False)
server_id = Column(Text, nullable=False, default="server1")
notified = Column(Boolean, nullable=False, default=False)
notified_24h = Column(Boolean, nullable=False, default=False)
# Связь с подключением
connection = relationship("Connection", back_populates="keys")
class Referral(Base):
"""
Модель для таблицы referrals
"""
__tablename__ = "referrals"
referred_tg_id = Column(
BigInteger, ForeignKey("connections.tg_id"), primary_key=True, nullable=False
)
referrer_tg_id = Column(BigInteger, ForeignKey("connections.tg_id"), nullable=False)
reward_issued = Column(Boolean, default=False)
# Связи с подключениями
referred = relationship(
"Connection", foreign_keys=[referred_tg_id], back_populates="referrals_received"
)
referrer = relationship(
"Connection", foreign_keys=[referrer_tg_id], back_populates="referrals_sent"
)
+2 -1
View File
@@ -28,4 +28,5 @@ yookassa==3.3.0
loguru
aiocryptopay
py3xui
sqlalchemy
sqlalchemy
robokassa
-164
View File
@@ -1,164 +0,0 @@
#!/bin/bash
# Цвета для вывода
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}3x-ui Installation Script by izzzzzi${NC}"
echo "----------------------------------------"
# Запрос данных у пользователя
read -p "Enter your domain (e.g., example.com): " DOMAIN
read -p "Enter desired admin panel port (default: 2053): " PANEL_PORT
read -p "Enter desired sub panel port (default: 2054): " SUB_PANEL_PORT
# Использование значения по умолчанию для порта, если не указано
PANEL_PORT=${PANEL_PORT:-2053}
SUB_PANEL_PORT=${SUB_PANEL_PORT:-2054}
# Создание необходимых директорий
echo -e "${GREEN}Creating directories...${NC}"
mkdir -p nginx/conf.d db cert
# Установка certbot
echo -e "${GREEN}Installing certbot...${NC}"
apt-get update
apt-get install certbot -y
# Остановка nginx если он запущен
echo -e "${GREEN}Stopping nginx if running...${NC}"
docker-compose down 2>/dev/null
systemctl stop nginx 2>/dev/null
# Получение сертификата
echo -e "${GREEN}Obtaining SSL certificate...${NC}"
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d $DOMAIN
# Проверка успешности получения сертификата
if [ ! -d "/etc/letsencrypt/live/$DOMAIN" ]; then
echo "Failed to obtain SSL certificate. Please check your domain settings."
exit 1
fi
# Тестирование автообновления
echo -e "${GREEN}Testing certificate renewal...${NC}"
certbot renew --dry-run
# Создание docker-compose.yml
echo -e "${GREEN}Creating docker-compose.yml...${NC}"
cat > docker-compose.yml << EOL
version: "3"
services:
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- /etc/letsencrypt:/etc/letsencrypt
- /var/lib/letsencrypt:/var/lib/letsencrypt
networks:
- proxy-network
restart: unless-stopped
depends_on:
- 3x-ui
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: ${DOMAIN}
volumes:
- \$PWD/db/:/etc/x-ui/
- \$PWD/cert/:/root/cert/
- /etc/letsencrypt:/etc/letsencrypt:ro
environment:
XRAY_VMESS_AEAD_FORCED: "false"
tty: true
networks:
- proxy-network
restart: unless-stopped
networks:
proxy-network:
driver: bridge
EOL
# Создание конфигурации Nginx
echo -e "${GREEN}Creating Nginx configuration...${NC}"
cat > nginx/conf.d/default.conf << EOL
server {
listen 80;
server_name ${DOMAIN};
location / {
return 301 https://\$host\$request_uri;
}
}
server {
listen 443 ssl;
server_name ${DOMAIN};
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
location / {
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Range \$http_range;
proxy_set_header If-Range \$http_if_range;
proxy_redirect off;
proxy_pass http://3x-ui:${PANEL_PORT};
}
location /sub {
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Range \$http_range;
proxy_set_header If-Range \$http_if_range;
proxy_redirect off;
proxy_pass http://3x-ui:${SUB_PANEL_PORT};
}
}
EOL
# Проверка наличия Docker и Docker Compose
if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then
echo -e "${GREEN}Installing Docker and Docker Compose...${NC}"
curl -fsSL https://get.docker.com | sh
curl -L "https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
fi
# Настройка автообновления сертификатов
echo -e "${GREEN}Setting up certificate auto-renewal...${NC}"
cat > /etc/cron.d/certbot-renew << EOL
0 */12 * * * root certbot renew --quiet --deploy-hook "docker-compose -f $(pwd)/docker-compose.yml restart nginx"
EOL
# Запуск сервисов
echo -e "${GREEN}Starting all services...${NC}"
docker-compose up -d
echo -e "${GREEN}Installation completed!${NC}"
echo -e "${BLUE}You can access your 3x-ui panel at: https://${DOMAIN}${NC}"
echo -e "${YELLOW}Important: Default access to 3x-ui panel - ${NC}"
echo -e "${YELLOW}Login: admin${NC}"
echo -e "${YELLOW}Password: admin${NC}"
echo -e "${YELLOW}For security, it is recommended to change the password at: https://${DOMAIN}/panel/settings${NC}"
echo "Please wait a few minutes for all services to start properly."
echo "Default credentials can be found in the 3x-ui documentation."
EOL
-108
View File
@@ -1,108 +0,0 @@
#!/bin/bash
# Цвета для вывода
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${BLUE}PostgreSQL Installation and Setup Script by izzzzzi${NC}"
echo "----------------------------------------"
# Запрос данных у пользователя
read -p "Enter PostgreSQL port (default: 5432): " DB_PORT
read -p "Enter database name: " DB_NAME
read -p "Enter database user: " DB_USER
read -s -p "Enter database password: " DB_PASS
echo
read -s -p "Confirm database password: " DB_PASS_CONFIRM
echo
# Проверка паролей
if [ "$DB_PASS" != "$DB_PASS_CONFIRM" ]; then
echo -e "${RED}Passwords do not match!${NC}"
exit 1
fi
# Использование значения по умолчанию для порта
DB_PORT=${DB_PORT:-5432}
# Установка PostgreSQL
echo -e "${GREEN}Installing PostgreSQL...${NC}"
apt-get update
apt-get install -y postgresql postgresql-contrib
# Остановка PostgreSQL для изменения конфигурации
systemctl stop postgresql
# Настройка PostgreSQL для внешних подключений
echo -e "${GREEN}Configuring PostgreSQL...${NC}"
# Настройка postgresql.conf
PG_VERSION=$(ls /etc/postgresql/)
PG_CONF="/etc/postgresql/$PG_VERSION/main/postgresql.conf"
PG_HBA="/etc/postgresql/$PG_VERSION/main/pg_hba.conf"
# Backup конфигурационных файлов
cp $PG_CONF "${PG_CONF}.backup"
cp $PG_HBA "${PG_HBA}.backup"
# Изменение postgresql.conf
sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" $PG_CONF
sed -i "s/#port = 5432/port = $DB_PORT/" $PG_CONF
# Изменение pg_hba.conf
cat > $PG_HBA << EOL
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
local all all peer
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
host all all 0.0.0.0/0 md5
EOL
# Запуск PostgreSQL
systemctl start postgresql
systemctl enable postgresql
# Создание пользователя и базы данных
echo -e "${GREEN}Creating database and user...${NC}"
sudo -u postgres psql << EOF
CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';
CREATE DATABASE $DB_NAME OWNER $DB_USER;
ALTER USER $DB_USER WITH SUPERUSER;
EOF
# Настройка файрвола
echo -e "${GREEN}Configuring firewall...${NC}"
if command -v ufw >/dev/null; then
ufw allow $DB_PORT/tcp
ufw status
fi
# Проверка статуса PostgreSQL
systemctl status postgresql --no-pager
echo -e "${GREEN}Installation completed!${NC}"
echo -e "${BLUE}PostgreSQL is running on port: ${DB_PORT}${NC}"
echo -e "${BLUE}Database name: ${DB_NAME}${NC}"
echo -e "${BLUE}Database user: ${DB_USER}${NC}"
echo "You can now connect to your database using:"
echo "psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME"
echo
echo "For remote connections use:"
echo "psql -h YOUR_SERVER_IP -p $DB_PORT -U $DB_USER -d $DB_NAME"
echo
echo -e "${RED}Important: Make sure to save these credentials in a secure place!${NC}"
# Проверка подключения
echo -e "${GREEN}Testing connection...${NC}"
PGPASSWORD=$DB_PASS psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME -c "\conninfo"
# Добавление информации о создании бэкапов
echo
echo "To create a backup, use:"
echo "pg_dump -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME > backup.sql"
echo
echo "To restore from backup, use:"
echo "psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME < backup.sql"