Короче опять дохуя изменений:

1. Оно перво на перво работает
2.Реализовано почти всё вроде из кнопок, осталось ток оплату подписок, выдачу URI и пополнение конечно.
3.Убрал .json конфиг и сделал всё через переменные окружения
This commit is contained in:
2024-12-07 18:05:27 +03:00
parent df50cc5ce7
commit 72d7fdd751
7 changed files with 309 additions and 71 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ config.json
TBot/ TBot/
logs/ logs/
__pycache__/ __pycache__/
.gitignore

View File

@@ -1,32 +1,23 @@
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from motor.motor_asyncio import AsyncIOMotorClient from motor.motor_asyncio import AsyncIOMotorClient
from databases.model import Base from databases.model import Base
from utils.LogCon import setup_logger, load_config
# Загружаем конфигурацию # Настройки PostgreSQL из переменных окружения
config = load_config() POSTGRES_DSN = os.getenv("POSTGRES_URL")
# Настройки PostgreSQL
postgres_user = config['postgreSQL']['username']
postgres_password = config['postgreSQL']['password_DB']
postgres_host = "postgres" # Хост для PostgreSQL в Docker
POSTGRES_DSN = f"postgresql+asyncpg://{postgres_user}:{postgres_password}@{postgres_host}:5432/bot_db"
# Создание движка для PostgreSQL # Создание движка для PostgreSQL
postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) postgres_engine = create_async_engine(POSTGRES_DSN, echo=False)
AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False) AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False)
# Настройки MongoDB # Настройки MongoDB из переменных окружения
mongodb_user = config['mongodb']['mongodb_username'] MONGO_URI = os.getenv("MONGO_URL")
mongodb_password = config['mongodb']['mongodb_password'] DATABASE_NAME = os.getenv("DB_NAME")
mongodb_host = "mongodb" # Хост для MongoDB в Docker
mongodb_uri = f"mongodb://{mongodb_user}:{mongodb_password}@{mongodb_host}:27017"
database_name = config['mongodb']['database_name']
# Создание клиента MongoDB # Создание клиента MongoDB
mongo_client = AsyncIOMotorClient(mongodb_uri) mongo_client = AsyncIOMotorClient(MONGO_URI)
mongo_db = mongo_client[database_name] mongo_db = mongo_client[DATABASE_NAME]
# Инициализация PostgreSQL # Инициализация PostgreSQL
async def init_postgresql(): async def init_postgresql():

View File

