From c37975941866bca60e6cd1eeb8002b833ac3eb26 Mon Sep 17 00:00:00 2001 From: Disledg Date: Sun, 1 Dec 2024 11:31:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=BD=D0=B0=D1=85=D1=83=D0=B9=20=D0=B2=D1=81?= =?UTF-8?q?=D1=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 14 ++ Middleware/anti_spam_middleware.py | 28 ++++ bot.py | 139 ------------------- databases/db_config.py | 74 ++++++++++ databases/model.py | 60 +++++++++ dal/db_Mong.py => databases/mongodb.py | 7 +- databases/postgresql.py | 105 +++++++++++++++ docker-compose.yml | 54 ++++++++ handlers/handlers.py | 178 +++++++++++++++++++++++++ keyboard/keyboards.py | 39 ++++++ main.py | 48 +++++++ models/db_Post.py | 94 ------------- requirements.txt | 40 ++++++ service/service.py | 24 ++-- utils/{logger_config.py => LogCon.py} | 17 ++- utils/panel.py | 53 +++++--- 16 files changed, 702 insertions(+), 272 deletions(-) create mode 100644 Dockerfile create mode 100644 Middleware/anti_spam_middleware.py delete mode 100644 bot.py create mode 100644 databases/db_config.py create mode 100644 databases/model.py rename dal/db_Mong.py => databases/mongodb.py (94%) create mode 100644 databases/postgresql.py create mode 100644 docker-compose.yml create mode 100644 handlers/handlers.py create mode 100644 keyboard/keyboards.py create mode 100644 main.py delete mode 100644 models/db_Post.py create mode 100644 requirements.txt rename utils/{logger_config.py => LogCon.py} (54%) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e4aa934 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Используем базовый Python-образ +FROM python:3.12-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем файлы проекта +COPY . . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Указываем команду запуска бота +CMD ["python", "main.py"] diff --git a/Middleware/anti_spam_middleware.py b/Middleware/anti_spam_middleware.py new file mode 100644 index 0000000..804c344 --- /dev/null +++ b/Middleware/anti_spam_middleware.py @@ -0,0 +1,28 @@ +from aiogram.dispatcher.middlewares.base import BaseMiddleware +from aiogram.types import TelegramObject +import time + + +class AntiSpamMiddleware(BaseMiddleware): + def __init__(self, rate_limit=1): + super().__init__() + self.rate_limit = rate_limit + self.users = {} + + async def __call__(self, handler, event: TelegramObject, data: dict): + user_id = None + + # Определяем user_id для события + if hasattr(event, "from_user") and event.from_user: + user_id = event.from_user.id + + if user_id: + now = time.time() + last_time = self.users.get(user_id) + if last_time and now - last_time < self.rate_limit: + # Если сообщение отправлено слишком быстро, игнорируем + return + self.users[user_id] = now + + # Если прошло достаточно времени, продолжаем обработку + return await handler(event, data) diff --git a/bot.py b/bot.py deleted file mode 100644 index 11b08ea..0000000 --- a/bot.py +++ /dev/null @@ -1,139 +0,0 @@ -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import Application, CallbackQueryHandler, ContextTypes, MessageHandler, filters,CommandHandler -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: - 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: - 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: - buttons = [("Главное меню", "start")] - await send_loading_message(update, context, 'Игорь чё нить напишет, я продублирую', create_keyboard(buttons)) - -# Функция для отображения поддержки -async def support(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - buttons = [("Главное меню", "start"), ("Написать", "sup")] - await send_loading_message(update, context, 'Для связи с поддержкой выберите "Написать" и изложите проблему в одном сообщении.', create_keyboard(buttons)) - -# Функция для пополнения баланса -async def pop_up(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - buttons = [("Главное меню", "start")] - await send_loading_message(update, context, 'Когда-нибудь эта функция заработает', create_keyboard(buttons)) - -# Функция для покупки подписки -async def buy_subscription(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - 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: - buttons = [("Главное меню", "start")] - text = 'У вас уже приобретена подписка' - - await send_loading_message(update, context, text, create_keyboard(buttons)) - -# Функция для отображения FAQ -async def faq(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - 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) - - try: - if data == 'account': - await personal_account(update, context) - elif data == 'start': - await start(update, context) - elif data == 'about': - await about(update, context) - elif data == 'support': - await support(update, context) - elif data == 'sup': - context.user_data['awaiting_input'] = True - elif data == 'pop_up': - await pop_up(update, context) - elif data == 'buy_tarif': - await buy_subscription(update, context) - elif data == 'faq': - await faq(update, context) - elif data == 'payment_history': - await active_sub(update, context) - 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"Ошибка при обработке запроса пользователя {tgid}: {e}") - await query.message.reply_text("Произошла ошибка. Пожалуйста, попробуйте снова.") - -def main() -> None: - init_db() - application = Application.builder().token(config['token']).build() - - application.add_handler(CommandHandler("start", start)) - - application.add_handler(CallbackQueryHandler(button_handler)) - application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, sup)) - application.run_polling(allowed_updates=Update.ALL_TYPES) - -if __name__ == "__main__": - main() diff --git a/databases/db_config.py b/databases/db_config.py new file mode 100644 index 0000000..e7dd20c --- /dev/null +++ b/databases/db_config.py @@ -0,0 +1,74 @@ +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 +from utils.LogCon import setup_logger, load_config + +# Загружаем конфигурацию +config = load_config() + +# Настройки PostgreSQL +postgres_user = config['postgreSQL']['username'] +postgres_password = config['postgreSQL']['password_DB'] +postgres_host = "postgres" # Хост для PostgreSQL в Docker +POSTGRES_DSN = f"postgresql+asyncpg://{postgres_user}:{postgres_password}@{postgres_host}:5432/bot_db" + +# Создание движка для PostgreSQL +postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) +AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False) + +# Настройки MongoDB +mongodb_user = config['mongodb']['mongodb_username'] +mongodb_password = config['mongodb']['mongodb_password'] +mongodb_host = "mongodb" # Хост для MongoDB в Docker +mongodb_uri = f"mongodb://{mongodb_user}:{mongodb_password}@{mongodb_host}:27017" +database_name = config['mongodb']['database_name'] + +# Создание клиента MongoDB +mongo_client = AsyncIOMotorClient(mongodb_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.") diff --git a/databases/model.py b/databases/model.py new file mode 100644 index 0000000..cea7a32 --- /dev/null +++ b/databases/model.py @@ -0,0 +1,60 @@ +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") diff --git a/dal/db_Mong.py b/databases/mongodb.py similarity index 94% rename from dal/db_Mong.py rename to databases/mongodb.py index 5fdf90b..e87580a 100644 --- a/dal/db_Mong.py +++ b/databases/mongodb.py @@ -1,10 +1,9 @@ from pymongo import MongoClient -import json +from utils.LogCon import setup_logger, load_config -class VPNServerRepository: +class MongoDBRepository: def __init__(self, config_path="config.json"): - with open(config_path, "r") as file: - config = json.load(file) + self.config = load_config() self.client = MongoClient(config["mongodb_uri"]) self.db = self.client[config["database_name"]] self.collection = self.db["vpn_servers"] diff --git a/databases/postgresql.py b/databases/postgresql.py new file mode 100644 index 0000000..faa717a --- /dev/null +++ b/databases/postgresql.py @@ -0,0 +1,105 @@ +from databases.model import User, Subscription, Transaction, Administrators +from sqlalchemy.future import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy import desc +import random +import string +import logging + + +class DatabaseManager: + def __init__(self, session_generator): + """ + Инициализация с асинхронным генератором сессий (например, get_postgres_session). + """ + self.session_generator = session_generator + self.logger = logging.getLogger(__name__) + + async def create_user(self, telegram_id): + """ + Создаёт нового пользователя, если его нет. + """ + async for session in self.session_generator(): + try: + username = self.generate_string(6) + result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + user = result.scalars().first() + if not user: + new_user = User(telegram_id=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 = amount + await self.add_transaction(user.id, amount) + await session.commit() + else: + self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.") + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при обновлении баланса: {e}") + await session.rollback() + + 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)) + ) + print(result) + return result.scalars().all() + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при получении последней подписки пользователя {user_id}: {e}") + return "ERROR" + + @staticmethod + def generate_string(length): + """ + Генерирует случайную строку заданной длины. + """ + characters = string.ascii_lowercase + string.digits + return ''.join(random.choices(characters, k=length)) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2d352cd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +version: "3.8" + +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: root + MONGO_INITDB_ROOT_PASSWORD: itOj4CE2miKR + volumes: + - mongo_data:/data/db + + postgres: + networks: + - bot_network + image: postgres:latest + container_name: postgres + ports: + - "5432:5432" + environment: + POSTGRES_USER: AH3J9GSPBYOP + POSTGRES_PASSWORD: uPS9?y~mcu2 + POSTGRES_DB: bot_db + volumes: + - postgres_data:/var/lib/postgresql/data + + bot: + networks: + - bot_network + build: + context: . + container_name: telegram_bot + environment: + POSTGRES_URL: "postgresql://AH3J9GSPBYOP:uPS9?y~mcu2@postgres:5432/bot_db" + MONGO_URL: "mongodb://root:itOj4CE2miKR@mongodb:27017" + volumes: + - logs_data:/app/logs # Логи сохраняются в контейнере + depends_on: + - postgres + - mongodb + command: ["python", "main.py"] # Задаем явную команду запуска diff --git a/handlers/handlers.py b/handlers/handlers.py new file mode 100644 index 0000000..7485cea --- /dev/null +++ b/handlers/handlers.py @@ -0,0 +1,178 @@ +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, popup_keyboard, main_keyboard, account_keyboard, buy_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("Привет! Я ваш Telegram-бот.", 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: + await callback.message.edit_text( + f"Ваш профиль:\n" + f"👤 Username: {user.username}\n", + 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=buy_keyboard() + ) + else: + await callback.message.edit_text("Вы еще не зарегистрированы.") + await callback.answer() + + +async def popup_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: + await callback.message.edit_text( + f"Ты думал здесь что то будет?", + reply_markup=account_keyboard() + ) + else: + await callback.message.edit_text("Вы еще не зарегистрированы.") + await callback.answer() + +async def subhist_callback_handler(callback: types.CallbackQuery): + """ + Обработчик callback_query с data="profile". + """ + subs = await db_manager.last_subscription(telegram_id=callback.from_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 subhist_callback_handler(callback: types.CallbackQuery): + """ + Обработчик callback_query с data="subhist". + """ + user = await db_manager.get_user_by_telegram_id(telegram_id=callback.from_user.id) + subs = await db_manager.last_subscription(user.id) + if subs == "ERROR": + await callback.message.answer( + "Произошла ошибка, попробуйте позже или свяжитесь с администрацией." + ) + await callback.answer() + return + if not subs: + 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 sub: + await callback.message.edit_text( + result, + reply_markup=account_keyboard() + ) + else: + await callback.message.edit_text("Вы еще не зарегистрированы.") + await callback.answer() + + +def register_handlers(dp: Dispatcher): + """ + Регистрация хэндлеров в диспетчере. + """ + dp.callback_query.register(popup_callback_handler, lambda c: c.data == "popup") + dp.callback_query.register(subhist_callback_handler, lambda c: c.data == "subs") + dp.callback_query.register(profile_callback_handler, lambda c: c.data == "profile") + dp.message.register(start_command, Command("start")) + dp.callback_query.register(balance_callback_handler, lambda c: c.data == "balance") diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py new file mode 100644 index 0000000..ac17ad6 --- /dev/null +++ b/keyboard/keyboards.py @@ -0,0 +1,39 @@ +from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.types import InlineKeyboardButton + +def main_keyboard(): + # Создаём билдер для клавиатуры + builder = InlineKeyboardBuilder() + + # Добавляем кнопки + builder.button(text="Профиль", callback_data="profile") + builder.button(text="FAQ", callback_data="faq") + builder.button(text="О нас", callback_data="about") + + # Строим клавиатуру и возвращаем её + return builder.as_markup() + +def account_keyboard(): + builder = InlineKeyboardBuilder() + builder.button(text="Баланс", callback_data="balance") + builder.button(text="Приобрести подписку", callback_data="buy_subscription") + builder.button(text="Руководство по подключению", callback_data="guide") + return builder.as_markup() + +def buy_keyboard(): + builder = InlineKeyboardBuilder() + builder.button(text="Подписки", callback_data="subs") + builder.button(text="О тарифах", callback_data="about_tarifs") + builder.button(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.button(text="Хуй знает что здесь", callback_data="unknown") + builder.button(text="Назад", callback_data="profile") + return builder.as_markup() diff --git a/main.py b/main.py new file mode 100644 index 0000000..c9b4229 --- /dev/null +++ b/main.py @@ -0,0 +1,48 @@ +import asyncio +from aiogram import Bot, Dispatcher +from databases.db_config import init_postgresql, init_mongodb, close_connections +from aiogram.types import BotCommand +from utils.LogCon import setup_logger, load_config +from Middleware.anti_spam_middleware import AntiSpamMiddleware +import logging + + +setup_logger() +logger = logging.getLogger(__name__) + +BOT_TOKEN = load_config()['token'] + +bot = Bot(token=BOT_TOKEN) +dp = Dispatcher() + +dp.message.middleware(AntiSpamMiddleware(rate_limit=1)) + + +async def set_commands(): + commands = [ + BotCommand(command="/start", description="Запустить бота"), + ] + await bot.set_my_commands(commands) + +async def on_startup(): + await init_mongodb() + await set_commands() + print("Бот запущен!") + +async def on_shutdown(): + await close_connections() + await bot.session.close() + print("Бот остановлен.") + +async def main(): + from handlers.handlers import register_handlers + register_handlers(dp) + await init_postgresql() # Убедитесь, что таблицы создаются здесь + await on_startup() + try: + await dp.start_polling(bot) + finally: + await on_shutdown() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/models/db_Post.py b/models/db_Post.py deleted file mode 100644 index a792795..0000000 --- a/models/db_Post.py +++ /dev/null @@ -1,94 +0,0 @@ -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 -from datetime import datetime -import json -import uuid -with open('config.json', 'r') as file : config = json.load(file) - -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") - requests = relationship("Requests", 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, 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") - -#Транзакции -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") - -class Requests(Base): - __tablename__ = 'requests' - - 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) - content = Column(String) - 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(Boolean,default=False) - user = relationship("User",back_populates="admins") - -# Настройка подключения к базе данных -DATABASE_URL = f"postgresql://{config['username']}:{config['password_DB']}@localhost/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/requirements.txt b/requirements.txt new file mode 100644 index 0000000..224c87a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,40 @@ +aiofiles==24.1.0 +aiogram==3.15.0 +aiohappyeyeballs==2.4.3 +aiohttp==3.10.11 +aiosignal==1.3.1 +annotated-types==0.7.0 +anyio==4.6.0 +asyncpg==0.30.0 +attrs==24.2.0 +certifi==2024.8.30 +charset-normalizer==3.4.0 +DateTime==5.5 +dnspython==2.7.0 +frozenlist==1.5.0 +greenlet==3.1.1 +h11==0.14.0 +httpcore==1.0.6 +httpx==0.27.2 +idna==3.10 +magic-filter==1.0.12 +motor==3.6.0 +multidict==6.1.0 +propcache==0.2.0 +psycopg2-binary==2.9.10 +pydantic==2.9.2 +pydantic_core==2.23.4 +pymongo==4.9.2 +python-dateutil==2.9.0.post0 +python-telegram-bot==21.6 +pytz==2024.2 +requests==2.32.3 +setuptools==75.1.0 +six==1.16.0 +sniffio==1.3.1 +SQLAlchemy==2.0.35 +telegram==0.0.1 +typing_extensions==4.12.2 +urllib3==2.2.3 +yarl==1.17.2 +zope.interface==7.1.0 \ No newline at end of file diff --git a/service/service.py b/service/service.py index 9f6f62e..406b78f 100644 --- a/service/service.py +++ b/service/service.py @@ -8,15 +8,15 @@ 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)) -with open('config.json', 'r') as file: - config = json.load(file) - class UserService: def __init__(self, logger): @@ -95,21 +95,21 @@ class UserService: ) if not server: - self.logger.error("Нет доступных VPN серверов.") + self.logger.error("Error: 120") return "120" # Рассчитываем дату окончания подписки - expiry_ = datetime.now() + relativedelta(months=expiry_duration) - self.logger.info(f"Создание подписки для пользователя {user.id} на сервере {server.id} с планом {plan} до {expiry_}") + 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"Подписка успешно создана для пользователя {user.id}") + self.logger.info(f"Subscribe successfully created for {user.id}") return "OK" except Exception as e: - self.logger.error(f"Ошибка в установке тарифа: {e}") + self.logger.error(f"Error with created subscribe: {e}") return "Ошибка" finally: session.close() @@ -119,12 +119,12 @@ class UserService: try: user = session.query(User).filter(User.telegram_id == telegram_id).first() if not user: - self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.") + 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"Тариф {plan} не найден в шаблонах.") + self.logger.error(f"Tarif {plan} not found.") return "error" cost = current_plan['cost'] @@ -141,11 +141,11 @@ class UserService: else: return "ERROR " + result - self.logger.error(f"Недостаточно средств на счету пользователя {telegram_id} для тарифа {plan}.") + self.logger.error(f"Nt enough money {telegram_id} for {plan}.") return 100 except Exception as e: - self.logger.error(f"Ошибка при покупке тарифа для пользователя {telegram_id}: {e}") + self.logger.error(f"Error with buying sub {telegram_id}: {e}") session.rollback() finally: session.close() diff --git a/utils/logger_config.py b/utils/LogCon.py similarity index 54% rename from utils/logger_config.py rename to utils/LogCon.py index 6088cdf..2f741d4 100644 --- a/utils/logger_config.py +++ b/utils/LogCon.py @@ -1,12 +1,16 @@ import logging from logging.handlers import TimedRotatingFileHandler +import json +import os + + def setup_logger(): logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) handler = TimedRotatingFileHandler( - "/logs/app.log", + "logs/app.log", when="midnight", interval=1, backupCount=7, @@ -19,3 +23,14 @@ def setup_logger(): logger.addHandler(handler) return logger + + +def load_config(config_path='config/config.json'): + """ + Загрузка конфигурации из JSON файла. + """ + if not os.path.exists(config_path): + raise FileNotFoundError(f"Конфигурационный файл не найден: {config_path}") + + with open(config_path, 'r') as file: + return json.load(file) diff --git a/utils/panel.py b/utils/panel.py index 458628e..850bca3 100644 --- a/utils/panel.py +++ b/utils/panel.py @@ -3,11 +3,11 @@ import uuid import string import secrets import json -from logger_config import setup_logger +from utils.LogCon import setup_logger, load_config from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta -with open('config.json', 'r') as file : config = json.load(file) +config = load_config() @@ -31,41 +31,49 @@ class PanelInteraction: def login(self): login_url = self.base_url + "/login" - self.logger.info(f"Login URL : {login_url}") - response = requests.post(login_url, data=self.login_data, verify=False) - if response.status_code == 200: + 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") - return session_id - else: - self.logger.error(f"Login failed: {response.status_code}") + 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) - if response.status_code == 200: + 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) return None - finally: - self.logger.info("Finished attempting to get inbound info.") + except requests.RequestException as e: + self.logger.error(f"Get inbound request failed: {e}") def get_client_traffic(self, email): url = f"{self.base_url}/panel/api/inbounds/getClientTraffics/{email}" try: - response = requests.get(url, headers=self.headers, verify=False) - if response.status_code == 200: + 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) return None - finally: - self.logger.info("Finished attempting to get client traffic.") - + 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): url = f"{self.base_url}/panel/api/inbounds/updateClient" update_data = { @@ -88,12 +96,13 @@ class PanelInteraction: } try: response = requests.post(url, headers=self.headers, json=update_data, verify=False) - if response.status_code == 200: + 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}") - finally: - self.logger.info("Finished attempting to update client expiry.") + 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): url = f"{self.base_url}/panel/api/inbounds/addClient"