From 824e007786c0d87e7eb204d58dd8bdb9921c9139 Mon Sep 17 00:00:00 2001 From: Disledg Date: Sat, 26 Oct 2024 19:38:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=BA=D0=BE=D0=BD=D0=B5=D1=86=20-=20=D0=9F=D0=BE?= =?UTF-8?q?=D0=BA=D1=83=D0=BF=D0=BA=D1=83,=20=D0=BD=D0=BE=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B4=D0=BE=20=D0=BF=D0=BE=D0=B4=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D0=B0=D1=80=D1=83=20=D0=BC=D0=BE=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20-=20=D0=98=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B9=20-=20=D0=9F=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B8=D0=BB=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=83=20panel.py=20=D0=BD=D0=B0=D0=BA=D0=BE=D0=BD=D0=B5=D1=86,?= =?UTF-8?q?=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D1=83=20=D0=BF=D1=80=D0=B8=D0=BA=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 269 +++++++++++++++-------------------------------- db.py | 27 +++-- db_operations.py | 74 ------------- panel.py | 96 ++++++++--------- service.py | 227 +++++++++++++++++++++++++++++---------- 5 files changed, 318 insertions(+), 375 deletions(-) delete mode 100644 db_operations.py diff --git a/bot.py b/bot.py index 52ef570..9d5fcea 100644 --- a/bot.py +++ b/bot.py @@ -1,243 +1,140 @@ - -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes,MessageHandler, filters, CommandHandler -from db import User, VPNServer, Transaction, Subscription -from db import get_db_session -from db import init_db, SessionLocal -from db_operations import last_subscription, create_user , get_sub_list,buy_sub +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import Application, CallbackQueryHandler, ContextTypes, MessageHandler, filters +from db import User, VPNServer, Transaction, Subscription, get_db_session, init_db, SessionLocal from sqlalchemy import desc - +from service import UserService import json - from datetime import datetime - from logger_config import setup_logger + +# Чтение конфигурации и настройка логгера with open('config.json', 'r') as file: config = json.load(file) - logger = setup_logger() +# Общая функция для создания клавиатуры +def create_keyboard(buttons): + return InlineKeyboardMarkup([[InlineKeyboardButton(text, callback_data=data)] for text, data in buttons]) +# Функция для отправки сообщений с загрузкой +async def send_loading_message(update, context, text, reply_markup=None): + loading_message = await context.bot.send_message(chat_id=update.effective_chat.id, text="Загрузка...") + await loading_message.edit_text(text, reply_markup=reply_markup) + +# Функция для обработки главного меню 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) - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - await loading_message.edit_text(f'Добро пожаловать в ...! Здесь вы можете приобрести VPN. И нечего более',reply_markup=reply_markup) + buttons = [("Личный кабинет", "account"), ("О нас ;)", "about"), ("Поддержка", "support")] + await send_loading_message(update, context, 'Добро пожаловать в ...! Здесь вы можете приобрести VPN. И нечего более', create_keyboard(buttons)) +# Функция для обработки личного кабинета 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()) - - if session.query(User).filter(User.telegram_id == str(update.callback_query.from_user.id)).first() == None: - create_user(str(update.callback_query.from_user.id),update.effective_user.username) - user = session.query(User).filter(User.telegram_id == str(update.callback_query.from_user.id)).first() - - subscription = last_subscription(session=session,user=user) - - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - - if not subscription: - await loading_message.edit_text( - f'Профиль {user.username}, {user.telegram_id}\nВы не приобретали ещё у нас подписку, но это явно стоит сделать:)\nВаш счёт составляет: {user.balance}',reply_markup=reply_markup - ) - # Проверяем, истекла ли подписка - elif subscription.expiry_date < datetime.now(): - await loading_message.edit_text( - f'Ваш профиль {user.username}, {user.telegram_id}, Ваша подписка действует до - {subscription.expiry_date}',reply_markup=reply_markup - ) - else: - await loading_message.edit_text( - f'Ваш профиль {user.username}, {user.telegram_id},\nВаша подписка истекла - {subscription.expiry_date}',reply_markup=reply_markup - ) + service = UserService(logger) + tgid = str(update.callback_query.from_user.id) + user = service.get_user_by_telegram_id(tgid) or service.add_user(tgid) + subscription = service.last_subscription(user) + buttons = [("Пополнить баланс", "pop_up"), ("Приобрести подписку", "buy_tarif"), ("❔FAQ❔", "faq"), ("История платежей", "payment_history")] + text = ( + f'Профиль {user.username}, {user.telegram_id}\n' + f'{"Вы не приобретали ещё у нас подписку, но это явно стоит сделать:)" if not subscription else f"Ваша подписка действует до - {subscription.expiry_date}" if subscription.expiry_date > datetime.now() else f"Ваша подписка истекла - {subscription.expiry_date}"}\n' + f'Ваш счёт составляет: {user.balance}' + ) + await send_loading_message(update, context, text, create_keyboard(buttons)) +# Функция для отображения информации "О нас" async def about(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - keyboard = [ - [ - InlineKeyboardButton("Главное меню", callback_data="start") - ]] - reply_markup = InlineKeyboardMarkup(keyboard) - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - await loading_message.edit_text(f'Игорь чё нить напишет, я продублирую',reply_markup=reply_markup) + buttons = [("Главное меню", "start")] + await send_loading_message(update, context, 'Игорь чё нить напишет, я продублирую', create_keyboard(buttons)) +# Функция для отображения поддержки async def support(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - keyboard = [ - [ - InlineKeyboardButton("Главное меню", callback_data="start"), - InlineKeyboardButton("Написать", callback_data="sup") # Нужно через каллбек доделать - ]] - reply_markup = InlineKeyboardMarkup(keyboard) - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - await loading_message.edit_text(f'Что бы отправить сообщение поддержке выберите в меню кнопку "Написать", а далее изложите в одном сообщении свою ошибку.',reply_markup=reply_markup) + buttons = [("Главное меню", "start"), ("Написать", "sup")] + await send_loading_message(update, context, 'Для связи с поддержкой выберите "Написать" и изложите проблему в одном сообщении.', create_keyboard(buttons)) +# Функция для пополнения баланса async def pop_up(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - keyboard = [ - [ - InlineKeyboardButton("Главное меню", callback_data="start"), - ]] - reply_markup = InlineKeyboardMarkup(keyboard) - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - await loading_message.edit_text(f'Когда нибудь эта штука заработает',reply_markup=reply_markup) + buttons = [("Главное меню", "start")] + await send_loading_message(update, context, 'Когда-нибудь эта функция заработает', create_keyboard(buttons)) +# Функция для покупки подписки async def buy_subscription(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - keyboard = [ - [ - InlineKeyboardButton("Главное меню", callback_data="start"), - ]] - reply_markup = InlineKeyboardMarkup(keyboard) - session = next(get_db_session()) - user = session.query(User).filter(User.telegram_id == str(update.callback_query.from_user.id)).first() - check = last_subscription(session=session,user=user) - - if not check: - keyboard = [ - [ - InlineKeyboardButton("Тариф 1 \"Бимжик\"", callback_data="Бимжик"), - ], - [ - InlineKeyboardButton("Тариф 2 \"Бизнес хомячёк\"", callback_data="Бизнес_хомячёк"), - ], - [ - InlineKeyboardButton("Тариф 3 \"Продвинутый Акулёнок\"", callback_data="Продвинутый_Акулёнок"), - ], - [ - InlineKeyboardButton("Главное меню", callback_data="start"), - ]] - reply_markup = InlineKeyboardMarkup(keyboard) - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - await loading_message.edit_text(f'Какую подписку вы хотели бы приобрести\nТариф 1 "Бимжик" - 200 рубликов - 1 месяцок\nТариф 2 "Бизнес хомячёк" - 500 рубликов - 3 месяцка\nТариф 3 "Продвинутый Акулёнок" - 888 рубликов - 6 месяцков\n',reply_markup=reply_markup) - # проверяем, истекла ли подписка + service = UserService(logger) + tgid = str(update.callback_query.from_user.id) + user = service.get_user_by_telegram_id(tgid) + subscription = service.last_subscription(user) + + if subscription is None: + buttons = [("Тариф 1 \"Бимжик\"", "Бимжик"), ("Тариф 2 \"Бизнес хомячёк\"", "Бизнес_хомячёк"), ("Тариф 3 \"Продвинутый Акулёнок\"", "Продвинутый_Акулёнок"), ("Главное меню", "start")] + text = 'Какую подписку вы хотите приобрести?\n1. "Бимжик" - 200 руб. на 1 месяц\n2. "Бизнес хомячёк" - 500 руб. на 3 месяца\n3. "Продвинутый Акулёнок" - 888 руб. на 6 месяцев' else: - keyboard = [ - [ - InlineKeyboardButton("Главное меню", callback_data="start"), - ]] - reply_markup = InlineKeyboardMarkup(keyboard) - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - await loading_message.edit_text(f'У вас уже приобретена подписка',reply_markup=reply_markup) - + buttons = [("Главное меню", "start")] + text = 'У вас уже приобретена подписка' + + await send_loading_message(update, context, text, create_keyboard(buttons)) +# Функция для отображения FAQ async def faq(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - keyboard = [ - [ - InlineKeyboardButton("Главное меню", callback_data="start"), - ]] - reply_markup = InlineKeyboardMarkup(keyboard) - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - await loading_message.edit_text(f'Когда нибудь что нибудь здесь будет написано!!;)',reply_markup=reply_markup) - + buttons = [("Главное меню", "start")] + await send_loading_message(update, context, 'Когда-нибудь здесь появится полезная информация!', create_keyboard(buttons)) +# Функция для обработки ввода пользователя в поддержку async def sup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if context.user_data.get('awaiting_input'): user_input = update.message.text await update.message.reply_text(f"Вы ввели: {user_input}") - - # После получения текста сбрасываем ожидание context.user_data['awaiting_input'] = False else: await update.message.reply_text("Выберите команду или нажмите кнопку для продолжения.") - +# Обработчик кнопок async def button_handler(update: Update, context): query = update.callback_query - await query.answer() + data = query.data + + service = UserService(logger) + tgid = str(query.from_user.id) - session = next(get_db_session()) try: - if query.data == 'account': - await personal_account(update,context) - elif query.data == 'start': - await start(update,context) - elif query.data == 'about': + if data == 'account': + await personal_account(update, context) + elif data == 'start': + await start(update, context) + elif data == 'about': await about(update, context) - elif query.data == 'support': + elif data == 'support': await support(update, context) - elif query.data == 'sup': - context.user_data['awaiting_input'] = True - elif query.data == 'pop_up': + elif data == 'sup': + context.user_data['awaiting_input'] = True + elif data == 'pop_up': await pop_up(update, context) - elif query.data == 'buy_tarif': + elif data == 'buy_tarif': await buy_subscription(update, context) - elif query.data == 'faq': + elif data == 'faq': await faq(update, context) - elif query.data == 'payment_history': + elif data == 'payment_history': await active_sub(update, context) - - if query.data in ['Бимжик', 'Бизнес_хомячёк', 'Продвинутый_Акулёнок']: - loading_message = await query.message.reply_text("Загрузка...") - plan = query.data.replace('_', ' ') if '_' in query.data else query.data - check = buy_sub(session, query.from_user.id, plan, logger) - - - if check != "OK": - await loading_message.edit_text("Неизвестный тариф.") - else: - await loading_message.edit_text("Тариф успешно установлен!") - + elif data in ['Бимжик', 'Бизнес_хомячёк', 'Продвинутый_Акулёнок']: + plan = data.replace('_', ' ') + result = service.buy_sub(tgid, data) + text = { + "OK": "Ваша конфигурация готова!", + "100": "Недостаточно средств.", + "120": "Нет доступных серверов, подождите немного.", + }.get(result, "Неизвестный тариф.") + await query.message.reply_text(text) except Exception as e: - logger.error(f"Ошибка при обработке запроса пользователя {query.from_user.id}: {e}") + logger.error(f"Ошибка при обработке запроса пользователя {tgid}: {e}") await query.message.reply_text("Произошла ошибка. Пожалуйста, попробуйте снова.") - finally: - session.close() - - - -async def active_sub(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - keyboard = [ - [ - InlineKeyboardButton("Главное меню", callback_data="start"), - ]] - reply_markup = InlineKeyboardMarkup(keyboard) - session = next(get_db_session()) - list_sub = get_sub_list(session, 10, update.callback_query.from_user.id) - loading_message = await context.bot.send_message(chat_id=update.callback_query.message.chat.id, text="Загрузка...") - if list_sub: - message = "Ваши подписки:\n" - for cur_sub in list_sub: - if cur_sub.expiry_date > datetime.now(): - message += f" Активная: {cur_sub.plan}, Дата покупки: {cur_sub.created_at}\n" - else: - message += f" Устаревшая: {cur_sub.plan}, Дата покупки: {cur_sub.created_at}\n" - else: - message = "Ты пидор, не приобрел у нас подписку?!" - - await loading_message.edit_text(message,reply_markup=reply_markup) +# Запуск приложения def main() -> None: 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.add_handler(CommandHandler("faq", faq)) - application.add_handler(CommandHandler("active_sub", active_sub)) application.add_handler(CallbackQueryHandler(button_handler)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, sup)) - application.run_polling(allowed_updates=Update.ALL_TYPES) - db.close() - if __name__ == "__main__": main() diff --git a/db.py b/db.py index 604dbbe..36fc128 100644 --- a/db.py +++ b/db.py @@ -1,4 +1,4 @@ -from sqlalchemy import create_engine, Column, String, Integer, Numeric, DateTime, ForeignKey, Text +from sqlalchemy import create_engine, Column, String, Integer, Numeric, DateTime, ForeignKey, Text, Boolean from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy import desc @@ -19,13 +19,15 @@ class User(Base): id = Column(String, primary_key=True, default=generate_uuid) telegram_id = Column(String, unique=True, nullable=False) - username = Column(String) # email 3x-ui + 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") + requests = relationship("Requests", back_populates="user") + admins = relationship("Administrators", back_populates="user") #Подписки class Subscription(Base): @@ -57,20 +59,22 @@ class Transaction(Base): class Requests(Base): __tablename__ = 'requests' - id = Column(String,primary_key=True,default=generate_uuid) - user_id = Column(String,ForeignKey('users.id')) + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey('users.id')) username = Column(String) - created_at = Column(DateTime,default=datetime.now) + created_at = Column(DateTime, default=datetime.now) content = Column(String) - status = Column(String,default='open') - user = relationship("User",back_populates="requests") + status = Column(String, default='open') + + user = relationship("User", back_populates="requests") + class Administrators(Base): __tablename__ = 'admins' id = Column(String,primary_key=True,default=generate_uuid) user_id = Column(String,ForeignKey('users.id')) - admin = Column(bool,default=False) + admin = Column(Boolean,default=False) user = relationship("User",back_populates="admins") # VPN-серверы class VPNServer(Base): @@ -80,9 +84,10 @@ class VPNServer(Base): server_name = Column(String) ip_address = Column(String) port = Column(Integer) - login_data = Column(Text) - inbound = Column(Text) - config = Column(Text) + login = Column(String) + password = Column(String) + config = Column(String) + secret = Column(String) current_users = Column(Integer, default=0) max_users = Column(Integer, default=4) diff --git a/db_operations.py b/db_operations.py deleted file mode 100644 index be5d785..0000000 --- a/db_operations.py +++ /dev/null @@ -1,74 +0,0 @@ - -from service import UserService - -#DataBase -from sqlalchemy import desc -from db import get_db_session -from db import User -from db import Subscription -from db import Transaction -from db import VPNServer - -import json - -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 -def get_sub_list(session,count,user_id): - subscriptions = ( - session.query(Subscription) - .filter(Subscription.user_id == str(user_id)) - .order_by(desc(Subscription.created_at)) - .limit(count) # Ограничиваем результат 10 записями - .all() # Получаем все записи - ) - return subscriptions - -def create_user(telegram_id: str, username: str = None, balance: float = 0.0): - db = next(get_db_session()) - try: - new_user = User( - telegram_id=telegram_id, - username=username, - balance=balance - ) - db.add(new_user) - db.commit() - db.refresh(new_user) - return new_user - except Exception as e: - db.rollback() - raise e - finally: - db.close() - -def buy_sub(session, telegram_id: str, plan: str, logger): - try: - user = session.query(User).filter(User.telegram_id == str(telegram_id)).first() - if user is None: - logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.") - return "error" - - current_plan = config['subscription_templates'].get(plan) - if current_plan is None: - logger.error(f"Тариф {plan} не найден в шаблонах.") - return "error" - - if user.balance == current_plan['cost']: - service = UserService(logger) - service.tariff_setting(user, plan, current_plan['duration']) - return "OK" - else: - logger.error(f"Недостаточно средств на счету пользователя {telegram_id} для тарифа {plan}.") - return "error" - except Exception as e: - logger.error(f"Ошибка при покупке тарифа для пользователя {telegram_id}: {e}") diff --git a/panel.py b/panel.py index f4e6388..458628e 100644 --- a/panel.py +++ b/panel.py @@ -9,27 +9,11 @@ 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 @@ -47,7 +31,8 @@ class PanelInteraction: def login(self): login_url = self.base_url + "/login" - response = requests.post(login_url, data=self.login_data) + self.logger.info(f"Login URL : {login_url}") + response = requests.post(login_url, data=self.login_data, verify=False) if response.status_code == 200: session_id = response.cookies.get("3x-ui") return session_id @@ -55,27 +40,32 @@ class PanelInteraction: self.logger.error(f"Login failed: {response.status_code}") return None - def getInboundInfo(self,inboundId): + 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 + try: + response = requests.get(url, headers=self.headers, verify=False) + 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 + finally: + self.logger.info("Finished attempting to get inbound info.") - 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 - + try: + response = requests.get(url, headers=self.headers, verify=False) + 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 + finally: + self.logger.info("Finished attempting to get client traffic.") + def update_client_expiry(self, client_uuid, new_expiry_time, client_email): url = f"{self.base_url}/panel/api/inbounds/updateClient" update_data = { @@ -96,23 +86,27 @@ class PanelInteraction: ] }) } - 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}") + try: + response = requests.post(url, headers=self.headers, json=update_data, verify=False) + 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}") + finally: + self.logger.info("Finished attempting to update client expiry.") - def add_client(self, inbound_id, months): + def add_client(self, inbound_id, expiry_date,email): url = f"{self.base_url}/panel/api/inbounds/addClient" client_info = { "clients": [ { "id": generate_uuid(), "alterId": 0, - "email": generate_random_string(), + "email": email, "limitIp": 2, "totalGB": 0, - "expiryTime": generate_date(months), + "flow":"xtls-rprx-vision", + "expiryTime": expiry_date, "enable": True, "tgId": "", "subId": "" @@ -123,11 +117,13 @@ class PanelInteraction: "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 + 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) + return None + finally: + self.logger.info("Finished attempting to add client.") diff --git a/service.py b/service.py index 502cf14..dabd5b8 100644 --- a/service.py +++ b/service.py @@ -1,23 +1,32 @@ -from db import User -from db import Subscription -from db import Transaction -from db import VPNServer +from db import User, Subscription, Transaction, VPNServer +import string +import secrets +import json from sqlalchemy import desc -from datetime import datetime,timedelta +from dateutil.relativedelta import relativedelta +from datetime import datetime from db import get_db_session -import json from panel import PanelInteraction -with open('config.json', 'r') as file : config = json.load(file) + +def generate_random_string(length=8): + characters = string.ascii_letters + string.digits + return ''.join(secrets.choice(characters) for _ in range(length)) + + +# Загрузка конфигурации один раз +with open('config.json', 'r') as file: + config = json.load(file) + class UserService: - def __init__(self,logger): + def __init__(self, logger): self.logger = logger - - def add_user(self,telegram_id: int, username: str): + + def add_user(self, telegram_id: int): session = next(get_db_session()) try: - new_user = User(telegram_id=telegram_id, username=username) + new_user = User(telegram_id=telegram_id, username=generate_random_string()) session.add(new_user) session.commit() except Exception as e: @@ -26,7 +35,7 @@ class UserService: finally: session.close() - def get_user_by_telegram_id(self,telegram_id: int): + 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() @@ -35,35 +44,48 @@ class UserService: finally: session.close() - def add_transaction(self,user_id: int,amount: float): + def add_transaction(self, user_id: int, amount: float): session = next(get_db_session()) try: - transaction = Transaction(user_id = user_id,amount = amount) + transaction = Transaction(user_id=user_id, amount=amount) session.add(transaction) session.commit() except Exception as e: - self.logger.error(f"Ошибка добавления транзакции:{e}") + self.logger.error(f"Ошибка добавления транзакции: {e}") finally: session.close() - def pop_up_balance(self,telegram_id: int,amount: float): + 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) + 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() + session.rollback() + self.logger.error(f"Ошибка при обновлении баланса: {e}") finally: session.close() - def tariff_setting(self, user, plan: str, expiry_duration): + 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 = ( @@ -72,59 +94,160 @@ class UserService: .order_by(VPNServer.current_users.asc()) .first() ) - - if server is None: - self.logger.error("Нет доступных VPN серверов.") - return - expiry_ = datetime.now() + timedelta(days=expiry_duration) + if not server: + self.logger.error("Нет доступных VPN серверов.") + return "120" + + # Рассчитываем дату окончания подписки + expiry_ = datetime.now() + relativedelta(months=expiry_duration) + self.logger.info(f"Создание подписки для пользователя {user.id} на сервере {server.id} с планом {plan} до {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"Подписка успешно создана для пользователя {user.id}") + return "OK" except Exception as e: self.logger.error(f"Ошибка в установке тарифа: {e}") + return "Ошибка" finally: session.close() - - - def create_uri(self,telegram_id,): + 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 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) + if not user: + self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.") + return "error" + + current_plan = config['subscription_templates'].get(plan) + if not current_plan: + self.logger.error(f"Тариф {plan} не найден в шаблонах.") + return "error" + + cost = current_plan['cost'] + if user.balance >= cost: + user.balance -= cost session.commit() - return URI + 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"Недостаточно средств на счету пользователя {telegram_id} для тарифа {plan}.") + return 100 + except Exception as e: - self.logger.error(f"Чё то ошибка в создании uri: {e}") + self.logger.error(f"Ошибка при покупке тарифа для пользователя {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, + } + + # Преобразование server.config из строки в словарь + 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) + # Проверяем тип vpn_config и загружаем его, если это строка + 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"] - # Поиск клиента по email (CIF3) + # Обрабатываем настройки клиентов + clients = json.loads(obj["settings"])["clients"] if isinstance(obj["settings"], str) else obj["settings"]["clients"] + for client in clients: - if client["email"] == CIF3: + if client["email"] == CIF3['obj']['email']: uuid = client["id"] flow = client["flow"] - + # Извлечение параметров из streamSettings - stream_settings = json.loads(obj["streamSettings"]) + 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"] @@ -132,19 +255,15 @@ class UserService: short_id = stream_settings["realitySettings"]["shortIds"][0] # Первый короткий ID # Сборка строки VLess - URI = ( + 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}" ) - - return URI - # Если клиент с указанным email не найден - self.logger.warning(f"Клиент с email {CIF3} не найден.") + self.logger.error(f"Клиент с email {CIF3} не найден.") return None except Exception as e: - self.logger.error(f"Ошибка в методе создания uri: {e}") + self.logger.error(f"Ошибка в методе создания URI: {e}") return None -