@@ -1,53 +1,103 @@
from pymongo import MongoClient import os
from utils.LogCon import setup_logger, load_config from motor.motor_asyncio import AsyncIOMotorClient
import logging
class MongoDBRepository: class MongoDBRepository:
def __init__(self, config_path="config.json"): def __init__(self):
self.config = load_config() # Настройки MongoDB из переменных окружения
self.client = MongoClient(config["mongodb_uri"]) mongo_uri = os.getenv("MONGO_URL")
self.db = self.client[config["database_name"]] database_name = os.getenv("DB_NAME")
self.collection = self.db["vpn_servers"] server_collection = os.getenv("SERVER_COLLECTION", "servers")
plan_collection = os.getenv("PLAN_COLLECTION", "plans")
def add_server(self, server_data): # Подключение к базе данных и коллекциям
"""Добавляет новый VPN сервер в коллекцию.""" self.client = AsyncIOMotorClient(mongo_uri)
result = self.collection.insert_one(server_data) self.db = self.client[database_name]
print(f"VPN сервер добавлен с ID: {result.inserted_id}") self.collection = self.db[server_collection] # Коллекция серверов
self.plans_collection = self.db[plan_collection] # Коллекция тарифных планов
self.logger = logging.getLogger(__name__)
async def add_subscription_plan(self, plan_data):
"""Добавляет новый тарифный план в коллекцию."""
result = await self.plans_collection.insert_one(plan_data)
self.logger.debug(f"Тарифный план добавлен с ID: {result.inserted_id}")
return result.inserted_id return result.inserted_id
def get_server(self, server_id): async def get_subscription_plan(self, plan_id):
"""Получает сервер VPN по его ID.""" """Получает тарифный план по его имени."""
server = self.collection.find_one({"_id": server_id}) plan = await self.plans_collection.find_one({"_id": plan_id})
if server: if plan:
print(f"Найден VPN сервер: {server}") self.logger.debug(f"Найден тарифный план: {plan}")
else: else:
print(f"VPN сервер с ID {server_id} не найден.") self.logger.error(f"Тарифный план {plan_id} не найден.")
return plan
async def add_server(self, server_data):
"""Добавляет новый VPN сервер в коллекцию."""
result = await self.collection.insert_one(server_data)
self.logger.debug(f"VPN сервер добавлен с ID: {result.inserted_id}")
return result.inserted_id
async def get_server(self, server_id):
"""Получает сервер VPN по его ID."""
server = await self.collection.find_one({"_id": server_id})
if server:
self.logger.debug(f"Найден VPN сервер: {server}")
else:
self.logger.debug(f"VPN сервер с ID {server_id} не найден.")
return server return server
def update_server(self, server_id, update_data): async def get_server_with_least_clients(self):
"""Обновляет данные VPN сервера.""" """Возвращает сервер с наименьшим количеством подключенных клиентов."""
result = self.collection.update_one({"_id": server_id}, {"$set": update_data}) pipeline = [
if result.matched_count > 0: {
print(f"VPN сервер с ID {server_id} обновлен.") "$addFields": {
"current_clients": {"$size": {"$ifNull": ["$clients", []]}}
}
},
{
"$sort": {"current_clients": 1}
},
{
"$limit": 1
}
]
result = await self.collection.aggregate(pipeline).to_list(length=1)
if result:
server = result[0]
self.logger.debug(f"Найден сервер с наименьшим количеством клиентов: {server}")
return server
else: else:
print(f"VPN сервер с ID {server_id} не найден.") self.logger.debug("Не найдено серверов.")
return None
async def update_server(self, server_id, update_data):
"""Обновляет данные VPN сервера."""
result = await self.collection.update_one({"_id": server_id}, {"$set": update_data})
if result.matched_count > 0:
self.logger.debug(f"VPN сервер с ID {server_id} обновлен.")
else:
self.logger.debug(f"VPN сервер с ID {server_id} не найден.")
return result.matched_count > 0 return result.matched_count > 0
def delete_server(self, server_id): async def delete_server(self, server_id):
"""Удаляет VPN сервер по его ID.""" """Удаляет VPN сервер по его ID."""
result = self.collection.delete_one({"_id": server_id}) result = await self.collection.delete_one({"_id": server_id})
if result.deleted_count > 0: if result.deleted_count > 0:
print(f"VPN сервер с ID {server_id} удален.") self.logger.debug(f"VPN сервер с ID {server_id} удален.")
else: else:
print(f"VPN сервер с ID {server_id} не найден.") self.logger.debug(f"VPN сервер с ID {server_id} не найден.")
return result.deleted_count > 0 return result.deleted_count > 0
def list_servers(self): async def list_servers(self):
"""Возвращает список всех VPN серверов.""" """Возвращает список всех VPN серверов."""
servers = list(self.collection.find()) servers = await self.collection.find().to_list(length=1000) # Получить до 1000 серверов (можно настроить)
print(f"Найдено {len(servers)} VPN серверов.") self.logger.debug(f"Найдено {len(servers)} VPN серверов.")
return servers return servers
def close_connection(self): async def close_connection(self):
"""Закрывает подключение к базе данных MongoDB.""" """Закрывает подключение к базе данных MongoDB."""
self.client.close() self.client.close()
print("Подключение к MongoDB закрыто.") self.logger.debug("Подключение к MongoDB закрыто.")

View File

