From 14f975d4a0f38bc31b5745c31d6797d75792bf3c Mon Sep 17 00:00:00 2001 From: hoshimach1 Date: Thu, 8 Jan 2026 04:24:15 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=BE=D0=B2=20=D0=B2=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 8 ++ .env.example | 7 ++ config.py | 20 ++++ main.py | 280 ++++++++++++++++++++++++++++----------------------- 4 files changed, 189 insertions(+), 126 deletions(-) create mode 100644 .env create mode 100644 .env.example create mode 100644 config.py diff --git a/.env b/.env new file mode 100644 index 0000000..de9cf71 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +BOT_TOKEN=8406127231:AAG5m0Ft0UUyTW2KI-jwYniXtIRcbSdlxf8 +MARZBAN_URL=http://144.31.66.170:7575/ +MARZBAN_USERNAME=admin +MARZBAN_PASSWORD=rY4tU8hX4nqF +# Оставьте пустым для использования SQLite (создаст файл bot.db) +# DATABASE_URL=postgresql://user:password@localhost/vpnbot +ADMIN_IDS=123456789 +PROVIDER_TOKEN= diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aa2098b --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +BOT_TOKEN= +MARZBAN_URL= +MARZBAN_USERNAME= +MARZBAN_PASSWORD= +# DATABASE_URL=postgresql://user:password@localhost/vpnbot +ADMIN_IDS= +PROVIDER_TOKEN= diff --git a/config.py b/config.py new file mode 100644 index 0000000..03e0bcf --- /dev/null +++ b/config.py @@ -0,0 +1,20 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +CONFIG = { + "BOT_TOKEN": os.getenv("BOT_TOKEN"), + "MARZBAN_URL": os.getenv("MARZBAN_URL"), + "MARZBAN_USERNAME": os.getenv("MARZBAN_USERNAME"), + "MARZBAN_PASSWORD": os.getenv("MARZBAN_PASSWORD"), + "DATABASE_URL": os.getenv("DATABASE_URL"), + "ADMIN_IDS": [int(i.strip()) for i in os.getenv("ADMIN_IDS", "").split(",") if i.strip()], + "PROVIDER_TOKEN": os.getenv("PROVIDER_TOKEN", ""), +} + +PLANS = { + "month_1": {"name": "1 месяц", "days": 30, "price": 100, "data_limit": 50}, + "month_3": {"name": "3 месяца", "days": 90, "price": 270, "data_limit": 150}, + "month_6": {"name": "6 месяцев", "days": 180, "price": 500, "data_limit": 300}, +} diff --git a/main.py b/main.py index 495837c..61539be 100644 --- a/main.py +++ b/main.py @@ -16,25 +16,9 @@ from aiogram.types import ( InlineKeyboardButton, LabeledPrice, PreCheckoutQuery ) import aiohttp -import asyncpg +import aiosqlite +from config import CONFIG, PLANS -# Конфигурация -CONFIG = { - "BOT_TOKEN": "YOUR_BOT_TOKEN", - "MARZBAN_URL": "https://your-panel.com", - "MARZBAN_USERNAME": "admin", - "MARZBAN_PASSWORD": "your_password", - "DATABASE_URL": "postgresql://user:password@localhost/vpnbot", - "ADMIN_IDS": [123456789], # ID администраторов - "PROVIDER_TOKEN": "", # Для Telegram Stars оставляем пустым -} - -# Тарифные планы -PLANS = { - "month_1": {"name": "1 месяц", "days": 30, "price": 100, "data_limit": 50}, - "month_3": {"name": "3 месяца", "days": 90, "price": 270, "data_limit": 150}, - "month_6": {"name": "6 месяцев", "days": 180, "price": 500, "data_limit": 300}, -} logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -132,143 +116,187 @@ class MarzbanAPI: # Database Manager class Database: - def __init__(self, url: str): + def __init__(self, url: Optional[str]): self.url = url + self.is_sqlite = not url or not url.startswith("postgresql://") self.pool = None + self.conn = None # For SQLite async def init_pool(self): - self.pool = await asyncpg.create_pool(self.url) + if self.is_sqlite: + db_path = "bot.db" + self.conn = await aiosqlite.connect(db_path) + self.conn.row_factory = aiosqlite.Row + logger.info(f"Using SQLite database: {db_path}") + else: + self.pool = await asyncpg.create_pool(self.url) + logger.info("Using PostgreSQL database") await self.create_tables() + async def execute(self, query: str, *args): + if self.is_sqlite: + query = query.replace("$1", "?").replace("$2", "?").replace("$3", "?").replace("$4", "?") + async with self.conn.execute(query, args) as cursor: + await self.conn.commit() + return cursor + else: + async with self.pool.acquire() as conn: + return await conn.execute(query, *args) + + async def fetchrow(self, query: str, *args): + if self.is_sqlite: + query = query.replace("$1", "?").replace("$2", "?").replace("$3", "?").replace("$4", "?") + async with self.conn.execute(query, args) as cursor: + return await cursor.fetchone() + else: + async with self.pool.acquire() as conn: + return await conn.fetchrow(query, *args) + + async def fetchval(self, query: str, *args): + if self.is_sqlite: + query = query.replace("$1", "?").replace("$2", "?").replace("$3", "?").replace("$4", "?") + async with self.conn.execute(query, args) as cursor: + row = await cursor.fetchone() + return row[0] if row else None + else: + async with self.pool.acquire() as conn: + return await conn.fetchval(query, *args) + + async def fetch(self, query: str, *args): + if self.is_sqlite: + query = query.replace("$1", "?").replace("$2", "?").replace("$3", "?").replace("$4", "?") + async with self.conn.execute(query, args) as cursor: + return await cursor.fetchall() + else: + async with self.pool.acquire() as conn: + return await conn.fetch(query, *args) + async def create_tables(self): - async with self.pool.acquire() as conn: - await conn.execute(""" - CREATE TABLE IF NOT EXISTS users ( - user_id BIGINT PRIMARY KEY, - username TEXT, - marzban_username TEXT UNIQUE, - subscription_until TIMESTAMP, - data_limit INTEGER, - invited_by BIGINT, - created_at TIMESTAMP DEFAULT NOW() - ) - """) - await conn.execute(""" - CREATE TABLE IF NOT EXISTS invite_codes ( - code TEXT PRIMARY KEY, - created_by BIGINT, - used_by BIGINT, - used_at TIMESTAMP, - created_at TIMESTAMP DEFAULT NOW() - ) - """) - await conn.execute(""" - CREATE TABLE IF NOT EXISTS promo_codes ( - code TEXT PRIMARY KEY, - discount INTEGER, - uses_left INTEGER, - created_by BIGINT, - created_at TIMESTAMP DEFAULT NOW() - ) - """) - await conn.execute(""" - CREATE TABLE IF NOT EXISTS payments ( - id SERIAL PRIMARY KEY, - user_id BIGINT, - plan TEXT, - amount INTEGER, - promo_code TEXT, - paid_at TIMESTAMP DEFAULT NOW() - ) - """) + now_default = "CURRENT_TIMESTAMP" if self.is_sqlite else "NOW()" + serial_type = "INTEGER PRIMARY KEY AUTOINCREMENT" if self.is_sqlite else "SERIAL PRIMARY KEY" + + queries = [ + f"""CREATE TABLE IF NOT EXISTS users ( + user_id BIGINT PRIMARY KEY, + username TEXT, + marzban_username TEXT UNIQUE, + subscription_until TIMESTAMP, + data_limit INTEGER, + invited_by BIGINT, + created_at TIMESTAMP DEFAULT {now_default} + )""", + f"""CREATE TABLE IF NOT EXISTS invite_codes ( + code TEXT PRIMARY KEY, + created_by BIGINT, + used_by BIGINT, + used_at TIMESTAMP, + created_at TIMESTAMP DEFAULT {now_default} + )""", + f"""CREATE TABLE IF NOT EXISTS promo_codes ( + code TEXT PRIMARY KEY, + discount INTEGER, + uses_left INTEGER, + created_by BIGINT, + created_at TIMESTAMP DEFAULT {now_default} + )""", + f"""CREATE TABLE IF NOT EXISTS payments ( + id {serial_type}, + user_id BIGINT, + plan TEXT, + amount INTEGER, + promo_code TEXT, + paid_at TIMESTAMP DEFAULT {now_default} + )""" + ] + for q in queries: + await self.execute(q) async def get_user(self, user_id: int): - async with self.pool.acquire() as conn: - return await conn.fetchrow("SELECT * FROM users WHERE user_id = $1", user_id) + return await self.fetchrow("SELECT * FROM users WHERE user_id = $1", user_id) async def create_user(self, user_id: int, username: str, marzban_username: str, invited_by: int = None): - async with self.pool.acquire() as conn: - await conn.execute( - "INSERT INTO users (user_id, username, marzban_username, invited_by) VALUES ($1, $2, $3, $4)", - user_id, username, marzban_username, invited_by - ) + await self.execute( + "INSERT INTO users (user_id, username, marzban_username, invited_by) VALUES ($1, $2, $3, $4)", + user_id, username, marzban_username, invited_by + ) async def update_subscription(self, user_id: int, days: int, data_limit: int): - async with self.pool.acquire() as conn: - user = await self.get_user(user_id) - if user and user['subscription_until'] and user['subscription_until'] > datetime.now(): - new_date = user['subscription_until'] + timedelta(days=days) - else: - new_date = datetime.now() + timedelta(days=days) - - await conn.execute( - "UPDATE users SET subscription_until = $1, data_limit = $2 WHERE user_id = $3", - new_date, data_limit, user_id - ) + user = await self.get_user(user_id) + + # SQLite returns Row object, datetime handling might need care + sub_until = user['subscription_until'] + if isinstance(sub_until, str): # SQLite might return it as string + sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S') if '.' not in sub_until else datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f') + + if user and sub_until and sub_until > datetime.now(): + new_date = sub_until + timedelta(days=days) + else: + new_date = datetime.now() + timedelta(days=days) + + await self.execute( + "UPDATE users SET subscription_until = $1, data_limit = $2 WHERE user_id = $3", + new_date, data_limit, user_id + ) async def create_invite_code(self, created_by: int): code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) - async with self.pool.acquire() as conn: - await conn.execute( - "INSERT INTO invite_codes (code, created_by) VALUES ($1, $2)", - code, created_by - ) + await self.execute( + "INSERT INTO invite_codes (code, created_by) VALUES ($1, $2)", + code, created_by + ) return code async def use_invite_code(self, code: str, user_id: int): - async with self.pool.acquire() as conn: - invite = await conn.fetchrow( - "SELECT * FROM invite_codes WHERE code = $1 AND used_by IS NULL", - code - ) - if not invite: - return False - await conn.execute( - "UPDATE invite_codes SET used_by = $1, used_at = NOW() WHERE code = $2", - user_id, code - ) - return invite['created_by'] + invite = await self.fetchrow( + "SELECT * FROM invite_codes WHERE code = $1 AND used_by IS NULL", + code + ) + if not invite: + return False + + now_val = datetime.now() + await self.execute( + "UPDATE invite_codes SET used_by = $1, used_at = $2 WHERE code = $3", + user_id, now_val, code + ) + return invite['created_by'] async def create_promo_code(self, code: str, discount: int, uses: int, created_by: int): - async with self.pool.acquire() as conn: - await conn.execute( - "INSERT INTO promo_codes (code, discount, uses_left, created_by) VALUES ($1, $2, $3, $4)", - code, discount, uses, created_by - ) + await self.execute( + "INSERT INTO promo_codes (code, discount, uses_left, created_by) VALUES ($1, $2, $3, $4)", + code, discount, uses, created_by + ) async def use_promo_code(self, code: str): - async with self.pool.acquire() as conn: - promo = await conn.fetchrow( - "SELECT * FROM promo_codes WHERE code = $1 AND uses_left > 0", - code - ) - if not promo: - return None - await conn.execute( - "UPDATE promo_codes SET uses_left = uses_left - 1 WHERE code = $1", - code - ) - return promo['discount'] + promo = await self.fetchrow( + "SELECT * FROM promo_codes WHERE code = $1 AND uses_left > 0", + code + ) + if not promo: + return None + await self.execute( + "UPDATE promo_codes SET uses_left = uses_left - 1 WHERE code = $1", + code + ) + return promo['discount'] async def add_payment(self, user_id: int, plan: str, amount: int, promo_code: str = None): - async with self.pool.acquire() as conn: - await conn.execute( - "INSERT INTO payments (user_id, plan, amount, promo_code) VALUES ($1, $2, $3, $4)", - user_id, plan, amount, promo_code - ) + await self.execute( + "INSERT INTO payments (user_id, plan, amount, promo_code) VALUES ($1, $2, $3, $4)", + user_id, plan, amount, promo_code + ) async def get_all_users(self): - async with self.pool.acquire() as conn: - return await conn.fetch("SELECT user_id FROM users") + return await self.fetch("SELECT user_id FROM users") async def get_stats(self): - async with self.pool.acquire() as conn: - total = await conn.fetchval("SELECT COUNT(*) FROM users") - active = await conn.fetchval( - "SELECT COUNT(*) FROM users WHERE subscription_until > NOW()" - ) - revenue = await conn.fetchval("SELECT SUM(amount) FROM payments") - return {"total": total, "active": active, "revenue": revenue or 0} + now_expr = "CURRENT_TIMESTAMP" if self.is_sqlite else "NOW()" + total = await self.fetchval("SELECT COUNT(*) FROM users") + active = await self.fetchval( + f"SELECT COUNT(*) FROM users WHERE subscription_until > {now_expr}" + ) + revenue = await self.fetchval("SELECT SUM(amount) FROM payments") + return {"total": total, "active": active, "revenue": revenue or 0} # Initialize bot = Bot(token=CONFIG["BOT_TOKEN"])