commit 47db6a5fb73d06965adf0816b408584665263191 Author: Disledg Date: Mon Sep 30 19:21:48 2024 +0300 Create project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15897e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.json +pyvenv.cfg +bin/ +include/ +lib/ +lib64/ +lib64 \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..08afe72 --- /dev/null +++ b/bot.py @@ -0,0 +1,107 @@ + +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes +from db import User +from db import Subscription +from db import Transaction +from db import VPNServer +from sqlalchemy import desc +import json +from datetime import datetime +from db import get_db_session +from db import init_db, SessionLocal +from logger_config import setup_logger +with open('config.json', 'r') as file: + config = json.load(file) + +def last_subscription(session,user): + last_subscription = ( + session.query(Subscription) + .filter(Subscription.user_id == user.id) + .order_by(desc(Subscription.created_at)) + .first() + ) + return last_subscription + + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + keyboard = [ + [ + InlineKeyboardButton("Личный кабинет", callback_data="account"), + InlineKeyboardButton("О нac ;)", callback_data='about'), + ], + [InlineKeyboardButton("Поддержка", callback_data='support')], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text(f'Добро пожаловать в ...! Здесь вы можете приобрести VPN. И нечего более',reply_markup=reply_markup) + +async def personal_account(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + keyboard = [ + [ + InlineKeyboardButton("Пополнить баланс", callback_data="pop_up"), + InlineKeyboardButton("Приобрести подписку", callback_data='buy_tarif'), + ], + [InlineKeyboardButton("FAQ", callback_data='faq')], + [InlineKeyboardButton("История платежей", callback_data='payment_history')] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + session = next(get_db_session()) + user = session.query(User).filter(User.telegram_id == update.chat_member.from_user.id).first() + check = last_subscription(session=session,user=user) + + if not check: + await update.message.reply_text(f'Профиль {user.username}\nВы не приобретали ещё у нас подписку, но это явно стоит сделать:)',reply_markup=reply_markup) + # проверяем, истекла ли подписка + if check.expiry_date < datetime.now(): + await update.message.reply_text(f'Ваш профиль {user.username},Ваша подписка действует до - {check.expiry_date}',reply_markup=reply_markup) + else: + await update.message.reply_text(f'Ваш профиль {user.username},\nВаша подписка истекла - {check.expiry_date}',reply_markup=reply_markup) + +async def about(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + keyboard = [ + [ + InlineKeyboardButton("Главное меню", callback_data="account") + ]] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text(f'Игорь чё нить напишет, я продублирую',reply_markup=reply_markup) + +async def support(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + keyboard = [ + [ + InlineKeyboardButton("Главное меню", callback_data="account"), + InlineKeyboardButton("Написать", callback_data="sup") # Нужно через каллбек доделать + ]] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text(f'Что бы отправить сообщение поддержке выберите в меню кнопку "Написать", а далее изложите в одном сообщении свою ошибку.',reply_markup=reply_markup) + +async def pop_up(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + keyboard = [ + [ + InlineKeyboardButton("Главное меню", callback_data="account"), + ]] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text(f'Когда нибудь эта штука заработает',reply_markup=reply_markup) + +#async def buy_subscription(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + + +def main() -> None: + logger = setup_logger() + init_db() + db = SessionLocal() + application = Application.builder().token(config['token']).build() + + application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("account", personal_account)) + application.add_handler(CommandHandler("about", about)) + application.add_handler(CommandHandler("support", support)) + application.add_handler(CommandHandler("popup", pop_up)) + #application.add_handler(CommandHandler("buy_subscription", buy_subscription)) + + application.run_polling(allowed_updates=Update.ALL_TYPES) + db.close() + + +if __name__ == "__main__": + main() diff --git a/db.py b/db.py new file mode 100644 index 0000000..1721acc --- /dev/null +++ b/db.py @@ -0,0 +1,84 @@ +from sqlalchemy import create_engine, Column, String, Integer, Numeric, DateTime, ForeignKey, Text +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship +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(String, unique=True, nullable=False) + username = Column(String) # email 3x-ui + balance = Column(Numeric(10, 2), default=0.0) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + subscriptions = relationship("Subscription", back_populates="user") + transactions = relationship("Transaction", 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, ForeignKey('vpn_servers.id')) + plan = Column(String) + expiry_date = Column(DateTime) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + user = relationship("User", back_populates="subscriptions") + vpn_server = relationship("VPNServer", 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.now) + + user = relationship("User", back_populates="transactions") + +# VPN-серверы +class VPNServer(Base): + __tablename__ = 'vpn_servers' + + id = Column(String, primary_key=True, default=generate_uuid) + server_name = Column(String) + ip_address = Column(String) + port = Column(Integer) + login_data = Column(Text) + inbound = Column(Text) + config = Column(Text) + current_users = Column(Integer, default=0) + max_users = Column(Integer, default=4) + + subscriptions = relationship("Subscription", back_populates="vpn_server") + +# Настройка подключения к базе данных +DATABASE_URL = "postgresql://vpn_bot_user:yourpassword@localhost/vpn_bot_db" + +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def init_db(): + Base.metadata.create_all(bind=engine) + +def get_db_session(): + db = SessionLocal() + try: + yield db + finally: + db.close() + diff --git a/logger_config.py b/logger_config.py new file mode 100644 index 0000000..d40f9b2 --- /dev/null +++ b/logger_config.py @@ -0,0 +1,21 @@ +import logging +from logging.handlers import TimedRotatingFileHandler + +def setup_logger(): + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + + handler = TimedRotatingFileHandler( + "app.log", + when="midnight", + interval=1, + backupCount=7, + encoding='utf-8' + ) + + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + handler.setFormatter(formatter) + + logger.addHandler(handler) + + return logger diff --git a/panel.py b/panel.py new file mode 100644 index 0000000..f4e6388 --- /dev/null +++ b/panel.py @@ -0,0 +1,133 @@ +import requests +import uuid +import string +import secrets +import json +from logger_config import setup_logger +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta + +with open('config.json', 'r') as file : config = json.load(file) + +def generate_date(months): + now = datetime.now() + + # Преобразуем months в число + try: + months = int(months) # или float(months), если месяцы могут быть дробными + except ValueError: + raise TypeError("months должно быть числом") + + future_date = now + timedelta(days=30 * months) + return future_date.isoformat() + + +def generate_random_string(length=8): + characters = string.ascii_letters + string.digits + return ''.join(secrets.choice(characters) for _ in range(length)) + +def generate_uuid(): + return str(uuid.uuid4()) + + +class PanelInteraction: + def __init__(self, base_url, login_data, logger_): + 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") + + def login(self): + login_url = self.base_url + "/login" + response = requests.post(login_url, data=self.login_data) + if response.status_code == 200: + session_id = response.cookies.get("3x-ui") + return session_id + else: + self.logger.error(f"Login failed: {response.status_code}") + return None + + def getInboundInfo(self,inboundId): + url = f"{self.base_url}/panel/api/inbounds/get/{inboundId}" + response = requests.get(url, headers=self.headers) + if response.status_code == 200: + return response.json() + else: + self.logger.error(f"Failed to get inbound info: {response.status_code}") + self.logger.debug("Response:", response.text) + return None + + + def get_client_traffic(self, email): + url = f"{self.base_url}/panel/api/inbounds/getClientTraffics/{email}" + response = requests.get(url, headers=self.headers) + if response.status_code == 200: + return response.json() + else: + self.logger.error(f"Failed to get client traffic: {response.status_code}") + self.logger.debug("Response:", response.text) + return None + + def update_client_expiry(self, client_uuid, new_expiry_time, client_email): + url = f"{self.base_url}/panel/api/inbounds/updateClient" + update_data = { + "id": 1, + "settings": json.dumps({ + "clients": [ + { + "id": client_uuid, + "alterId": 0, + "email": client_email, + "limitIp": 2, + "totalGB": 0, + "expiryTime": new_expiry_time, + "enable": True, + "tgId": "", + "subId": "" + } + ] + }) + } + response = requests.post(url, headers=self.headers, json=update_data) + if response.status_code == 200: + self.logger.debug("Client expiry time updated successfully.") + else: + self.logger.error(f"Failed to update client: {response.status_code} {response.text}") + + def add_client(self, inbound_id, months): + url = f"{self.base_url}/panel/api/inbounds/addClient" + client_info = { + "clients": [ + { + "id": generate_uuid(), + "alterId": 0, + "email": generate_random_string(), + "limitIp": 2, + "totalGB": 0, + "expiryTime": generate_date(months), + "enable": True, + "tgId": "", + "subId": "" + } + ] + } + payload = { + "id": inbound_id, + "settings": json.dumps(client_info) + } + response = requests.post(url, headers=self.headers, json=payload) + if response.status_code == 200: + self.logger.debug("Client added successfully!") + return response.json() + else: + self.logger.error(f"Failed to add client: {response.status_code}") + self.logger.debug("Response:", response.text) + return None diff --git a/service.py b/service.py new file mode 100644 index 0000000..cd57c73 --- /dev/null +++ b/service.py @@ -0,0 +1,145 @@ +from db import User +from db import Subscription +from db import Transaction +from db import VPNServer +from datetime import datetime,timedelta +from db import get_db_session +import json +from panel import PanelInteraction + +with open('config.json', 'r') as file : config = json.load(file) + +class UserService: + def __init__(self,logger): + self.logger = logger + + def add_user(self,telegram_id: int, username: str): + session = next(get_db_session()) + try: + new_user = User(telegram_id=telegram_id, username=username) + 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 pop_up_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: + self.logger.error(f"Ошибка при обновлении баланса:{e}") + self.logger.error(f"Сумма: {amount}, Пользователь: {telegram_id}") + session.rollback() + finally: + session.close() + + def tariff_setting(self,telegram_id: int,plan: str): + session = next(get_db_session()) + try: + user = session.query(User).filter(User.telegram_id == telegram_id).first() + if user: + server = ( + session.query(VPNServer) + .filter(VPNServer.current_users < VPNServer.max_users) + .order_by(VPNServer.current_users.asc()) + .first()) + current_plan = config['subscription_templates'].get(plan) + expiry_ = datetime.now() + timedelta(days=current_plan['expiry_duration']) + new_subscription = Subscription(user_id = user.id,vpn_server_id = server.id,plan = plan,expiry_date = expiry_) + session.add(new_subscription) + session.commit() + except Exception as e: + self.logger.error(f"Чё то ошибка в установке тарифа: {e}") + finally: + session.close() + + + def create_uri(self,telegram_id,): + session = next(get_db_session()) + try: + user = session.query(User).filter(User.telegram_id == telegram_id).first() + if user: + subscription = user.subscriptions + if not subscription: + return None + vpn_server = session.query(VPNServer).filter_by(id=subscription.vpn_server_id).first() + baseURL ="http://" + vpn_server.ip_address + ":" + vpn_server.port + PI = PanelInteraction(baseURL,vpn_server.login_data,self.logger) + CIF3 = PI.get_client_traffic(user.username) # Client Info From 3x-ui + URI = self.generate_uri(vpn_config=vpn_server.config,CIF3=CIF3) + session.commit() + return URI + except Exception as e: + self.logger.error(f"Чё то ошибка в создании uri: {e}") + finally: + session.close() + + + def generate_uri(self, vpn_config, CIF3): + try: + # Извлечение необходимых данных из конфигурации + config_data = json.loads(vpn_config) + obj = config_data["obj"] + port = obj["port"] + clients = json.loads(obj["settings"])["clients"] + + # Поиск клиента по email (CIF3) + for client in clients: + if client["email"] == CIF3: + uuid = client["id"] + flow = client["flow"] + + # Извлечение параметров из streamSettings + stream_settings = json.loads(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 + + # Сборка строки VLess + URI = ( + 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}" + ) + + return URI + + # Если клиент с указанным email не найден + self.logger.warning(f"Клиент с email {CIF3} не найден.") + return None + + except Exception as e: + self.logger.error(f"Ошибка в методе создания uri: {e}") + return None +