@@ -2,10 +2,14 @@ from databases.model import User, Subscription, Transaction, Administrators
from sqlalchemy.future import select from sqlalchemy.future import select
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import desc from sqlalchemy import desc
from dateutil.relativedelta import relativedelta
from datetime import datetime
from utils.panel import PanelInteraction
from databases.mongodb import MongoDBRepository
import random import random
import string import string
import logging import logging
import asyncio
class DatabaseManager: class DatabaseManager:
def __init__(self, session_generator): def __init__(self, session_generator):
@@ -14,18 +18,19 @@ class DatabaseManager:
""" """
self.session_generator = session_generator self.session_generator = session_generator
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.mongo_repo = MongoDBRepository()
async def create_user(self, telegram_id): async def create_user(self, telegram_id: int):
""" """
Создаёт нового пользователя, если его нет. Создаёт нового пользователя, если его нет.
""" """
async for session in self.session_generator(): async for session in self.session_generator():
try: try:
username = self.generate_string(6) username = self.generate_string(6)
result = await session.execute(select(User).where(User.telegram_id == telegram_id)) result = await session.execute(select(User).where(User.telegram_id == int(telegram_id)))
user = result.scalars().first() user = result.scalars().first()
if not user: if not user:
new_user = User(telegram_id=telegram_id, username=username) new_user = User(telegram_id=int(telegram_id), username=username)
session.add(new_user) session.add(new_user)
await session.commit() await session.commit()
return new_user return new_user
@@ -35,7 +40,6 @@ class DatabaseManager:
await session.rollback() await session.rollback()
return "ERROR" return "ERROR"
async def get_user_by_telegram_id(self, telegram_id: int): async def get_user_by_telegram_id(self, telegram_id: int):
""" """
Возвращает пользователя по Telegram ID. Возвращает пользователя по Telegram ID.
@@ -112,6 +116,105 @@ class DatabaseManager:
self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}") self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}")
return "ERROR" return "ERROR"
async def buy_sub(self, telegram_id: str, plan_id: str):
async for session in self.session_generator():
try:
result = await self.create_user(telegram_id)
if not result:
self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.")
return "ERROR"
# Получение тарифного плана из MongoDB
plan = await self.mongo_repo.get_subscription_plan(plan_id)
if not plan:
self.logger.error(f"Тарифный план {plan_id} не найден.")
return "ERROR"
# Проверка достаточности средств для покупки подписки
cost = plan["cost"]
if result.balance >= cost:
result.balance -= cost
await session.commit()
# Создание подписки для пользователя
expiry_date = datetime.now(datetime.timezone.utc) + relativedelta(months=plan["duration_months"])
new_subscription = Subscription(user_id=result.id, vpn_server_id=None, plan=plan_id, expiry_date=expiry_date)
session.add(new_subscription)
await session.commit()
self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id} на план {plan_id}.")
return "OK"
else:
self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_id}.")
return "INSUFFICIENT_FUNDS"
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при покупке подписки {plan_id} для пользователя {telegram_id}: {e}")
await session.rollback()
return "ERROR"
async def add_to_server(self, telegram_id: int):
"""
Метод для добавления пользователя на сервер.
"""
async for session in self.session_generator():
try:
# Получаем подписку пользователя по telegram_id
result = await session.execute(select(Subscription).join(User).where(User.telegram_id == int(telegram_id)))
user_sub = result.scalars().first()
if not user_sub:
self.logger.error(f"Не удалось найти подписку для пользователя с Telegram ID {telegram_id}.")
return "ERROR"
# Получаем информацию о пользователе
user_result = await session.execute(select(User).where(User.telegram_id == telegram_id))
user = user_result.scalars().first()
# Получаем сервер с MongoDB
server = await self.mongo_repo.get_server(user_sub.vpn_server_id)
if not server:
self.logger.error(f"Не удалось найти сервер с ID {user_sub.vpn_server_id}.")
return "ERROR"
# Найдем клиента на сервере по telegram_id
client = None
for client_info in server['clients']:
if client_info['subscriptions']['tgId'] == telegram_id:
client = client_info
break
if not client:
self.logger.error(f"Не удалось найти клиента с Telegram ID {telegram_id} на сервере.")
return "ERROR"
# Доступ к данным сервера для добавления клиента
server_info = server['server']
url_base = f"https://{server_info['ip']}:{server_info['port']}/{server_info['secretKey']}"
login_data = {
'username': server_info['login'],
'password': server_info['password'],
}
# Инициализируем взаимодействие с панелью управления
panel = PanelInteraction(url_base, login_data, self.logger)
# Добавляем клиента на сервер
client_id = client['id']
expiry_date_iso = user_sub.expiry_date.isoformat()
response = await panel.add_client(client_id, expiry_date_iso, user.username)
# Логируем результат
if response == "OK":
self.logger.info(f"Клиент {telegram_id} успешно добавлен на сервер.")
return "OK"
else:
self.logger.error(f"Ошибка при добавлении клиента {telegram_id} на сервер: {response}")
return "ERROR"
except Exception as e:
self.logger.error(f"Ошибка при установке на сервер для пользователя {telegram_id}: {e}")
return "ERROR"
@staticmethod @staticmethod
def generate_string(length): def generate_string(length):

View File

