37 Commits

Author SHA1 Message Date
unknown
54530c8521 Исправлен субскрпи 2025-12-06 14:33:32 +03:00
unknown
a4d863c8d3 Исправил пару вещей 2025-12-06 14:19:58 +03:00
unknown
206e5b2fd9 Исправленные саппорты 2025-12-06 14:00:58 +03:00
unknown
ebb523acec помогите.. функции.. бот.. проекты... ларк(( 2025-12-06 13:54:59 +03:00
unknown
8050855b33 исправил одну строку 2025-12-06 13:38:48 +03:00
unknown
26b2bd5fdf Исправил ошибку, надеюсь 2025-12-06 12:16:25 +03:00
unknown
ead48389b7 Много чего изменил, надеюсь, запушил правильно 2025-12-06 11:49:36 +03:00
unknown
33414dee65 Исправил много чего в фронте 2025-12-06 11:38:34 +03:00
unknown
709e8f09eb Чет исправил, я забыл 2025-12-02 14:28:03 +03:00
root
51645096d3 Мелкое изменение что бы работали подписк 2025-12-01 16:36:21 +03:00
unknown
00edfdbd19 вова помоги (merge front-misha-ui) 2025-12-01 12:57:44 +03:00
unknown
94df5ee9a5 вова помоги 2025-12-01 12:50:24 +03:00
root
0d9a86ee54 Для криптобота так же пофиксил 2025-11-30 18:13:00 +03:00
root
12c60b884a Ошибка в способе передачи данных, нужен был query 2025-11-30 16:57:08 +03:00
root
6276490ee1 Изменил call_api добавив тип данных и преобразую во float amount 2025-11-30 16:51:47 +03:00
root
3e976d9960 Исправил ошибку с id и изменил call_api что бы он работал с billing 2025-11-30 16:41:40 +03:00
root
77ca27a09c Получение ссылки на оплату юкассы и криптобота 2025-11-30 16:33:04 +03:00
root
76daf3244f Временные изменения что бы тестировать бекенд, в дальнейшем должен быть заменён на то что делает Михаил 2025-11-27 20:57:11 +03:00
unknown
d1417653c3 Фикс keyboards: убрал дубли, добавил account/popup/payment_methods 2025-11-25 22:40:42 +03:00
unknown
cbf113f7bb Реферальная программа: кнопка и экран 2025-11-25 20:20:38 +03:00
unknown
a572c90194 UI: карточки подписок и выбор тарифов 2025-11-25 19:18:22 +03:00
unknown
f042cc2f9e UI: тарифы Basic/Pro/Family и меню оплат 2025-11-25 15:40:33 +03:00
unknown
779f13b98e Обновлён профиль и логика выбора способа оплаты 2025-11-25 07:55:27 +03:00
unknown
c7ded9e499 Обновлён список сумм пополнения 2025-11-25 07:32:55 +03:00
unknown
cda94583ed Сделал две кнопки, как у меня 2025-11-25 00:11:33 +03:00
unknown
79307b9791 Обновлён текст профиля и статус подписки 2025-11-24 23:48:29 +03:00
unknown
c1021ae8f1 Обновил логику бота / фиксы 2025-11-23 16:50:19 +03:00
Disledg
5b29fb25c0 Ошибку исправил в выдаче конфигурации при покупке 2025-01-07 14:10:12 +03:00
Disledg
675103ac67 Мелкие фиксы от игоря 2025-01-07 11:55:47 +03:00
Disledg
a9a137b766 Много переделал 2025-01-07 08:28:59 +03:00
Disledg
5997d67640 Возможность тикеты создавать 2025-01-04 21:19:39 +03:00
Disledg
9c00529216 Переделал меню и под роуты адаптировал 2024-12-28 21:34:08 +03:00
Disledg
379da6cbba Перенос части логики на бекенд rest api и изменение логики работы бота так же из за бекенда 2024-12-24 16:43:55 +03:00
4151e2a546 Save 2024-12-20 21:19:38 +03:00
33b502fd86 Удалил мусор и сертификаты вроде добавил 2024-12-19 23:01:48 +03:00
197e291bdc Pizdec 2024-12-17 18:23:06 +03:00
13d00f3bde Я еблан 2024-12-17 18:21:34 +03:00
23 changed files with 2130 additions and 1343 deletions

8
.gitignore vendored
View File

@@ -1,6 +1,6 @@
config.json
TBot/
logs/
logs/*
*.code-workspace
__pycache__/
.gitignore
docker-compose.yml
handlers.py
*.pyc

View File

@@ -4,6 +4,18 @@ FROM python:3.12-slim
# Устанавливаем рабочую директорию
WORKDIR /app
# Устанавливаем необходимые пакеты и локаль
RUN apt-get update && apt-get install -y --no-install-recommends \
locales && \
echo "ru_RU.UTF-8 UTF-8" > /etc/locale.gen && \
locale-gen && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Устанавливаем переменные окружения для локали
ENV LANG ru_RU.UTF-8
ENV LANGUAGE ru_RU:ru
ENV LC_ALL ru_RU.UTF-8
# Копируем файлы проекта
COPY . .

View File

@@ -0,0 +1,14 @@
{
"folders": [
{
"path": "."
},
{
"path": "../Bot"
},
{
"path": "../bot/Lark_VPN_Bot"
}
],
"settings": {}
}

View File

@@ -1,65 +0,0 @@
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from motor.motor_asyncio import AsyncIOMotorClient
from databases.model import Base
# Настройки PostgreSQL из переменных окружения
POSTGRES_DSN = os.getenv("POSTGRES_URL")
# Создание движка для PostgreSQL
postgres_engine = create_async_engine(POSTGRES_DSN, echo=False)
AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False)
# Настройки MongoDB из переменных окружения
MONGO_URI = os.getenv("MONGO_URL")
DATABASE_NAME = os.getenv("DB_NAME")
# Создание клиента MongoDB
mongo_client = AsyncIOMotorClient(MONGO_URI)
mongo_db = mongo_client[DATABASE_NAME]
# Инициализация PostgreSQL
async def init_postgresql():
"""
Инициализация подключения к PostgreSQL.
"""
try:
async with postgres_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
print("PostgreSQL connected.")
except Exception as e:
print(f"Failed to connect to PostgreSQL: {e}")
# Инициализация MongoDB
async def init_mongodb():
"""
Проверка подключения к MongoDB.
"""
try:
# Проверяем подключение к MongoDB
await mongo_client.admin.command("ping")
print("MongoDB connected.")
except Exception as e:
print(f"Failed to connect to MongoDB: {e}")
# Получение сессии PostgreSQL
async def get_postgres_session():
"""
Асинхронный генератор сессий PostgreSQL.
"""
async with AsyncSessionLocal() as session:
yield session
# Закрытие соединений
async def close_connections():
"""
Закрытие всех соединений с базами данных.
"""
# Закрытие PostgreSQL
await postgres_engine.dispose()
print("PostgreSQL connection closed.")
# Закрытие MongoDB
mongo_client.close()
print("MongoDB connection closed.")

View File

@@ -1,60 +0,0 @@
from sqlalchemy import Column, String, Numeric, DateTime, Boolean, ForeignKey, Integer
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
from datetime import datetime
import uuid
Base = declarative_base()
def generate_uuid():
return str(uuid.uuid4())
"""Пользователи"""
class User(Base):
__tablename__ = 'users'
id = Column(String, primary_key=True, default=generate_uuid)
telegram_id = Column(Integer, unique=True, nullable=False)
username = Column(String)
balance = Column(Numeric(10, 2), default=0.0)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
subscriptions = relationship("Subscription", back_populates="user")
transactions = relationship("Transaction", back_populates="user")
admins = relationship("Administrators", back_populates="user")
"""Подписки"""
class Subscription(Base):
__tablename__ = 'subscriptions'
id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey('users.id'))
vpn_server_id = Column(String)
plan = Column(String)
expiry_date = Column(DateTime)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user = relationship("User", back_populates="subscriptions")
"""Транзакции"""
class Transaction(Base):
__tablename__ = 'transactions'
id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey('users.id'))
amount = Column(Numeric(10, 2))
transaction_type = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="transactions")
"""Администраторы"""
class Administrators(Base):
__tablename__ = 'admins'
id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey('users.id'))
user = relationship("User", back_populates="admins")

View File

@@ -1,103 +0,0 @@
import os
from motor.motor_asyncio import AsyncIOMotorClient
import logging
class MongoDBRepository:
def __init__(self):
# Настройки MongoDB из переменных окружения
mongo_uri = os.getenv("MONGO_URL")
database_name = os.getenv("DB_NAME")
server_collection = os.getenv("SERVER_COLLECTION", "servers")
plan_collection = os.getenv("PLAN_COLLECTION", "plans")
# Подключение к базе данных и коллекциям
self.client = AsyncIOMotorClient(mongo_uri)
self.db = self.client[database_name]
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
async def get_subscription_plan(self, plan_id):
"""Получает тарифный план по его имени."""
plan = await self.plans_collection.find_one({"_id": plan_id})
if plan:
self.logger.debug(f"Найден тарифный план: {plan}")
else:
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_name: str):
"""Получает сервер VPN по его ID."""
server = await self.collection.find_one({"server.name": server_name})
if server:
self.logger.debug(f"Найден VPN сервер: {server}")
else:
self.logger.debug(f"VPN сервер с ID {server_name} не найден.")
return server
async def get_server_with_least_clients(self):
"""Возвращает сервер с наименьшим количеством подключенных клиентов."""
pipeline = [
{
"$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:
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
async def delete_server(self, server_id):
"""Удаляет VPN сервер по его ID."""
result = await self.collection.delete_one({"_id": server_id})
if result.deleted_count > 0:
self.logger.debug(f"VPN сервер с ID {server_id} удален.")
else:
self.logger.debug(f"VPN сервер с ID {server_id} не найден.")
return result.deleted_count > 0
async def list_servers(self):
"""Возвращает список всех VPN серверов."""
servers = await self.collection.find().to_list(length=1000) # Получить до 1000 серверов (можно настроить)
self.logger.debug(f"Найдено {len(servers)} VPN серверов.")
return servers
async def close_connection(self):
"""Закрывает подключение к базе данных MongoDB."""
self.client.close()
self.logger.debug("Подключение к MongoDB закрыто.")

View File

@@ -1,216 +0,0 @@
from databases.model import User, Subscription, Transaction, Administrators
from sqlalchemy.future import select
from sqlalchemy.exc import SQLAlchemyError
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 string
import logging
import asyncio
class DatabaseManager:
def __init__(self, session_generator):
"""
Инициализация с асинхронным генератором сессий (например, get_postgres_session).
"""
self.session_generator = session_generator
self.logger = logging.getLogger(__name__)
self.mongo_repo = MongoDBRepository()
async def create_user(self, telegram_id: int):
"""
Создаёт нового пользователя, если его нет.
"""
async for session in self.session_generator():
try:
username = self.generate_string(6)
result = await session.execute(select(User).where(User.telegram_id == int(telegram_id)))
user = result.scalars().first()
if not user:
new_user = User(telegram_id=int(telegram_id), username=username)
session.add(new_user)
await session.commit()
return new_user
return user
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}")
await session.rollback()
return "ERROR"
async def get_user_by_telegram_id(self, telegram_id: int):
"""
Возвращает пользователя по Telegram ID.
"""
async for session in self.session_generator():
try:
result = await session.execute(select(User).where(User.telegram_id == telegram_id))
return result.scalars().first()
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}")
return None
async def add_transaction(self, user_id: int, amount: float):
"""
Добавляет транзакцию для пользователя.
"""
async for session in self.session_generator():
try:
transaction = Transaction(user_id=user_id, amount=amount)
session.add(transaction)
await session.commit()
except SQLAlchemyError as e:
self.logger.error(f"Ошибка добавления транзакции для пользователя {user_id}: {e}")
await session.rollback()
async def update_balance(self, telegram_id: int, amount: float):
"""
Обновляет баланс пользователя и добавляет транзакцию.
"""
async for session in self.session_generator():
try:
result = await session.execute(select(User).where(User.telegram_id == telegram_id))
user = result.scalars().first()
if user:
user.balance += int(amount)
await self.add_transaction(user.id, amount)
await session.commit()
else:
self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.")
return "ERROR"
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при обновлении баланса: {e}")
await session.rollback()
return "ERROR"
async def last_subscription(self, user_id: int):
"""
Возвращает список подписок пользователя.
"""
async for session in self.session_generator():
try:
result = await session.execute(
select(Subscription)
.where(Subscription.user_id == user_id)
.order_by(desc(Subscription.created_at))
)
return result.scalars().all()
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении последней подписки пользователя {user_id}: {e}")
return "ERROR"
async def last_transaction(self, user_id: int):
"""
Возвращает список транзакций пользователя.
"""
async for session in self.session_generator():
try:
result = await session.execute(
select(Transaction)
.where(Transaction.user_id == user_id)
.order_by(desc(Transaction.created_at))
)
transactions = result.scalars().all()
return transactions
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}")
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 = int(plan["price"])
if result.balance >= cost:
result.balance -= cost
await session.commit()
# Создание подписки для пользователя
expiry_date = datetime.utcnow() + relativedelta(months=plan["duration_months"])
server = await self.mongo_repo.get_server_with_least_clients()
self.logger.info(f"{server}")
new_subscription = Subscription(user_id=result.id, vpn_server_id=str(server['server']["name"]), 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"
# Доступ к данным сервера для добавления клиента
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)
expiry_date_iso = user_sub.expiry_date.isoformat()
response = await panel.add_client(user.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
def generate_string(length):
"""
Генерирует случайную строку заданной длины.
"""
characters = string.ascii_lowercase + string.digits
return ''.join(random.choices(characters, k=length))

View File

@@ -1,56 +0,0 @@
networks:
bot_network:
driver: bridge
volumes:
mongo_data:
postgres_data:
logs_data:
services:
mongodb:
networks:
- bot_network
image: mongo:latest
container_name: mongodb
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: USERNAME
MONGO_INITDB_ROOT_PASSWORD: PASSWORD
volumes:
- mongo_data:/data/db
postgres:
networks:
- bot_network
image: postgres:latest
container_name: postgres
ports:
- "5432:5432"
environment:
POSTGRES_USER: USER
POSTGRES_PASSWORD: PASSWORD
POSTGRES_DB: DB_NAME
volumes:
- postgres_data:/var/lib/postgresql/data
bot:
networks:
- bot_network
build:
context: .
container_name: telegram_bot
environment:
TOKEN: "TOKEN"
POSTGRES_URL: "postgresql://USER:PASSWORD@postgres:5432/DB_NAME"
MONGO_URL: "mongodb://USERNAME:PASSWORD@mongodb:27017"
DB_NAME: "MONGO_DB_NAME"
SERVER_COLLECTION: "COLLECTION IN MONGO DB "
PLAN_COLLECTION: "COLLECTION IN MONGO DB"
volumes:
- logs_data:/app/logs
depends_on:
- postgres
- mongodb
command: ["python", "main.py"]

View File

@@ -1,3 +1,4 @@
networks:
bot_network:
driver: bridge
@@ -8,6 +9,7 @@ volumes:
logs_data:
services:
# MongoDB
mongodb:
networks:
- bot_network
@@ -21,6 +23,7 @@ services:
volumes:
- mongo_data:/data/db
# PostgreSQL
postgres:
networks:
- bot_network
@@ -35,22 +38,39 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
bot:
# Бэкенд (FastAPI)
backend:
networks:
- bot_network
build:
context: .
container_name: telegram_bot
context: ./Flask-Backend-All
dockerfile: Dockerfile
container_name: backend
environment:
TOKEN: "8104061818:AAGJ6H1PTFmfJm-_mZqGv7EnHxGl4dZndnU"
POSTGRES_URL: "postgresql+asyncpg://AH3J9GSPBYOP:uPS9?y~mcu2@postgres:5432/bot_db"
MONGO_URL: "mongodb://root:itOj4CE2miKR@mongodb:27017"
DB_NAME: "MongoDBSub&Ser"
SERVER_COLLECTION: "servers"
PLAN_COLLECTION: "plans"
volumes:
- logs_data:/app/logs
BASE_URL: "http://backend:8000"
ports:
- "8000:8000"
depends_on:
- postgres
- mongodb
command: ["python", "main.py"]
# Telegram Bot
bot:
networks:
- bot_network
build:
context: ./Lark_VPN_Bot
dockerfile: Dockerfile
container_name: telegram_bot
environment:
BASE_URL: "http://backend:8000"
TOKEN: "8104061818:AAGJ6H1PTFmfJm-_mZqGv7EnHxGl4dZndnU"
volumes:
- logs_data:/app/logs
depends_on:
- backend

16
handlers/__init__.py Normal file
View File

@@ -0,0 +1,16 @@
from .start import router as start_router
from .profile import router as profile_router
from .subscriptions import router as subscriptions_router
from .support import router as support_router
from .referrals import router as referrals_router
# Список всех роутеров бота
routers = [
start_router,
profile_router,
subscriptions_router,
support_router,
referrals_router,
]
__all__ = ["routers"]

View File

@@ -1,327 +0,0 @@
from aiogram import types, Dispatcher
from aiogram.filters import Command
from databases.postgresql import DatabaseManager
from databases.model import User, Subscription, Transaction, Administrators
from databases.db_config import get_postgres_session
from keyboard.keyboards import subhist_keyboard,confirm_popup_keyboard,tarif_confirm_keyboard, popup_keyboard, main_keyboard,faq_keyboard, account_keyboard, buy_keyboard,balance_keyboard,guide_keyboard,tarif_Lark_keyboard,tarif_Lark_pro_keyboard,tranhist_keyboard
# Инициализируем менеджер базы данных
db_manager = DatabaseManager(get_postgres_session)
async def popup_command(message: types.Message):
"""
Обработчик команды для отправки popup-сообщения.
"""
await message.answer("HAHAHHAHAHAHAHHAHA", reply_markup=popup_keyboard())
async def subhist_command(message: types.Message):
"""
Обработчик команды для отправки истории подписок.
"""
await message.answer("subhist", reply_markup=subhist_keyboard())
async def start_command(message: types.Message):
"""
Обработчик команды /start.
"""
await message.answer(f"""Приветствуем в рядах птенец {message.from_user.username}🐣
\nСкорее пополни баланс, приобрети подписку и получай доступ к TikTok, YouTube, Discord, Instagram на всех устройствах без ограничений ❕❕❕
\nОзнакомься с руководством по пользованию и выбери подходящий тариф 🦅
\nСледи за акциями, спец-предложениями, а также розыгрышами по ссылке ниже
\n👇👇👇
\nhttps://t.me/+0z5xqn3F1m02OTJi
\nС любовью ваши пернатые разработчики 🤍🤍🤍""", reply_markup=main_keyboard())
async def start_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="base".
"""
await callback.message.edit_text(
f"""Приветствуем в рядах птенец {callback.from_user.username}🐣
\nСкорее пополни баланс, приобрети подписку и получай доступ к TikTok, YouTube, Discord, Instagram на всех устройствах без ограничений ❕❕❕
\nОзнакомься с руководством по пользованию и выбери подходящий тариф 🦅
\nСледи за акциями, спец-предложениями, а также розыгрышами по ссылке ниже
\n👇👇👇
\nhttps://t.me/+0z5xqn3F1m02OTJi
\nС любовью ваши пернатые разработчики 🤍🤍🤍""",
reply_markup=main_keyboard()
)
async def profile_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="profile".
"""
user = await db_manager.create_user(telegram_id=callback.from_user.id)
if user == "ERROR":
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
if user:
text = f"""Ваш профиль:\nID: {user.username}\nБаланс: {user.balance}"""
await callback.message.edit_text(
text,
reply_markup=account_keyboard()
)
else:
await callback.message.edit_text("Вы еще не зарегистрированы.")
await callback.answer()
async def balance_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="balance".
"""
user = await db_manager.create_user(telegram_id=callback.from_user.id)
if user == "ERROR":
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
if user:
await callback.message.edit_text(
f"Ваш баланс: {user.balance} ₽. Выберите сумму для пополнения 🐥",
reply_markup=balance_keyboard()
)
else:
await callback.message.edit_text("Вы еще не зарегистрированы.")
await callback.answer()
async def popup_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="popup".
"""
user = await db_manager.create_user(telegram_id=callback.from_user.id)
if user == "ERROR":
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
if user:
await callback.message.edit_text(
f"Работает в режиме теста!!!",
reply_markup=popup_keyboard()
)
else:
await callback.message.edit_text("Вы еще не зарегистрированы.")
await callback.answer()
async def tranhist_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="tranhist".
"""
user = await db_manager.create_user(callback.from_user.id)
trans = await db_manager.last_transaction(user.id)
if trans == "ERROR":
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
if not trans:
await callback.message.edit_text(
"У вас нет транзакций. Пожалуйста, пополните баланс.",
reply_markup=tranhist_keyboard()
)
await callback.answer()
return
result = "Ваши транзакции:\n"
for count, tran in enumerate(trans, start=1):
result += f"{count}. Сумма: {tran.amount}, Дата: {tran.created_at}\n"
await callback.message.edit_text(
result,
reply_markup=tranhist_keyboard()
)
await callback.answer()
async def subhist_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="subhist".
"""
user = await db_manager.create_user(callback.from_user.id)
subs = await db_manager.last_subscription(user.id)
if subs == "ERROR":
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
if subs is None:
await callback.message.edit_text(
f"Ты хули тут забыл, ты ж не покупаешь нихуя",
reply_markup=account_keyboard()
)
await callback.answer()
return
result = ""
count = 0
for sub in subs:
if count > 0:
result += f"Последняя подписка истекает: {sub.expiry_date}\n"
count += 1
result += f"{count}. Истекла {sub.expiry_date}"
count += 1
if subs:
await callback.message.edit_text(
result,
reply_markup=account_keyboard()
)
else:
await callback.message.edit_text("Вы еще не зарегистрированы.")
await callback.answer()
async def buy_subscription_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="buy_subscription".
"""
await callback.message.edit_text(
f"Ознакомься с условиями в вкладке \"О тарифах\" и выбери подходящий 🦅",
reply_markup=buy_keyboard()
)
async def guide_callback_handler(callback:types.CallbackQuery):
"""
Обработчик callback_query с data="guide".
"""
await callback.message.edit_text(
f"Руководство по использованию продкута что мы высрали;)",
reply_markup=guide_keyboard()
)
async def subs_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="subs".
"""
await callback.message.edit_text(
f"Подписки птенчик",
reply_markup=tarif_Lark_keyboard()
)
async def subs_pro_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="subs_pro".
"""
await callback.message.edit_text(
f"Подписки птенчик ПРО",
reply_markup=tarif_Lark_pro_keyboard()
)
# async def about_tarifs_callback_handler(callback: types.CallbackQuery):
# """
# Обработчик callback_query с data="about_tarifs".
# """
# await callback.message.edit_text(
# f"Бла бла бла, хуйня на хуйне",
# reply_markup=about_tarifs_keyboard()
# )
async def faq_callback_handler(callback:types.CallbackQuery):
"""
Обработчик callback_query с data="faq".
"""
await callback.message.edit_text(
f"FAQ YOU",
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 popup_confirm_callback_handler(callback: types.CallbackQuery):
"""
Обработчик подтверждения пополнения баланса.
"""
data = callback.data.split(":")
popup_info = data[1]
result = await db_manager.update_balance(callback.from_user.id,popup_info)
if result == "ERROR":
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
text = f"Вы пополнили свой баланс на {popup_info}. P.S. Мы завтра закрываемся"
await callback.message.edit_text(text=text, reply_markup=confirm_popup_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):
"""
Регистрация хэндлеров в диспетчере.
"""
dp.callback_query.register(popup_callback_handler, lambda c: c.data == "popup")
dp.callback_query.register(start_callback_handler, lambda c: c.data == "base")
dp.callback_query.register(subs_callback_handler, lambda c: c.data == "subs")
dp.callback_query.register(subs_pro_callback_handler, lambda c: c.data == "subs_pro")
dp.callback_query.register(faq_callback_handler, lambda c: c.data == "faq")
dp.callback_query.register(profile_callback_handler, lambda c: c.data == "profile")
dp.callback_query.register(tranhist_callback_handler, lambda c: c.data == "tranhist")
dp.callback_query.register(buy_subscription_callback_handler, lambda c: c.data == "buy_subscription")
dp.message.register(start_command, Command("start"))
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(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:"))
dp.callback_query.register(popup_confirm_callback_handler, lambda c: c.data.startswith("popup:"))

512
handlers/profile.py Normal file
View File

@@ -0,0 +1,512 @@
# Профиль. последнее изменение 24.11.2025
from aiogram import Router, types, F
from aiogram.types import (
CallbackQuery,
LabeledPrice,
Message,
PreCheckoutQuery,
)
import logging
from datetime import datetime
from aiogram.enums.parse_mode import ParseMode
import locale
from instences.config import BASE_URL_FASTAPI
import aiohttp
from keyboard.keyboards import (
account_keyboard,
popup_keyboard,
tranhist_keyboard,
confirm_popup_keyboard,
guide_keyboard,
balance_keyboard,
payment_methods_keyboard,
)
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
router = Router()
logger = logging.getLogger(__name__)
async def call_api(method, endpoint, data=None, base_url=BASE_URL_FASTAPI):
"""
Выполняет HTTP-запрос к FastAPI.
"""
url = f"{base_url}{endpoint}"
logger.info(f"Инициализация запроса: {method} {url} с данными {data}")
try:
async with aiohttp.ClientSession() as session:
async with session.request(
method,
url,
json=data,
headers={"Content-Type": "application/json"}
) as response:
logger.info(
f"Получен ответ от {url}: статус {response.status}"
)
if response.status in {200, 201}:
result = await response.json()
logger.debug(f"Ответ JSON: {result}")
return result
if response.status == 404:
logger.debug(f"Код {response.status}, возвращаю ничего")
return None
logger.error(
f"Ошибка в запросе: статус {response.status}, причина {response.reason}"
)
return "ERROR"
except Exception as e:
logger.exception(f"Исключение при выполнении запроса к {url}: {e}")
return "ERROR"
@router.callback_query(lambda callback: callback.data == "profile")
async def profile_callback_handler(callback: CallbackQuery):
"""
Профиль пользователя.
Логика работы с API сохранена, изменён только текст/визуал.
"""
try:
user_data = await call_api("GET", f"/user/{callback.from_user.id}")
if not user_data:
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
# Последняя подписка пользователя
sub_data = await call_api(
"GET", f"/subscription/{user_data['telegram_id']}/last"
)
if sub_data == "ERROR" or not isinstance(sub_data, dict):
sub_data = None
username = callback.from_user.username or "-"
balance = user_data.get("balance", 0)
# Статус подписки: Активна / Нет активных
sub_status = "⚫ Нет активных"
if sub_data:
end_date_str = sub_data.get("end_date") # Исправил
if end_date_str:
try:
is_expired = (
datetime.fromisoformat(end_date_str) < datetime.now()
)
except ValueError:
is_expired = True
else:
is_expired = True
if not is_expired:
sub_status = "🟢 Активна"
text = (
"🥚 <b>Профиль</b>\n\n"
f"Пользователь: @{username}\n"
f"Баланс: {balance}\n"
f"Статус подписки: {sub_status}\n\n"
"Выберите действие:"
)
await callback.message.edit_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=account_keyboard(),
)
except Exception as e:
logger.exception(f"Ошибка в обработчике профиля: {e}")
await callback.message.answer("Произошла ошибка. Попробуйте позже.")
finally:
await callback.answer()
@router.callback_query(lambda callback: callback.data == "balance")
async def balance_callback_handler(callback: CallbackQuery):
"""
При нажатии «Пополнить баланс» показываем выбор суммы пополнения.
"""
await callback.message.edit_text(
"💳 Выберите сумму пополнения:",
reply_markup=popup_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "popup")
async def popup_callback_handler(callback: CallbackQuery):
"""
Обработчик callback_query для выбора суммы пополнения.
"""
user = await call_api("GET", f"/user/{callback.from_user.id}")
if not user:
await callback.message.answer(
"Произошла ошибка, попробуйте позже или свяжитесь с администрацией."
)
await callback.answer()
return
await callback.message.edit_text(
"Выбери сумму для пополнения баланса.",
reply_markup=popup_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "tranhist")
async def tranhist_callback_handler(callback: CallbackQuery):
"""
Обработчик callback_query для истории транзакций.
(Логику и формат Markdown_V2 не трогаем, чтобы не поймать новые баги)
"""
user_data = await call_api("GET", f"/user/{callback.from_user.id}")
if not user_data:
await callback.message.edit_text("Вы еще не зарегистрированы.")
await callback.answer()
return
try:
transactions = await call_api(
"GET", f"/user/{user_data['telegram_id']}/transactions"
)
if not transactions:
await callback.message.edit_text(
"У вас нет транзакций.", reply_markup=tranhist_keyboard()
)
await callback.answer()
return
result = "Ваши транзакции:```\n"
for count, tran in enumerate(transactions, start=1):
dt = datetime.fromisoformat(tran["created_at"]).strftime(
"%d.%m.%Y %H:%M:%S"
)
result += f"{count}. Сумма: {tran['amount']}, Дата: {dt}\n"
if len(result) > 4000:
result += "...\nСлишком много транзакций для отображения."
break
result += "```"
await callback.message.edit_text(
result,
parse_mode=ParseMode.MARKDOWN_V2,
reply_markup=tranhist_keyboard(),
)
except Exception as e:
logger.error(f"Ошибка обработки транзакций: {e}")
await callback.message.edit_text(
"Произошла ошибка. Попробуйте позже."
)
finally:
await callback.answer()
@router.callback_query(lambda callback: callback.data.startswith("popup:"))
async def popup_confirm_callback_handler(callback: CallbackQuery):
"""
После выбора суммы показываем варианты оплаты.
Разрешены только суммы 200, 300, 600, 1000 ₽.
"""
try:
_, amount_raw = callback.data.split(":", maxsplit=1)
amount = int(float(amount_raw))
except Exception:
await callback.message.answer("Некорректная сумма пополнения.")
await callback.answer()
return
if amount not in {200, 300, 600, 1000}:
await callback.message.answer(
"Эта сумма пополнения недоступна. Выбери вариант от 200 до 1000 ₽."
)
await callback.answer()
return
text = (
f"💰 Сумма пополнения: {amount}\n\n"
"Выбери способ оплаты:"
)
await callback.message.edit_text(
text=text,
reply_markup=payment_methods_keyboard(amount),
)
await callback.answer()
# ===== Telegram Stars =====
@router.callback_query(lambda callback: callback.data.startswith("method_stars_"))
async def method_stars_handler(callback: CallbackQuery):
"""
Оплата через Telegram Stars.
Формируем invoice прямо из бота, без отдельного биллинга.
"""
try:
amount_str = callback.data.split("_")[-1]
amount_rub = int(float(amount_str))
except Exception:
await callback.message.answer("Некорректная сумма для оплаты.")
await callback.answer()
return
# Внутренний payload, чтобы при успешной оплате понять, что это пополнение баланса
payload = f"stars_topup:{callback.from_user.id}:{amount_rub}"
# 1 ₽ == 1 Star, если нужна другая конвертация — меняй тут
stars_amount = amount_rub
prices = [
LabeledPrice(
label=f"Пополнение баланса на {amount_rub}",
amount=stars_amount,
)
]
try:
await callback.message.answer_invoice(
title="Пополнение баланса Lark VPN",
description=(
f"Пополнение баланса на {amount_rub} ₽ через Telegram Stars.\n\n"
"После успешной оплаты баланс будет зачислен автоматически."
),
payload=payload,
provider_token="", # для Stars провайдер пустой
currency="XTR",
prices=prices,
)
await callback.answer()
except Exception as e:
logger.exception(f"Ошибка при отправке invoice Telegram Stars: {e}")
await callback.message.answer(
"Не удалось создать счёт в Telegram Stars. Попробуй позже или выбери другой способ оплаты."
)
await callback.answer()
@router.callback_query(lambda callback: callback.data.startswith("method_ykassa_"))
async def method_ykassa_handler(callback: CallbackQuery):
"""
Обработчик оплаты через YooKassa.
"""
amount = callback.data.split("_")[-1]
# Сразу отвечаем на callback, чтобы Telegram не считал запрос "старым"
try:
await callback.answer()
except Exception:
# Если по какой-то причине уже отвечали — просто игнорируем
pass
# Формируем URL с query parameters вместо JSON body
endpoint = (
f"/billing/payments/init?"
f"user_id={callback.from_user.id}&amount={float(amount)}&provider=yookassa"
)
logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}")
# Отправляем POST запрос с пустым телом (параметры в URL)
result = await call_api("POST", endpoint, None, "http://billing:8000")
# Биллинг вообще не ответил/упал
if result == "ERROR" or not isinstance(result, dict):
await callback.message.edit_text(
"❌ Произошла ошибка при создании платежа. Попробуйте позже."
)
return
# Биллинг вернул ошибку (success = False)
if not result.get("success", False):
error_msg = (
result.get("error")
or result.get("detail")
or "Неизвестная ошибка"
)
await callback.message.edit_text(f"❌ Ошибка: {error_msg}")
return
payment_url = result.get("confirmation_url", "#")
payment_id = result.get("payment_id", "")
await callback.message.edit_text(
f"💵 <b>Оплата через YooKassa</b>\n\n"
f"💰 Сумма: <code>{amount}</code> руб\n"
f"📋 ID платежа: <code>{payment_id}</code>\n\n"
f"➡️ <a href='{payment_url}'>Перейти к оплате</a>\n\n"
f"<i>После оплаты нажмите кнопку 'Проверить оплату'</i>",
parse_mode=ParseMode.HTML,
disable_web_page_preview=True,
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[[
types.InlineKeyboardButton(
text="🔄 Проверить оплату",
callback_data=f"check_payment:{payment_id}",
)
]]
),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data.startswith("method_crypto_"))
async def method_crypto_handler(callback: CallbackQuery):
"""
Оплата через CryptoBot.
"""
amount = callback.data.split("_")[-1]
# Сразу отвечаем на callback, чтобы избежать таймаута
try:
await callback.answer()
except Exception:
pass
endpoint = (
f"/billing/payments/init?"
f"user_id={callback.from_user.id}&amount={float(amount)}&provider=cryptobot"
)
logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}")
result = await call_api("POST", endpoint, None, "http://billing:8000")
if result == "ERROR" or not isinstance(result, dict):
await callback.message.edit_text(
"❌ Произошла ошибка при создании платежа. Попробуйте позже."
)
return
if not result.get("success", False):
error_msg = (
result.get("error")
or result.get("detail")
or "Неизвестная ошибка"
)
await callback.message.edit_text(f"❌ Ошибка: {error_msg}")
return
payment_url = result.get("confirmation_url", "#")
payment_id = result.get("payment_id", "")
await callback.message.edit_text(
f"💵 <b>Оплата через Сryptobot</b>\n\n"
f"💰 Сумма: <code>{amount}</code> руб\n"
f"📋 ID платежа: <code>{payment_id}</code>\n\n"
f"➡️ <a href='{payment_url}'>Перейти к оплате</a>\n\n"
f"<i>После оплаты нажмите кнопку 'Проверить оплату'</i>",
parse_mode=ParseMode.HTML,
disable_web_page_preview=True,
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[[
types.InlineKeyboardButton(
text="🔄 Проверить оплату",
callback_data=f"check_payment:{payment_id}",
)
]]
),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "guide")
async def guide_callback_handler(callback: CallbackQuery):
"""
Обработчик callback_query для руководства.
"""
await callback.message.edit_text(
"Выбери платформу, для которой нужно руководство по подключению:",
reply_markup=guide_keyboard(),
)
await callback.answer()
# ===== Служебные хендлеры для платежей Telegram Stars =====
@router.pre_checkout_query()
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery):
"""
Обязательный шаг для Telegram Payments:
подтверждаем pre_checkout_query, иначе платёж не пройдёт.
"""
try:
await pre_checkout_query.bot.answer_pre_checkout_query(
pre_checkout_query.id,
ok=True,
)
except Exception as e:
logger.exception(f"Ошибка при answer_pre_checkout_query: {e}")
async def _process_stars_topup(message: Message):
"""
Логика зачисления средств после успешной оплаты Stars.
"""
sp = message.successful_payment
if not sp:
return
payload = sp.invoice_payload or ""
parts = payload.split(":")
if len(parts) != 3 or parts[0] != "stars_topup":
logger.info(
"successful_payment не относится к пополнению баланса Stars.")
return
_, telegram_id_str, amount_str = parts
try:
amount_rub = int(amount_str)
except ValueError:
# На всякий случай fallback к total_amount
amount_rub = sp.total_amount
data = {
"telegram_id": telegram_id_str,
"amount": amount_rub,
"currency": sp.currency,
"provider": "telegram_stars",
"telegram_payment_charge_id": sp.telegram_payment_charge_id,
}
logger.info(
f"Обработка успешного платежа Telegram Stars: "
f"user={telegram_id_str}, amount={amount_rub}, currency={sp.currency}"
)
result = await call_api("POST", "/user/deposit", data)
if result == "ERROR" or result is None:
await message.answer(
"⭐ Оплата через Telegram Stars прошла, но не удалось автоматически обновить баланс.\n"
"Если баланс не изменился — напиши, пожалуйста, в поддержку и укажи время платежа."
)
return
await message.answer(
f"⭐ Оплата через Telegram Stars успешно проведена.\n"
f"На твой баланс зачислено {amount_rub} ₽."
)
@router.message(F.successful_payment)
async def successful_payment_handler(message: Message):
"""
Глобальный хендлер успешных платежей.
Сейчас используем только для пополнения баланса через Stars.
"""
try:
await _process_stars_topup(message)
except Exception as e:
logger.exception(f"Ошибка при обработке successful_payment: {e}")
await message.answer(
"Оплата прошла, но произошла ошибка при обработке. "
"Если баланс не изменился — напиши, пожалуйста, в поддержку."
)

63
handlers/referrals.py Normal file
View File

@@ -0,0 +1,63 @@
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.enums.parse_mode import ParseMode
import logging
from .start import call_api
router = Router()
logger = logging.getLogger(__name__)
async def _build_referral_text(bot, user_id: int) -> str:
me = await bot.get_me()
bot_username = me.username or "LarkVPN_bot"
link = f"https://t.me/{bot_username}?start=ref_{user_id}"
invited_count = ""
try:
data = await call_api("GET", f"/user/{user_id}/referrals")
if isinstance(data, dict):
invited_count = str(data.get("invited_count", 0))
except Exception as e:
logger.exception(
f"Ошибка при получении количества рефералов для {user_id}: {e}"
)
text = (
"👥 <b>Реферальная программа</b>\n\n"
"Зови друзей в Lark VPN и получай бонусы на баланс.\n\n"
f"🔗 Твоя ссылка:\n<code>{link}</code>\n\n"
f"👤 Приглашено: {invited_count}\n\n"
)
return text
@router.message(Command("referrals"))
async def referrals_command(message: types.Message):
logger.info(f"Получена команда /referrals от {message.from_user.id}")
try:
text = await _build_referral_text(message.bot, message.from_user.id)
await message.answer(text, parse_mode=ParseMode.HTML)
except Exception as e:
logger.exception(f"Ошибка в обработчике /referrals: {e}")
await message.answer("Произошла ошибка. Попробуй позже.")
@router.callback_query(lambda callback: callback.data == "referral")
async def referrals_callback(callback: types.CallbackQuery):
try:
text = await _build_referral_text(
callback.message.bot,
callback.from_user.id,
)
await callback.message.edit_text(
text,
parse_mode=ParseMode.HTML,
)
except Exception as e:
logger.exception(f"Ошибка в обработчике callback 'referral': {e}")
await callback.message.answer("Произошла ошибка. Попробуй позже.")
finally:
await callback.answer()

190
handlers/start.py Normal file
View File

@@ -0,0 +1,190 @@
from aiogram import Router, types
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command
from aiogram.enums.parse_mode import ParseMode
import logging
import aiohttp
from instences.config import BASE_URL_FASTAPI
from keyboard.keyboards import main_keyboard
router = Router()
logger = logging.getLogger(__name__)
async def call_api(method: str, endpoint: str, data: dict | None = None):
"""
Мини-обёртка для запросов к backend API.
"""
url = f"{BASE_URL_FASTAPI}{endpoint}"
logger.info(f"API {method} {url} data={data}")
try:
async with aiohttp.ClientSession() as session:
async with session.request(method, url, json=data) as resp:
logger.info(f"API response {resp.status} {resp.reason}")
if resp.status in {200, 201}:
return await resp.json()
if resp.status in {400, 404}:
try:
return await resp.json()
except Exception:
return None
logger.error(f"Unexpected status {resp.status}: {await resp.text()}")
return "ERROR"
except Exception as e:
logger.exception(f"API exception {url}: {e}")
return "ERROR"
def _welcome_text(username: str | None) -> str:
"""
Текст приветствия в /start и в главном меню.
"""
return "🥚 Lark Security\n\nВыберите действие из меню ниже."
def _parse_referrer_id(message: Message) -> int | None:
"""
Достаём ref_<telegram_id> из /start.
Примеры:
/start
/start ref_123456789
"""
text = message.text or ""
parts = text.split(maxsplit=1)
if len(parts) < 2:
return None
arg = parts[1].strip()
if not arg.startswith("ref_"):
return None
raw_id = arg[4:]
if not raw_id.isdigit():
return None
try:
return int(raw_id)
except ValueError:
return None
@router.message(Command("start"))
async def start_command(message: Message):
"""
/start c обработкой реферального параметра.
Логика:
- проверяем, есть ли пользователь в БД по telegram_id;
- если нет — создаём /user/create;
- если есть корректный ref_ и пользователь новый — вызываем
/user/{referrer_id}/add_referral с invited_id = telegram_id.
"""
user_id = message.from_user.id
username = message.from_user.username
referrer_id = _parse_referrer_id(message)
logger.info(
f"[start] Команда /start от {user_id} (@{username}), "
f"text={message.text!r}, referrer_id={referrer_id}"
)
try:
existing = await call_api("GET", f"/user/{user_id}")
user_exists = isinstance(
existing, dict) and existing.get("id") is not None
if not user_exists:
logger.debug(f"[start] Пользователь {user_id} не найден, создаю.")
create_result = await call_api(
"POST",
"/user/create",
{"telegram_id": user_id},
)
if create_result == "ERROR":
logger.error(
f"[start] Не удалось создать пользователя {user_id} в БД"
)
if referrer_id is not None:
if referrer_id == user_id:
logger.info(
f"[start] Пользователь {user_id} попытался зайти "
f"по своей реферальной ссылке."
)
await message.answer(
"Нельзя переходить по своей же реферальной ссылке."
)
elif user_exists:
logger.info(
f"[start] Пользователь {user_id} уже есть в БД, "
f"реферальная ссылка {referrer_id} не сработает."
)
await message.answer(
"Вы уже зарегистрированы в боте, "
"реферальная ссылка не сработает."
)
else:
payload = {
"invited_id": user_id,
}
logger.info(
f"[start] Фиксирую реферала в бекенде: "
f"referrer_id={referrer_id}, payload={payload}"
)
result = await call_api(
"POST",
f"/user/{referrer_id}/add_referral",
payload,
)
if result == "ERROR":
logger.error(
f"[start] Ошибка при фиксации реферала через "
f"/user/{referrer_id}/add_referral: referrer={referrer_id}, "
f"invited={user_id}"
)
await message.answer("Вы вошли по реферальной ссылке.")
await message.answer(
_welcome_text(username),
reply_markup=main_keyboard(),
)
logger.info(f"[start] Главное меню отправлено пользователю {user_id}.")
except Exception as e:
logger.exception(
f"[start] Ошибка при обработке /start для пользователя {user_id}: {e}"
)
await message.answer("Произошла ошибка. Попробуйте позже.")
@router.callback_query(lambda callback: callback.data == "base")
async def start_callback_handler(callback: CallbackQuery):
"""
Callback с data="base" — возврат в главное меню.
"""
try:
user_id = callback.from_user.id
username = callback.from_user.username
logger.info(f"[start] callback 'base' от {user_id} (@{username})")
user_data = await call_api("GET", f"/user/{user_id}")
if user_data in (None, "ERROR"):
await call_api(
"POST",
"/user/create",
{"telegram_id": user_id},
)
await callback.message.edit_text(
_welcome_text(username),
reply_markup=main_keyboard(),
)
except Exception as e:
logger.exception(f"[start] Ошибка при обработке callback 'base': {e}")
await callback.message.answer("Произошла ошибка. Попробуйте позже.")
finally:
await callback.answer()

498
handlers/subscriptions.py Normal file
View File

@@ -0,0 +1,498 @@
from aiogram import Router, types
from aiogram.enums.parse_mode import ParseMode
from aiogram.filters import Command
import logging
from datetime import datetime
import aiohttp
from instences.config import BASE_URL_FASTAPI
from keyboard.keyboards import (
buy_keyboard,
tarif_Lark_keyboard,
tarif_Lark_pro_keyboard,
tarif_Lark_family_keyboard,
tarif_confirm_keyboard,
subscriptions_card_keyboard,
)
router = Router()
logger = logging.getLogger(__name__)
async def call_api(method: str, endpoint: str, data: dict | None = None):
"""
Унифицированный вызов backend API.
"""
url = f"{BASE_URL_FASTAPI}{endpoint}"
logger.info(f"API {method} {url} data={data}")
try:
async with aiohttp.ClientSession() as session:
async with session.request(method, url, json=data) as resp:
logger.info(f"API response {resp.status} {resp.reason}")
if resp.status in {200, 201}:
return await resp.json()
if resp.status in {400, 404}:
# Возвращаем JSON с detail, чтобы разобрать ошибку
try:
return await resp.json()
except Exception:
return None
logger.error(f"Unexpected status {resp.status}: {await resp.text()}")
return "ERROR"
except Exception as e:
logger.exception(f"API exception {url}: {e}")
return "ERROR"
def escape_markdown_v2(text: str) -> str:
"""
Экранирует спецсимволы для Markdown_V2.
"""
special_chars = r"_*[]()~`>#+-=|{}.!\\"
for ch in special_chars:
text = text.replace(ch, f"\\{ch}")
return text
def _plan_human_title(plan: str) -> str:
"""
Красивое имя тарифа по plan-строке из БД.
"""
if not plan:
return ""
if plan.startswith("Lark_Standart"):
return "Lark Basic"
if plan.startswith("Lark_Pro"):
return "Lark Pro"
if plan.startswith("Lark_Family"):
return "Lark Family"
return plan
async def _fetch_user_and_subs(telegram_id: int):
user = await call_api("GET", f"/user/{telegram_id}")
if not isinstance(user, dict):
return None, []
user_id = user.get("id") or user.get("telegram_id") or user.get("user_id")
if not user_id:
return None, []
subs = await call_api("GET", f"/subscriptions/{user_id}")
if subs == "ERROR" or not isinstance(subs, list):
subs = []
return user, subs
async def _show_subscriptions_view(
msg: types.Message,
telegram_id: int,
index: int = 0,
edit: bool = False,
):
"""
Рендер карточки подписки: навигация + конфиг + продление.
"""
user, subs = await _fetch_user_and_subs(telegram_id)
if not user:
text = "Не удалось получить данные пользователя. Попробуй чуть позже."
if edit:
await msg.edit_text(text)
else:
await msg.answer(text)
return
if not subs:
text = (
"У тебя пока нет активных подписок.\n\n"
"Можешь подключить тариф через меню подписок."
)
if edit:
await msg.edit_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=buy_keyboard(),
)
else:
await msg.answer(
text,
parse_mode=ParseMode.HTML,
reply_markup=buy_keyboard(),
)
return
# Нормализуем индекс
index = max(0, min(index, len(subs) - 1))
sub = subs[index]
plan_title = _plan_human_title(sub.get("plan_name"))
end_raw = sub.get("end_date") # Исправил
date_str = ""
days_left = None
status_emoji = "🟢"
if end_raw:
try:
dt = datetime.fromisoformat(end_raw)
date_str = dt.strftime("%d.%m.%Y")
now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now()
days_left = (dt.date() - now.date()).days
if days_left < 0:
status_emoji = ""
else:
status_emoji = "🟢"
except ValueError:
status_emoji = ""
lines = [
f"{status_emoji} <b>Подписка</b>",
f"Тариф: {plan_title}",
]
if days_left is None:
lines.append(f"Действует до {date_str}")
else:
if days_left >= 0:
lines.append(f"Действует до {date_str} (ещё {days_left} дн.)")
else:
lines.append(f"Истекла {date_str}")
lines.append("")
lines.append(f"{index + 1} из {len(subs)}")
text = "\n".join(lines)
kb = subscriptions_card_keyboard(
sub_id=sub["id"],
index=index,
total=len(subs),
)
if edit:
await msg.edit_text(text, parse_mode=ParseMode.HTML, reply_markup=kb)
else:
await msg.answer(text, parse_mode=ParseMode.HTML, reply_markup=kb)
# ===== Мои подписки / карточка =====
@router.message(Command("subscriptions"))
async def cmd_subscriptions(message: types.Message):
"""
/subscriptions — открыть карточку подписок.
"""
await _show_subscriptions_view(
msg=message,
telegram_id=message.from_user.id,
index=0,
edit=False,
)
@router.callback_query(lambda c: c.data == "go_subscriptions")
async def cb_go_subscriptions(callback: types.CallbackQuery):
"""
Кнопка из других экранов — перейти в 'Мои подписки'.
"""
await _show_subscriptions_view(
msg=callback.message,
telegram_id=callback.from_user.id,
index=0,
edit=True,
)
await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_prev:"))
async def cb_sub_prev(callback: types.CallbackQuery):
"""
Навигация: предыдущая подписка.
"""
try:
index = int(callback.data.split(":")[1])
except (IndexError, ValueError):
index = 0
await _show_subscriptions_view(
msg=callback.message,
telegram_id=callback.from_user.id,
index=index,
edit=True,
)
await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_next:"))
async def cb_sub_next(callback: types.CallbackQuery):
"""
Навигация: следующая подписка.
"""
try:
index = int(callback.data.split(":")[1])
except (IndexError, ValueError):
index = 0
await _show_subscriptions_view(
msg=callback.message,
telegram_id=callback.from_user.id,
index=index,
edit=True,
)
await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_cfg:"))
async def cb_sub_cfg(callback: types.CallbackQuery):
"""
Кнопка 'Конфиг' в карточке подписки.
Тянем URI через /uri?telegram_id=...
"""
result = await call_api(
"GET",
f"/uri?telegram_id={callback.from_user.id}",
)
if result == "ERROR" or not isinstance(result, dict):
await callback.message.answer(
"Не удалось получить конфиг. Попробуй позже."
)
await callback.answer()
return
detail = result.get("detail")
if detail == "SUB_ERROR":
await callback.message.answer(
"У тебя нет активной подписки для генерации конфига."
)
await callback.answer()
return
uri = None
if isinstance(detail, str):
uri = detail.strip()
elif isinstance(detail, list) and detail:
uri = str(detail[0]).strip()
if not uri:
await callback.message.answer(
"Что-то пошло не так при выдаче конфига."
)
await callback.answer()
return
await callback.message.answer(
f"Твой конфиг:\n<code>{uri}</code>",
parse_mode=ParseMode.HTML,
)
await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_renew:"))
async def cb_sub_renew(callback: types.CallbackQuery):
sub_id = callback.data.split(":", maxsplit=1)[
1] if ":" in callback.data else None
user, subs = await _fetch_user_and_subs(callback.from_user.id)
if not user or not subs:
await callback.message.answer("Подписка не найдена.")
await callback.answer()
return
target = next((s for s in subs if s.get("id") == sub_id), None)
if not target:
await callback.message.answer("Подписка не найдена.")
await callback.answer()
return
plan_name = target.get("plan_name")
if not plan_name:
await callback.message.answer("Не удалось определить тариф для продления.")
await callback.answer()
return
result = await call_api(
"POST",
"/subscription/buy",
{"telegram_id": str(callback.from_user.id), "plan_name": plan_name},
)
if result == "ERROR" or not isinstance(result, dict):
await callback.message.answer("Ошибка при продлении. Попробуй позже.")
await callback.answer()
return
detail = result.get("detail")
if detail == "INSUFFICIENT_FUNDS":
await callback.message.answer("Недостаточно средств на балансе.")
await callback.answer()
return
if detail == "TARIFF_NOT_FOUND":
await callback.message.answer("Тариф не найден.")
await callback.answer()
return
if detail == "ACTIVE_SUBSCRIPTION_EXISTS":
await callback.message.answer("У тебя уже есть активная подписка этого типа.")
await callback.answer()
return
uri = result.get("message")
if isinstance(uri, str) and uri:
await callback.message.answer(
"Подписка успешно продлена.\n"
f"Твой конфиг:\n<code>{uri}</code>",
parse_mode=ParseMode.HTML,
)
else:
await callback.message.answer("Подписка успешно продлена.")
await _show_subscriptions_view(
msg=callback.message,
telegram_id=callback.from_user.id,
index=0,
edit=True,
)
await callback.answer()
# ===== Меню покупки подписки (Basic / Pro / Family) =====
@router.callback_query(lambda callback: callback.data == "buy_subscription")
async def buy_subscription_callback_handler(callback: types.CallbackQuery):
"""
Меню выбора тарифа: Basic / Pro / Family + показ баланса.
"""
user_data = await call_api("GET", f"/user/{callback.from_user.id}")
balance = user_data.get("balance", 0) if user_data else "?"
text = (
" <b>Подключить новую подписку</b>\n\n"
"🐣 Lark Basic - 200 ₽ / мес\n"
"🦅 Lark Pro - 400 ₽ / мес\n"
"👨‍👩‍👧 Lark Family - 700 ₽ / мес\n\n"
f"💰 Баланс: {balance}\n\n"
"Выбери тариф:"
)
await callback.message.edit_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=buy_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "subs")
async def subs_callback_handler(callback: types.CallbackQuery):
"""
Кнопка 'Lark Basic' — раскрываем варианты сроков.
"""
await callback.message.edit_text(
"Тариф Lark Basic — выбери срок:",
reply_markup=tarif_Lark_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "subs_pro")
async def subs_pro_callback_handler(callback: types.CallbackQuery):
"""
Кнопка 'Lark Pro'.
"""
await callback.message.edit_text(
"Тариф Lark Pro — выбери срок:",
reply_markup=tarif_Lark_pro_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "subs_family")
async def subs_family_callback_handler(callback: types.CallbackQuery):
"""
Кнопка 'Lark Family'.
"""
await callback.message.edit_text(
"Тариф Lark Family — выбери срок:",
reply_markup=tarif_Lark_family_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data.startswith("Lark:"))
async def lark_tariff_callback_handler(callback: types.CallbackQuery):
"""
Обработчик выбора конкретного тарифа Lark.
callback_data: Lark:<Class>:<months>
"""
try:
_, tariff_class, months_str = callback.data.split(":")
months = int(months_str)
except Exception:
await callback.message.answer("Некорректные данные тарифа.")
await callback.answer()
return
if months == 1:
months_label = "1 месяц"
elif 2 <= months <= 4:
months_label = f"{months} месяца"
else:
months_label = f"{months} месяцев"
text = f"Тариф Lark {tariff_class} на {months_label}. Продолжить покупку?"
keyboard = tarif_confirm_keyboard("Lark", months, tariff_class)
await callback.message.edit_text(text=text, reply_markup=keyboard)
await callback.answer()
@router.callback_query(lambda callback: callback.data.startswith("confirm:"))
async def confirm_callback_handler(callback: types.CallbackQuery):
try:
data = callback.data.split(":", maxsplit=1)[1]
name, classif, amount_str = data.split("_")
plan_name = f"{name}_{classif}_{amount_str}"
except Exception:
await callback.message.edit_text("Некорректные данные тарифа.")
await callback.answer()
return
result = await call_api(
"POST",
"/subscription/buy",
{"telegram_id": str(callback.from_user.id), "plan_name": plan_name},
)
if result == "ERROR" or not isinstance(result, dict):
await callback.message.edit_text("Ошибка при оформлении подписки.")
await callback.answer()
return
detail = result.get("detail")
if detail == "INSUFFICIENT_FUNDS":
await callback.message.edit_text(
"Недостаточно средств. Попробуй пополнить баланс."
)
await callback.answer()
return
if detail == "TARIFF_NOT_FOUND":
await callback.message.edit_text("Тариф не найден.")
await callback.answer()
return
if detail == "ACTIVE_SUBSCRIPTION_EXISTS":
await callback.message.edit_text("У тебя уже есть активная подписка.")
await callback.answer()
return
uri = result.get("message", "")
if uri:
await callback.message.edit_text("Подписка успешно оформлена.")
await callback.message.answer(
f"Подписка успешно оформлена!\n\nТвой конфиг:\n<code>{uri}</code>",
parse_mode=ParseMode.HTML,
)
else:
await callback.message.edit_text("Подписка успешно оформлена.")
await callback.answer()

209
handlers/support.py Normal file
View File

@@ -0,0 +1,209 @@
from aiogram import Router, types
from aiogram.filters import Command
import logging
from instences.config import BASE_URL_FASTAPI
import aiohttp
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from keyboard.keyboards import (
faq_keyboard,
sup_keyboard,
ticket_list_keyboard,
ticket_keyboard,
)
logger = logging.getLogger(__name__)
router = Router()
class TicketState(StatesGroup):
subject = State()
message = State()
async def call_api(method, endpoint, data=None):
"""
Выполняет HTTP-запрос к FastAPI.
"""
url = f"{BASE_URL_FASTAPI}{endpoint}"
logger.info(f"Инициализация запроса: {method} {url} с данными {data}")
try:
async with aiohttp.ClientSession() as session:
async with session.request(method, url, json=data) as response:
logger.info(
f"Получен ответ от {url}: статус {response.status}")
if response.status in {200, 201}:
result = await response.json()
logger.debug(f"Ответ JSON: {result}")
return result
if response.status == 404:
logger.debug(f"Код {response.status}, возвращаю ничего")
return None
logger.error(
f"Ошибка в запросе: статус {response.status}, причина {response.reason}"
)
return "ERROR"
except Exception as e:
logger.exception(f"Исключение при выполнении запроса к {url}: {e}")
return "ERROR"
@router.callback_query(lambda callback: callback.data == "faq")
async def faq_callback_handler(callback: types.CallbackQuery):
"""
Обработчик callback_query с data="faq".
"""
await callback.message.edit_text(
"FAQ YOU",
reply_markup=faq_keyboard(),
)
await callback.answer()
@router.message(Command("support"))
async def supp(message: types.Message):
"""
Меню сапп системы (через команду).
"""
await message.answer(
"Добро пожаловать в саппорт систему!",
reply_markup=sup_keyboard(),
)
@router.callback_query(lambda callback: callback.data == "main_sup")
async def supp_callback(callback: types.CallbackQuery):
"""
Меню сапп системы (callback-версия).
"""
await callback.message.edit_text(
"Добро пожаловать в саппорт систему!",
reply_markup=sup_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "my_tickets")
async def list_tickets_callback(callback: types.CallbackQuery):
user_id = callback.from_user.id
user_data = await call_api("GET", f"/user/{user_id}")
if not user_data:
user_data = await call_api(
"POST",
"/user/create",
{"telegram_id": f"{user_id}"},
)
tickets = await call_api(
"GET",
f"/support/tickets?user_id={user_data['id']}",
)
if tickets == "ERROR" or not tickets:
await callback.message.edit_text(
"У вас нет тикетов.",
reply_markup=sup_keyboard(),
)
await callback.answer()
return
await callback.message.edit_text(
"Ваши тикеты:",
reply_markup=ticket_list_keyboard(tickets),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "make_ticket")
async def start_ticket_creation(callback: types.CallbackQuery, state: FSMContext):
"""
Начинает процесс создания тикета.
"""
await callback.message.answer(
"Введите тему тикета (или нажмите 'Отмена', чтобы выйти):",
reply_markup=ticket_keyboard(),
)
await state.set_state(TicketState.subject)
await callback.answer()
@router.message()
async def handle_ticket_input(message: types.Message, state: FSMContext):
"""
Обрабатывает ввод данных для тикета.
"""
current_state = await state.get_state()
if current_state == TicketState.subject:
await state.update_data(subject=message.text)
await message.answer(
"Введите описание проблемы:",
reply_markup=ticket_keyboard(),
)
await state.set_state(TicketState.message)
elif current_state == TicketState.message:
user_data = await state.get_data()
subject = user_data.get("subject")
message_text = message.text
await create_ticket(message, subject, message_text, state)
await state.clear()
async def create_ticket(
message: types.Message,
subject: str,
message_text: str,
state: FSMContext,
):
"""
Отправляет запрос на создание тикета через FastAPI.
"""
user_id = message.from_user.id
try:
logger.info(
f"Создание тикета для пользователя {user_id}: "
f"Тема - {subject}, Сообщение - {message_text}"
)
user_data = await call_api("GET", f"/user/{user_id}")
if not user_data:
await message.answer("Вы еще не зарегистрированы.")
return
ticket_data = await call_api(
"POST",
f"/support/tickets?user_id={user_data['id']}",
data={"subject": subject, "message": message_text},
)
if ticket_data != "ERROR":
await message.answer(
"✅ Тикет успешно создан!\n"
f"📌 Тема: {ticket_data['subject']}\n"
f"📊 Статус: {ticket_data['status']}\n"
f"📅 Дата создания: {ticket_data['created_at']}"
)
else:
await message.answer("❌ Ошибка создания тикета. Попробуйте позже.")
except Exception as e:
logger.exception(
f"Ошибка при создании тикета для пользователя {user_id}: {e}"
)
await message.answer("❌ Произошла ошибка при создании тикета.")
@router.callback_query(lambda callback: callback.data == "cancel_ticket")
async def cancel_ticket_creation(callback: types.CallbackQuery, state: FSMContext):
"""
Отмена создания тикета.
"""
await state.clear()
await callback.message.answer(
"Создание тикета отменено.",
reply_markup=types.ReplyKeyboardRemove(),
)
await callback.answer()

9
instences/config.py Normal file
View File

@@ -0,0 +1,9 @@
import os
import logging
logger = logging.getLogger(__name__)
BASE_URL_FASTAPI = os.getenv("BASE_URL")

View File

@@ -1,140 +1,446 @@
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
from aiogram.types import InlineKeyboardButton, KeyboardButton
def main_keyboard():
"""
База
Главное меню
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Профиль", callback_data="profile"))
builder.row(InlineKeyboardButton(text="FAQ", callback_data="faq"))
builder.row(InlineKeyboardButton(text="О нас", url="https://www.youtube.com/watch?v=Zirn-CKck-c"))
builder.row(
InlineKeyboardButton(
text="📜 Профиль",
callback_data="profile",
)
)
builder.row(
InlineKeyboardButton(
text="❔ FAQ ❔",
callback_data="faq",
)
)
builder.row(
InlineKeyboardButton(
text=" О нас",
url="https://www.youtube.com/watch?v=Zirn-CKck-c",
)
)
return builder.as_markup()
def account_keyboard():
"""
Аккаунт
Клавиатура профиля:
пополнить баланс, история транзакций, назад в главное меню.
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Баланс", callback_data="balance"))
builder.row(InlineKeyboardButton(text="Приобрести подписку", callback_data="buy_subscription"))
builder.row(InlineKeyboardButton(text="Руководство по подключению", callback_data="guide"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="base"))
builder.row(
InlineKeyboardButton(
text="🪙 Пополнить баланс",
callback_data="balance",
)
)
builder.row(
InlineKeyboardButton(
text="🧾 История транзакций",
callback_data="tranhist",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="base",
)
)
return builder.as_markup()
def balance_keyboard():
"""
Экран баланса
"""
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🪙 Пополнить баланс",
callback_data="popup",
)
)
builder.row(
InlineKeyboardButton(
text="🧾 История транзакций",
callback_data="tranhist",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="profile",
)
)
return builder.as_markup()
def popup_keyboard():
"""
Суммы пополнения: 200, 300, 600, 1000 ₽.
"""
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="200 ₽", callback_data="popup:200"),
)
builder.row(
InlineKeyboardButton(text="300 ₽", callback_data="popup:300"),
)
builder.row(
InlineKeyboardButton(text="600 ₽", callback_data="popup:600"),
)
builder.row(
InlineKeyboardButton(text="1000 ₽", callback_data="popup:1000"),
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="profile",
)
)
return builder.as_markup()
def payment_methods_keyboard(amount: int):
"""
Способы оплаты для выбранной суммы.
"""
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="⭐ Telegram Stars",
callback_data=f"method_stars_{amount}",
)
)
builder.row(
InlineKeyboardButton(
text="💵 СБП",
callback_data=f"method_ykassa_{amount}",
)
)
builder.row(
InlineKeyboardButton(
text="🪙 CryptoBot",
callback_data=f"method_crypto_{amount}",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="popup",
)
)
return builder.as_markup()
def ticket_list_keyboard(tickets):
builder = InlineKeyboardBuilder()
for ticket in tickets:
builder.row(
InlineKeyboardButton(
text=f"Тикет: {ticket['subject']}",
callback_data=f"ticket_{ticket['id']}",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="main_sup",
)
)
return builder.as_markup()
def sup_keyboard():
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📝 Создать запрос",
callback_data="make_ticket",
)
)
builder.row(
InlineKeyboardButton(
text="📂 Мои запросы",
callback_data="my_tickets",
)
)
return builder.as_markup()
def ticket_keyboard():
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Отмена",
callback_data="cancel_ticket", # БЫЛО "cancel"
)
)
return builder.as_markup()
def buy_keyboard():
"""
Приобрести подписку
Меню выбора тарифа.
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Тариф Lark", callback_data="subs"))
builder.row(InlineKeyboardButton(text="Тариф Lark Pro", callback_data="subs_pro"))
builder.row(InlineKeyboardButton(text="О тарифах", url="https://t.me/proxylark/19"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="profile"))
builder.row(
InlineKeyboardButton(
text="🐣 Lark Basic",
callback_data="subs",
)
)
builder.row(
InlineKeyboardButton(
text="🦅 Lark Pro",
callback_data="subs_pro",
)
)
builder.row(
InlineKeyboardButton(
text="👨‍👩‍👧 Lark Family",
callback_data="subs_family",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="profile",
)
)
return builder.as_markup()
def subhist_keyboard():
"""
Подписки
"""
builder = InlineKeyboardBuilder()
builder.button(text="Назад", callback_data="profile")
return builder.as_markup()
def popup_keyboard():
"""
Пополнение
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="200₽", callback_data="popup:200"),InlineKeyboardButton(text="500₽", callback_data="popup:500"))
builder.row(InlineKeyboardButton(text="1000₽", callback_data="popup:1000"),InlineKeyboardButton(text="2000₽", callback_data="popup:2000"))
builder.row(InlineKeyboardButton(text="3000₽", callback_data="popup:3000"),InlineKeyboardButton(text="5000₽", callback_data="popup:5000"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="balance"))
return builder.as_markup()
def balance_keyboard():
"""
Баланс
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Пополнение", callback_data="popup"))
builder.row(InlineKeyboardButton(text="История транзакций", callback_data="tranhist"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="profile"))
return builder.as_markup()
def tarif_Lark_keyboard():
"""
Тариф Lark
Тариф Lark Basic (Standart)
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Тариф Lark 1 Месяц", callback_data="Lark:Standart:1"))
builder.row(InlineKeyboardButton(text="Тариф Lark 6 Месяц", callback_data="Lark:Standart:6"))
builder.row(InlineKeyboardButton(text="Тариф Lark 12 Месяц", callback_data="Lark:Standart:12"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="buy_subscription"))
builder.row(
InlineKeyboardButton(
text="🐣 Lark 1 месяц",
callback_data="Lark:Standart:1",
)
)
builder.row(
InlineKeyboardButton(
text="🐣 Lark 6 месяцев",
callback_data="Lark:Standart:6",
)
)
builder.row(
InlineKeyboardButton(
text="🐣 Lark 12 месяцев",
callback_data="Lark:Standart:12",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="buy_subscription",
)
)
return builder.as_markup()
def tarif_Lark_pro_keyboard():
"""
Тариф Lark Pro
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Тариф Lark Pro 1 Месяц", callback_data="Lark:Pro:1"))
builder.row(InlineKeyboardButton(text="Тариф Lark Pro 6 Месяц", callback_data="Lark:Pro:6"))
builder.row(InlineKeyboardButton(text="Тариф Lark Pro 12 Месяц", callback_data="Lark:Pro:12"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="buy_subscription"))
builder.row(
InlineKeyboardButton(
text="🦅 Lark Pro 1 месяц",
callback_data="Lark:Pro:1",
)
)
builder.row(
InlineKeyboardButton(
text="🦅 Lark Pro 6 месяцев",
callback_data="Lark:Pro:6",
)
)
builder.row(
InlineKeyboardButton(
text="🦅 Lark Pro 12 месяцев",
callback_data="Lark:Pro:12",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="buy_subscription",
)
)
return builder.as_markup()
def tarif_Lark_family_keyboard():
"""
Тариф Lark Family.
"""
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="👨‍👩‍👧 Lark Family 1 месяц",
callback_data="Lark:Family:1",
)
)
builder.row(
InlineKeyboardButton(
text="👨‍👩‍👧 Lark Family 6 месяцев",
callback_data="Lark:Family:6",
)
)
builder.row(
InlineKeyboardButton(
text="👨‍👩‍👧 Lark Family 12 месяцев",
callback_data="Lark:Family:12",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="buy_subscription",
)
)
return builder.as_markup()
def subscriptions_card_keyboard(sub_id: str, index: int, total: int):
"""
Карточка подписки:
навигация, конфиг, назад в главное меню.
"""
builder = InlineKeyboardBuilder()
# Навигация по подпискам
nav = []
if index > 0:
nav.append(
InlineKeyboardButton(
text="⬅️",
callback_data=f"sub_prev:{index-1}",
)
)
if index < total - 1:
nav.append(
InlineKeyboardButton(
text="➡️",
callback_data=f"sub_next:{index+1}",
)
)
if nav:
builder.row(*nav)
# Конфиг
builder.row(
InlineKeyboardButton(
text="🔑 Конфиг",
callback_data=f"sub_cfg:{sub_id}",
)
)
# Назад в главное меню
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="base", # было "profile" изменил
)
)
return builder.as_markup()
def guide_keyboard():
"""
Руководство по подключению
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="IOS,Android", callback_data="mob"))
builder.row(InlineKeyboardButton(text="Windows,MacOS", callback_data="pc"))
builder.row(InlineKeyboardButton(text="Назад", callback_data="profile"))
builder.row(
InlineKeyboardButton(
text="📱 iOS / Android",
callback_data="mob",
)
)
builder.row(
InlineKeyboardButton(
text="💻 Windows / macOS",
callback_data="pc",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="profile",
)
)
return builder.as_markup()
# def about_tarifs_keyboard():
# """
# О тарифах
# """
# builder = InlineKeyboardBuilder()
# builder.row(InlineKeyboardButton(text="Назад", callback_data="buy_subscription"))
# return builder.as_markup()
def faq_keyboard():
"""
FAQ
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Назад", callback_data="base"))
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="base",
)
)
return builder.as_markup()
def tranhist_keyboard():
"""
История транзакций
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Назад",callback_data="balance"))
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
callback_data="profile",
)
)
return builder.as_markup()
def tarif_confirm_keyboard(name,amount,classif):
def tarif_confirm_keyboard(name: str, amount: int, classif: str):
"""
Подтверждение покупки тарифа
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Подтвердить", callback_data=f"confirm:{name}_{classif}_{amount}"))
builder.row(InlineKeyboardButton(text="Отменить",callback_data="buy_subscription"))
builder.row(
InlineKeyboardButton(
text="✅ Подтвердить",
callback_data=f"confirm:{name}_{classif}_{amount}",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Отменить",
callback_data="buy_subscription",
)
)
return builder.as_markup()
def confirm_popup_keyboard():
"""
Подтверждение пополнения
Подтверждение пополнения.
"""
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Теперь иди нахуй", callback_data="balance"))
builder.row(
InlineKeyboardButton(
text="✅ Готово, вернуться в профиль",
callback_data="profile",
)
)
return builder.as_markup()

22
main.py
View File

@@ -1,8 +1,8 @@
import os
import asyncio
from aiogram import Bot, Dispatcher
from databases.db_config import init_postgresql, init_mongodb, close_connections
from aiogram.types import BotCommand
from handlers import routers
from Middleware.anti_spam_middleware import AntiSpamMiddleware
import logging
@@ -11,7 +11,8 @@ import logging
BOT_TOKEN = os.getenv("TOKEN")
if not BOT_TOKEN:
raise ValueError("Не задан токен бота. Убедитесь, что переменная окружения 'TOKEN' установлена.")
raise ValueError(
"Не задан токен бота. Убедитесь, что переменная окружения 'TOKEN' установлена.")
bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()
@@ -23,21 +24,19 @@ dp.message.middleware(AntiSpamMiddleware(rate_limit=1))
async def set_commands():
"""Устанавливает команды для бота."""
commands = [
BotCommand(command="/start", description="Запустить бота"),
BotCommand(command="start", description="Запуск бота"),
BotCommand(command="subscriptions", description="Мои подписки"),
BotCommand(command="support", description="Техподдержка"),
BotCommand(command="referrals", description="Реферальная программа"),
]
await bot.set_my_commands(commands)
async def on_startup():
"""Действия при запуске бота."""
# Инициализация баз данных
await init_mongodb()
await init_postgresql()
# Установка команд бота
await set_commands()
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Бот запущен!")
@@ -45,18 +44,15 @@ async def on_startup():
async def on_shutdown():
"""Действия при остановке бота."""
# Закрытие подключений к базам данных
await close_connections()
# Закрытие сессии бота
await bot.session.close()
print("Бот остановлен.")
async def main():
"""Основной цикл работы бота."""
from handlers.handlers import register_handlers
register_handlers(dp) # Регистрация хендлеров
for router in routers:
dp.include_router(router) # Регистрация хендлеров
await on_startup()

View File

@@ -1,263 +0,0 @@
from db import User, Subscription, Transaction, VPNServer
import string
import secrets
import json
from sqlalchemy import desc
from dateutil.relativedelta import relativedelta
from datetime import datetime
from db import get_db_session
from panel import PanelInteraction
from utils.LogCon import setup_logger, load_config
config = load_config()
def generate_random_string(length=8):
characters = string.ascii_letters + string.digits
return ''.join(secrets.choice(characters) for _ in range(length))
class UserService:
def __init__(self, logger):
self.logger = logger
def add_user(self, telegram_id: int):
session = next(get_db_session())
try:
new_user = User(telegram_id=telegram_id, username=generate_random_string())
session.add(new_user)
session.commit()
except Exception as e:
session.rollback()
self.logger.error(f"Ошибка при добавлении пользователя: {e}")
finally:
session.close()
def get_user_by_telegram_id(self, telegram_id: int):
session = next(get_db_session())
try:
return session.query(User).filter(User.telegram_id == telegram_id).first()
except Exception as e:
self.logger.error(f"Ошибка при получении пользователя: {e}")
finally:
session.close()
def add_transaction(self, user_id: int, amount: float):
session = next(get_db_session())
try:
transaction = Transaction(user_id=user_id, amount=amount)
session.add(transaction)
session.commit()
except Exception as e:
self.logger.error(f"Ошибка добавления транзакции: {e}")
finally:
session.close()
def update_balance(self, telegram_id: int, amount: float):
session = next(get_db_session())
try:
user = session.query(User).filter(User.telegram_id == telegram_id).first()
if user:
user.balance = amount
self.add_transaction(user.id, amount)
session.commit()
else:
self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.")
except Exception as e:
session.rollback()
self.logger.error(f"Ошибка при обновлении баланса: {e}")
finally:
session.close()
def last_subscription(self, user):
session = next(get_db_session())
try:
return (
session.query(Subscription)
.filter(Subscription.user_id == user.id)
.order_by(desc(Subscription.created_at))
.first()
)
except Exception as e:
self.logger.error(f"Ошибка при получении последней подписки: {e}")
finally:
session.close()
def tariff_setting(self, user, plan: str, expiry_duration: int):
session = next(get_db_session())
try:
server = (
session.query(VPNServer)
.filter(VPNServer.current_users < VPNServer.max_users)
.order_by(VPNServer.current_users.asc())
.first()
)
if not server:
self.logger.error("Error: 120")
return "120"
# Рассчитываем дату окончания подписки
expiry_ = datetime.utcnow() + relativedelta(months=expiry_duration)
self.logger.info(f"Create subscribe to {user.id} on server {server.id} with plan {plan} until {expiry_}")
new_subscription = Subscription(user_id=user.id, vpn_server_id=server.id, plan=plan, expiry_date=expiry_)
session.add(new_subscription)
session.commit()
self.logger.info(f"Subscribe successfully created for {user.id}")
return "OK"
except Exception as e:
self.logger.error(f"Error with created subscribe: {e}")
return "Ошибка"
finally:
session.close()
def buy_sub(self, telegram_id: str, plan: str):
session = next(get_db_session())
try:
user = session.query(User).filter(User.telegram_id == telegram_id).first()
if not user:
self.logger.error(f"User with Telegram ID {telegram_id} not found.")
return "error"
current_plan = config['subscription_templates'].get(plan)
if not current_plan:
self.logger.error(f"Tarif {plan} not found.")
return "error"
cost = current_plan['cost']
if user.balance >= cost:
user.balance -= cost
session.commit()
result = self.tariff_setting(user, plan, current_plan['duration'])
if result == "OK":
add_server_result = self.add_to_server(telegram_id)
if add_server_result == "OK":
return "OK"
else:
return "ERROR " + add_server_result
else:
return "ERROR " + result
self.logger.error(f"Nt enough money {telegram_id} for {plan}.")
return 100
except Exception as e:
self.logger.error(f"Error with buying sub {telegram_id}: {e}")
session.rollback()
finally:
session.close()
def get_sub_list(self, count: int, user_id: int):
session = next(get_db_session())
try:
return (
session.query(Subscription)
.filter(Subscription.user_id == user_id)
.order_by(desc(Subscription.created_at))
.limit(count)
.all()
)
except Exception as e:
self.logger.error(f"Ошибка при получении списка подписок для пользователя {user_id}: {e}")
def add_to_server(self, telegram_id: str):
session = next(get_db_session())
try:
user_sub = (
session.query(Subscription)
.join(User)
.filter(User.telegram_id == telegram_id)
.first()
)
user = session.query(User).filter(User.telegram_id == telegram_id).first()
server = session.query(VPNServer).filter(VPNServer.id == user_sub.vpn_server_id).first()
url_base = f"https://{server.ip_address}:{server.port}/{server.secret}"
login_data = {
'username': server.login,
'password': server.password,
}
try:
server_config_dict = json.loads(server.config)
except json.JSONDecodeError as e:
self.logger.error(f"Ошибка разбора JSON: {e}")
return "180"
client_id = server_config_dict['obj']['id']
panel = PanelInteraction(url_base, login_data, self.logger)
panel.add_client(client_id, user_sub.expiry_date.isoformat(), user.username)
return "OK"
except Exception as e:
self.logger.error(f"Ошибка при установке на сервер для пользователя {telegram_id}: {e}")
return "ERROR"
def create_uri(self, telegram_id: str):
session = next(get_db_session())
try:
user = session.query(User).filter(User.telegram_id == telegram_id).first()
if not user:
self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.")
return "error"
sub = self.last_subscription(user)
if not sub:
self.logger.error("Подписка не найдена.")
return "error"
vpn_server = session.query(VPNServer).filter_by(id=sub.vpn_server_id).first()
base_url = f"https://{vpn_server.ip_address}:{vpn_server.port}/{vpn_server.secret}"
login_data = {
'username': vpn_server.login,
'password': vpn_server.password
}
server_config_dict = json.loads(vpn_server.config)
client_id = server_config_dict['obj']['id']
PI = PanelInteraction(base_url, login_data, self.logger)
CIF3 = PI.get_client_traffic(user.username) # Client Info From 3x-ui
VPNCIF3 = PI.getInboundInfo(client_id)
return self.generate_uri(vpn_config=VPNCIF3, CIF3=CIF3)
except Exception as e:
self.logger.error(f"Ошибка в создании URI: {e}")
return "error"
finally:
session.close()
def generate_uri(self, vpn_config, CIF3):
try:
config_data = json.loads(vpn_config) if isinstance(vpn_config, str) else vpn_config
obj = config_data["obj"]
port = obj["port"]
clients = json.loads(obj["settings"])["clients"] if isinstance(obj["settings"], str) else obj["settings"]["clients"]
for client in clients:
if client["email"] == CIF3['obj']['email']:
uuid = client["id"]
flow = client["flow"]
stream_settings = json.loads(obj["streamSettings"]) if isinstance(obj["streamSettings"], str) else obj["streamSettings"]
dest = stream_settings["realitySettings"]["dest"]
server_names = stream_settings["realitySettings"]["serverNames"]
public_key = stream_settings["realitySettings"]["settings"]["publicKey"]
fingerprint = stream_settings["realitySettings"]["settings"]["fingerprint"]
short_id = stream_settings["realitySettings"]["shortIds"][0] # Первый короткий ID
return (
f"vless://{uuid}@{dest}:{port}?type=tcp&security=reality"
f"&pbk={public_key}&fp={fingerprint}&sni={server_names[0]}"
f"&sid={short_id}&spx=%2F&flow={flow}#user-{CIF3}"
)
self.logger.error(f"Клиент с email {CIF3} не найден.")
return None
except Exception as e:
self.logger.error(f"Ошибка в методе создания URI: {e}")
return None

View File

@@ -1,70 +0,0 @@
import argparse
from datetime import datetime
import json
import base64
from pymongo import MongoClient
def connect_to_mongo(uri, db_name):
"""Подключение к MongoDB."""
client = MongoClient(uri)
db = client[db_name]
return db
def load_json(json_path):
"""Загружает JSON-данные из файла."""
with open(json_path, "r", encoding="utf-8") as f:
return json.load(f)
def encode_file(file_path):
"""Читает файл и кодирует его в Base64."""
with open(file_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def insert_certificate(data, cert_path, cert_location):
"""Добавляет сертификат в указанное место внутри структуры JSON."""
# Читаем и кодируем сертификат
certificate_data = encode_file(cert_path)
# Разбиваем путь на вложенные ключи
keys = cert_location.split(".")
target = data
for key in keys[:-1]:
if key not in target:
target[key] = {} # Создаем вложенные ключи, если их нет
target = target[key]
target[keys[-1]] = {
"data": certificate_data,
"uploaded_at": datetime.utcnow()
}
def insert_data(db, collection_name, data):
"""Вставляет данные в указанную коллекцию MongoDB."""
collection = db[collection_name]
collection.insert_one(data)
print(f"Данные успешно вставлены в коллекцию '{collection_name}'.")
def main():
parser = argparse.ArgumentParser(description="Insert JSON data into MongoDB with certificate")
parser.add_argument("--mongo-uri",default="mongodb://root:itOj4CE2miKR@mongodb:27017" ,required=True, help="MongoDB URI")
parser.add_argument("--db-name",default="MongoDBSub&Ser" ,required=True, help="MongoDB database name")
parser.add_argument("--collection",default="servers", required=True, help="Collection name")
parser.add_argument("--json-path", required=True, help="Path to the JSON file with data")
parser.add_argument("--cert-path", required=True, help="Path to the certificate file (.crt)")
parser.add_argument("--cert-location", required=True, help="Path inside JSON structure to store certificate (e.g., 'server.certificate')")
args = parser.parse_args()
# Подключение к MongoDB
db = connect_to_mongo(args.mongo_uri, args.db_name)
# Загрузка данных из JSON-файла
data = load_json(args.json_path)
# Вставка сертификата в структуру данных
insert_certificate(data, args.cert_path, args.cert_location)
# Вставка данных в MongoDB
insert_data(db, args.collection, data)
if __name__ == "__main__":
main()

View File

@@ -22,4 +22,6 @@ def setup_logger():
logger.addHandler(handler)
return logger
return logger
app_logger = setup_logger()

View File

@@ -1,107 +1,203 @@
import requests
import aiohttp
import uuid
import string
import secrets
import json
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import json
import base64
import ssl
from datetime import datetime
from dateutil.relativedelta import relativedelta
def generate_uuid():
return str(uuid.uuid4())
class PanelInteraction:
def __init__(self, base_url, login_data, logger_):
def __init__(self, base_url, login_data, logger, certificate=None, is_encoded=True):
"""
Initialize the PanelInteraction class.
:param base_url: Base URL for the panel.
:param login_data: Login data (username/password or token).
:param logger: Logger for debugging.
:param certificate: Certificate content (Base64-encoded or raw string).
:param is_encoded: Indicates whether the certificate is Base64-encoded.
"""
self.base_url = base_url
self.login_data = login_data
self.logger = logger_
self.session_id = self.login()
if self.session_id:
self.headers = {
'Accept': 'application/json',
'Cookie': f'3x-ui={self.session_id}',
'Content-Type': 'application/json'
}
else:
raise ValueError("Login failed, session_id is None")
self.logger = logger
self.cert_content = self._decode_certificate(certificate, is_encoded)
self.session_id = None # Session ID will be initialized lazily
self.headers = None
def login(self):
login_url = self.base_url + "/login"
def _decode_certificate(self, certificate, is_encoded):
"""
Decode the provided certificate content.
:param certificate: Certificate content (Base64-encoded or raw string).
:param is_encoded: Indicates whether the certificate is Base64-encoded.
:return: Decoded certificate content as bytes.
"""
if not certificate:
self.logger.error("No certificate provided.")
raise ValueError("Certificate is required.")
try:
# Создаем SSLContext
ssl_context = ssl.create_default_context()
# Декодируем, если нужно
if is_encoded:
certificate = base64.b64decode(certificate).decode()
# Загружаем сертификат в SSLContext
ssl_context.load_verify_locations(cadata=certificate)
return ssl_context
except Exception as e:
self.logger.error(f"Error while decoding certificate: {e}")
raise ValueError("Invalid certificate format or content.") from e
async def _ensure_logged_in(self):
"""
Ensure the session ID is available for authenticated requests.
"""
if not self.session_id:
self.session_id = await self.login()
if self.session_id:
self.headers = {
'Accept': 'application/json',
'Cookie': f'3x-ui={self.session_id}',
'Content-Type': 'application/json'
}
else:
raise ValueError("Unable to log in and retrieve session ID.")
async def login(self):
"""
Perform login to the panel.
:return: Session ID or None.
"""
login_url = f"{self.base_url}/login"
self.logger.info(f"Attempting to login at: {login_url}")
try:
response = requests.post(login_url, data=self.login_data, verify=False, timeout=10)
response.raise_for_status()
session_id = response.cookies.get("3x-ui")
if session_id:
return session_id
else:
self.logger.error(f"Login failed: {response.status_code}")
self.logger.debug(f"Response content: {response.text}")
return None
except requests.RequestException as e:
self.logger.error(f"Login request failed: {e}")
return None
def getInboundInfo(self, inboundId):
url = f"{self.base_url}/panel/api/inbounds/get/{inboundId}"
try:
response = requests.get(url, headers=self.headers, verify=False, timeout=10)
response.raise_for_status()
if response:
return response.json()
else:
self.logger.error(f"Failed to get inbound info: {response.status_code}")
self.logger.debug("Response:", response.text)
async with aiohttp.ClientSession() as session:
try:
async with session.post(
login_url, data=self.login_data, ssl=self.cert_content, timeout=10
) as response:
if response.status == 200:
session_id = response.cookies.get("3x-ui")
if session_id:
return session_id.value
else:
self.logger.error("Login failed: No session ID received.")
return None
else:
self.logger.error(f"Login failed: {response.status}")
return None
except aiohttp.ClientError as e:
self.logger.error(f"Login request failed: {e}")
return None
except requests.RequestException as e:
self.logger.error(f"Get inbound request failed: {e}")
def get_client_traffic(self, email):
async def get_inbound_info(self, inbound_id):
"""
Fetch inbound information by ID.
:param inbound_id: ID of the inbound.
:return: JSON response or None.
"""
await self._ensure_logged_in()
url = f"{self.base_url}/panel/api/inbounds/get/{inbound_id}"
async with aiohttp.ClientSession() as session:
try:
async with session.get(
url, headers=self.headers, ssl=self.cert_content, timeout=10
) as response:
if response.status == 200:
return await response.json()
else:
self.logger.error(f"Failed to get inbound info: {response.status}")
return None
except aiohttp.ClientError as e:
self.logger.error(f"Get inbound info request failed: {e}")
return None
async def get_client_traffic(self, email):
"""
Fetch traffic information for a specific client.
:param email: Client's email.
:return: JSON response or None.
"""
await self._ensure_logged_in()
url = f"{self.base_url}/panel/api/inbounds/getClientTraffics/{email}"
try:
response = requests.get(url, headers=self.headers, verify=False, timeout=10)
response.raise_for_status()
if response:
return response.json()
else:
self.logger.error(f"Failed to get client traffic: {response.status_code}")
self.logger.debug("Response:", response.text)
async with aiohttp.ClientSession() as session:
try:
async with session.get(
url, headers=self.headers, ssl=self.cert_content, timeout=10
) as response:
if response.status == 200:
return await response.json()
else:
self.logger.error(f"Failed to get client traffic: {response.status}")
return None
except aiohttp.ClientError as e:
self.logger.error(f"Get client traffic request failed: {e}")
return None
except requests.RequestException as e:
self.loggin.error(f"Get client request failed: {e}")
def update_client_expiry(self, client_uuid, new_expiry_time, client_email):
async def update_client_expiry(self, client_uuid, new_expiry_time, client_email):
"""
Update the expiry date of a specific client.
:param client_uuid: UUID of the client.
:param new_expiry_time: New expiry date in ISO format.
:param client_email: Client's email.
:return: None.
"""
await self._ensure_logged_in()
url = f"{self.base_url}/panel/api/inbounds/updateClient"
update_data = {
"id": 1,
"settings": json.dumps({
"id": 1,
"settings": {
"clients": [
{
"id": client_uuid,
"alterId": 0,
"email": client_email,
"email": client_email,
"limitIp": 2,
"totalGB": 0,
"totalGB": 0,
"expiryTime": new_expiry_time,
"enable": True,
"tgId": "",
"subId": ""
}
]
})
}
}
try:
response = requests.post(url, headers=self.headers, json=update_data, verify=False)
response.raise_for_status()
if response:
self.logger.debug("Client expiry time updated successfully.")
else:
self.logger.error(f"Failed to update client: {response.status_code}, {response.text}")
except requests.RequestException as e:
self.logger.error(f"Update client request failed: {e}")
def add_client(self, inbound_id, expiry_date,email):
async with aiohttp.ClientSession() as session:
try:
async with session.post(
url, headers=self.headers, json=update_data, ssl=self.cert_content
) as response:
if response.status == 200:
self.logger.info("Client expiry updated successfully.")
else:
self.logger.error(f"Failed to update client expiry: {response.status}")
except aiohttp.ClientError as e:
self.logger.error(f"Update client expiry request failed: {e}")
async def add_client(self, inbound_id, expiry_date, email):
"""
Add a new client to an inbound.
:param inbound_id: ID of the inbound.
:param expiry_date: Expiry date in ISO format.
:param email: Client's email.
:return: JSON response or None.
"""
await self._ensure_logged_in()
url = f"{self.base_url}/panel/api/inbounds/addClient"
client_info = {
"clients": [
@@ -111,7 +207,7 @@ class PanelInteraction:
"email": email,
"limitIp": 2,
"totalGB": 0,
"flow":"xtls-rprx-vision",
"flow": "xtls-rprx-vision",
"expiryTime": expiry_date,
"enable": True,
"tgId": "",
@@ -121,15 +217,19 @@ class PanelInteraction:
}
payload = {
"id": inbound_id,
"settings": json.dumps(client_info)
"settings": client_info
}
try:
response = requests.post(url, headers=self.headers, json=payload, verify=False)
if response.status_code == 200:
return response.json()
else:
self.logger.error(f"Failed to add client: {response.status_code}")
self.logger.debug("Response:", response.text)
async with aiohttp.ClientSession() as session:
try:
async with session.post(
url, headers=self.headers, json=payload, ssl=self.cert_content
) as response:
if response.status == 200:
return await response.status
else:
self.logger.error(f"Failed to add client: {response.status}")
return None
except aiohttp.ClientError as e:
self.logger.error(f"Add client request failed: {e}")
return None
finally:
self.logger.info("Finished attempting to add client.")