@@ -3,7 +3,7 @@ from aiogram.filters import Command
from databases.postgresql import DatabaseManager from databases.postgresql import DatabaseManager
from databases.model import User, Subscription, Transaction, Administrators from databases.model import User, Subscription, Transaction, Administrators
from databases.db_config import get_postgres_session from databases.db_config import get_postgres_session
from keyboard.keyboards import subhist_keyboard, popup_keyboard, main_keyboard,faq_keyboard,about_tarifs_keyboard, account_keyboard, buy_keyboard,balance_keyboard,guide_keyboard,tarif_Lark_keyboard,tarif_Lark_pro_keyboard,tranhist_keyboard from keyboard.keyboards import subhist_keyboard,tarif_confirm_keyboard, popup_keyboard, main_keyboard,faq_keyboard,about_tarifs_keyboard, account_keyboard, buy_keyboard,balance_keyboard,guide_keyboard,tarif_Lark_keyboard,tarif_Lark_pro_keyboard,tranhist_keyboard
# Инициализируем менеджер базы данных # Инициализируем менеджер базы данных
db_manager = DatabaseManager(get_postgres_session) db_manager = DatabaseManager(get_postgres_session)
@@ -219,6 +219,64 @@ async def faq_callback_handler(callback:types.CallbackQuery):
reply_markup=faq_keyboard() reply_markup=faq_keyboard()
) )
async def lark_tariff_callback_handler(callback: types.CallbackQuery):
"""
Обработчик для выбора тарифа Lark.
"""
data = callback.data.split(":")
tariff_name = data[0]
tariff_class = data[1]
tariff_time = int(data[2])
# Определение окончания для месяцев
if tariff_time == 1:
months = f"{tariff_time} месяц"
elif 2 <= tariff_time <= 4:
months = f"{tariff_time} месяца"
else:
months = f"{tariff_time} месяцев"
text = f"Тариф {tariff_name} на {months}. Продолжите покупку..."
# Рендеринг клавиатуры
keyboard = tarif_confirm_keyboard(tariff_name, tariff_time, tariff_class)
await callback.message.edit_text(text=text, reply_markup=keyboard)
async def confirm_callback_handler(callback: types.CallbackQuery):
"""
Обработчик подтверждения покупки тарифа.
"""
tariff_info = callback.data.split(":")[1].split("_")
tariff_name = tariff_info[0]
tariff_class = tariff_info[1]
tariff_amount = int(tariff_info[2])
sub = await db_manager.buy_sub(callback.from_user.id, f"{tariff_name}_{tariff_class}_{tariff_amount}")
if sub == "ERROR":
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
elif sub == "INSUFFICIENT_FUNDS":
await callback.message.answer(
"Произошла ошибка, не достаточно средств на балансе."
)
await callback.answer()
return
add_to_server = await db_manager.add_to_server(callback.from_user.id)
if add_to_server == "ERROR":
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
# Текст подтверждения на основе тарифа
months_text = f"{tariff_amount} месяцев" if tariff_amount > 1 else f"{tariff_amount} месяц"
text = f"Вы успешно оформили тариф {tariff_name} на {months_text}. Спасибо за покупку!"
await callback.message.edit_text(text=text)
def register_handlers(dp: Dispatcher): def register_handlers(dp: Dispatcher):
""" """
@@ -236,4 +294,5 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(balance_callback_handler, lambda c: c.data == "balance") dp.callback_query.register(balance_callback_handler, lambda c: c.data == "balance")
dp.callback_query.register(guide_callback_handler, lambda c: c.data == "guide") dp.callback_query.register(guide_callback_handler, lambda c: c.data == "guide")
dp.callback_query.register(about_tarifs_callback_handler, lambda c: c.data == "about_tarifs") dp.callback_query.register(about_tarifs_callback_handler, lambda c: c.data == "about_tarifs")
dp.callback_query.register(lark_tariff_callback_handler, lambda c: c.data.startswith("Lark:"))
dp.callback_query.register(confirm_callback_handler, lambda c: c.data.startswith("confirm:"))

View File

@@ -20,6 +20,7 @@ def account_keyboard():
builder.row(InlineKeyboardButton(text="Баланс", callback_data="balance")) builder.row(InlineKeyboardButton(text="Баланс", callback_data="balance"))
builder.row(InlineKeyboardButton(text="Приобрести подписку", callback_data="buy_subscription")) builder.row(InlineKeyboardButton(text="Приобрести подписку", callback_data="buy_subscription"))
builder.row(InlineKeyboardButton(text="Руководство по подключению", callback_data="guide")) builder.row(InlineKeyboardButton(text="Руководство по подключению", callback_data="guide"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="base"))
return builder.as_markup() return builder.as_markup()
@@ -66,9 +67,9 @@ def tarif_Lark_keyboard():
Тариф Lark Тариф Lark
""" """
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Тариф Lark 1 Месяц", callback_data="lark1")) builder.row(InlineKeyboardButton(text="Тариф Lark 1 Месяц", callback_data="Lark:Standart:1"))
builder.row(InlineKeyboardButton(text="Тариф Lark 3 Месяц", callback_data="lark3")) builder.row(InlineKeyboardButton(text="Тариф Lark 3 Месяц", callback_data="Lark:Standart:3"))
builder.row(InlineKeyboardButton(text="Тариф Lark 6 Месяц", callback_data="lark6")) builder.row(InlineKeyboardButton(text="Тариф Lark 6 Месяц", callback_data="Lark:Standart:6"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="buy_subscription")) builder.row(InlineKeyboardButton(text="Назад", callback_data="buy_subscription"))
return builder.as_markup() return builder.as_markup()
@@ -77,9 +78,9 @@ def tarif_Lark_pro_keyboard():
Тариф Lark Pro Тариф Lark Pro
""" """
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Тариф Lark Pro 1 Месяц", callback_data="lark1pro")) builder.row(InlineKeyboardButton(text="Тариф Lark Pro 1 Месяц", callback_data="Lark:Pro:1"))
builder.row(InlineKeyboardButton(text="Тариф Lark Pro 3 Месяц", callback_data="lark3pro")) builder.row(InlineKeyboardButton(text="Тариф Lark Pro 3 Месяц", callback_data="Lark:Pro:3"))
builder.row(InlineKeyboardButton(text="Тариф Lark Pro 6 Месяц", callback_data="lark6pro")) builder.row(InlineKeyboardButton(text="Тариф Lark Pro 6 Месяц", callback_data="Lark:Pro:6"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="buy_subscription")) builder.row(InlineKeyboardButton(text="Назад", callback_data="buy_subscription"))
return builder.as_markup() return builder.as_markup()
@@ -118,3 +119,12 @@ def tranhist_keyboard():
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Назад",callback_data="balance")) builder.row(InlineKeyboardButton(text="Назад",callback_data="balance"))
return builder.as_markup() return builder.as_markup()
def tarif_confirm_keyboard(name,amount,classif):
"""
Подтверждение покупки тарифа
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Подтвердить", callback_data=f"confirm:{name}_{classif}_{amount}"))
builder.row(InlineKeyboardButton(text="Отменить",callback_data="buy_subscription"))
return builder.as_markup()

38
main.py
View File

@@ -1,48 +1,72 @@
import os
import asyncio import asyncio
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from databases.db_config import init_postgresql, init_mongodb, close_connections from databases.db_config import init_postgresql, init_mongodb, close_connections
from aiogram.types import BotCommand from aiogram.types import BotCommand
from utils.LogCon import setup_logger, load_config
from Middleware.anti_spam_middleware import AntiSpamMiddleware from Middleware.anti_spam_middleware import AntiSpamMiddleware
import logging import logging
setup_logger() # Получение токена бота из переменных окружения
logger = logging.getLogger(__name__) BOT_TOKEN = os.getenv("TOKEN")
BOT_TOKEN = load_config()['token'] if not BOT_TOKEN:
raise ValueError("Не задан токен бота. Убедитесь, что переменная окружения 'TOKEN' установлена.")
bot = Bot(token=BOT_TOKEN) bot = Bot(token=BOT_TOKEN)
dp = Dispatcher() dp = Dispatcher()
# Установка middleware для защиты от спама
dp.message.middleware(AntiSpamMiddleware(rate_limit=1)) dp.message.middleware(AntiSpamMiddleware(rate_limit=1))
async def set_commands(): async def set_commands():
"""Устанавливает команды для бота."""
commands = [ commands = [
BotCommand(command="/start", description="Запустить бота"), BotCommand(command="/start", description="Запустить бота"),
] ]
await bot.set_my_commands(commands) await bot.set_my_commands(commands)
async def on_startup(): async def on_startup():
"""Действия при запуске бота."""
# Инициализация баз данных
await init_mongodb() await init_mongodb()
await init_postgresql()
# Установка команд бота
await set_commands() await set_commands()
print("Бот запущен!")
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Бот запущен!")
async def on_shutdown(): async def on_shutdown():
"""Действия при остановке бота."""
# Закрытие подключений к базам данных
await close_connections() await close_connections()
# Закрытие сессии бота
await bot.session.close() await bot.session.close()
print("Бот остановлен.") print("Бот остановлен.")
async def main(): async def main():
"""Основной цикл работы бота."""
from handlers.handlers import register_handlers from handlers.handlers import register_handlers
register_handlers(dp) register_handlers(dp) # Регистрация хендлеров
await init_postgresql() # Убедитесь, что таблицы создаются здесь
await on_startup() await on_startup()
try: try:
# Запуск polling
await dp.start_polling(bot) await dp.start_polling(bot)
finally: finally:
# Действия при завершении работы
await on_shutdown() await on_shutdown()
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())