first step
This commit is contained in:
25
.env.example
Normal file
25
.env.example
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Telegram Bot
|
||||||
|
BOT_TOKEN=YOUR_BOT_TOKEN_HERE
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=botyobshik
|
||||||
|
|
||||||
|
# Support
|
||||||
|
SUPPORT_USERNAME=@support_username
|
||||||
|
|
||||||
|
# Payment
|
||||||
|
PAYMENT_CHANNEL_ID=-1003591520479
|
||||||
|
PAYMENT_THREAD_ID=5910
|
||||||
|
PAYMENT_TICKET_CHANNEL_ID=-1003591520479
|
||||||
|
PAYMENT_TICKET_THREAD_ID=788
|
||||||
|
PAYMENT_PHONE=+7 904 788 77 35
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
ADMIN_IDS=123456789,987654321
|
||||||
|
|
||||||
|
# Remnawave API
|
||||||
|
REMWAVE_API_URL=https://panel.remnawave.com/api
|
||||||
|
REMWAVE_API_KEY=your_api_key_here
|
||||||
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
7
.qwen/settings.json
Normal file
7
.qwen/settings.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebFetch(github.com)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# OreolRP Subscription Bot
|
||||||
59
config/__init__.py
Normal file
59
config/__init__.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Загрузка переменных окружения
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Класс для управления конфигурацией"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Telegram
|
||||||
|
self.bot_token = os.getenv("BOT_TOKEN", "")
|
||||||
|
|
||||||
|
# Database
|
||||||
|
self.db_host = os.getenv("DB_HOST", "localhost")
|
||||||
|
self.db_user = os.getenv("DB_USER", "root")
|
||||||
|
self.db_password = os.getenv("DB_PASSWORD", "")
|
||||||
|
self.db_name = os.getenv("DB_NAME", "botyobshik")
|
||||||
|
|
||||||
|
# Support
|
||||||
|
self.support_username = os.getenv("SUPPORT_USERNAME", "@support_username")
|
||||||
|
|
||||||
|
# Payment
|
||||||
|
self.payment_channel_id = os.getenv("PAYMENT_CHANNEL_ID", "")
|
||||||
|
self.payment_thread_id = os.getenv("PAYMENT_THREAD_ID", "")
|
||||||
|
self.payment_ticket_channel_id = os.getenv("PAYMENT_TICKET_CHANNEL_ID", "")
|
||||||
|
self.payment_ticket_thread_id = os.getenv("PAYMENT_TICKET_THREAD_ID", "")
|
||||||
|
self.payment_phone = os.getenv("PAYMENT_PHONE", "+7 904 788 77 35")
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
admin_ids_str = os.getenv("ADMIN_IDS", "")
|
||||||
|
self.admin_ids = [int(x.strip()) for x in admin_ids_str.split(",") if x.strip().isdigit()]
|
||||||
|
|
||||||
|
# Remnawave API
|
||||||
|
self.remwave_api_url = os.getenv("REMWAVE_API_URL", "")
|
||||||
|
self.remwave_api_key = os.getenv("REMWAVE_API_KEY", "")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_config(self) -> dict:
|
||||||
|
"""Конфигурация подключения к БД"""
|
||||||
|
return {
|
||||||
|
"host": self.db_host,
|
||||||
|
"user": self.db_user,
|
||||||
|
"password": self.db_password,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_config_with_name(self) -> dict:
|
||||||
|
"""Конфигурация подключения к БД с именем базы"""
|
||||||
|
return {
|
||||||
|
**self.db_config,
|
||||||
|
"database": self.db_name
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр конфигурации
|
||||||
|
config = Config()
|
||||||
518
core/__init__.py
Normal file
518
core/__init__.py
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
import mysql.connector
|
||||||
|
from mysql.connector import Error
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
import asyncio
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
|
||||||
|
def run_sync(func):
|
||||||
|
"""Декоратор для запуска синхронных функций в async"""
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
return await loop.run_in_executor(None, func, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
"""Класс для работы с базой данных"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.db_config = config.db_config
|
||||||
|
self.db_config_with_name = config.db_config_with_name
|
||||||
|
|
||||||
|
def get_connection(self, with_db: bool = True):
|
||||||
|
"""Подключение к MySQL"""
|
||||||
|
db_config = self.db_config_with_name if with_db else self.db_config
|
||||||
|
try:
|
||||||
|
connection = mysql.connector.connect(**db_config)
|
||||||
|
return connection
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка подключения к БД: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def create_database_if_not_exists(self) -> bool:
|
||||||
|
"""Создание базы данных если она не существует"""
|
||||||
|
connection = self.get_connection(with_db=False)
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
f"CREATE DATABASE IF NOT EXISTS `{config.db_name}` "
|
||||||
|
"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
print(f"База данных '{config.db_name}' проверена/создана")
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка создания БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def create_tables(self) -> bool:
|
||||||
|
"""Создание таблиц в базе данных"""
|
||||||
|
connection = self.get_connection(with_db=True)
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
# Таблица подписок
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS subscriptions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id BIGINT UNIQUE NOT NULL,
|
||||||
|
username VARCHAR(255),
|
||||||
|
is_active BOOLEAN DEFAULT FALSE,
|
||||||
|
subscription_start DATETIME,
|
||||||
|
subscription_end DATETIME,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Таблица языков пользователей
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_languages (
|
||||||
|
user_id BIGINT PRIMARY KEY,
|
||||||
|
language_code VARCHAR(10) DEFAULT 'ru',
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Таблица тикетов
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
username VARCHAR(255),
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
status VARCHAR(20) DEFAULT 'open',
|
||||||
|
admin_response TEXT,
|
||||||
|
closed_by VARCHAR(255),
|
||||||
|
closed_at DATETIME,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Индексы
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_id ON subscriptions(user_id)")
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
print("Таблицы созданы/проверены")
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка создания таблиц: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
"""Инициализация БД: создание БД и таблиц"""
|
||||||
|
await self.create_database_if_not_exists()
|
||||||
|
await self.create_tables()
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def get_user_subscription(self, user_id: int) -> bool:
|
||||||
|
"""Проверка статуса подписки пользователя"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT is_active FROM subscriptions WHERE user_id = %s",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return result['is_active'] if result else False
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def create_user(self, user_id: int, username: str) -> bool:
|
||||||
|
"""Создание нового пользователя в базе"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""INSERT INTO subscriptions (user_id, username, is_active)
|
||||||
|
VALUES (%s, %s, FALSE)
|
||||||
|
ON DUPLICATE KEY UPDATE username = %s""",
|
||||||
|
(user_id, username, username)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def activate_subscription(self, user_id: int, days: int = 30) -> bool:
|
||||||
|
"""Активация подписки на указанное количество дней"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
now = datetime.now()
|
||||||
|
end_date = now + timedelta(days=days)
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""INSERT INTO subscriptions (user_id, is_active, subscription_start, subscription_end)
|
||||||
|
VALUES (%s, TRUE, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
is_active = TRUE,
|
||||||
|
subscription_start = %s,
|
||||||
|
subscription_end = %s""",
|
||||||
|
(user_id, now, end_date, now, end_date)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def deactivate_subscription(self, user_id: int) -> bool:
|
||||||
|
"""Деактивация подписки"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE subscriptions SET is_active = FALSE WHERE user_id = %s",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def get_subscription_end_date(self, user_id: int) -> Optional[datetime]:
|
||||||
|
"""Получение даты окончания подписки"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT subscription_end FROM subscriptions WHERE user_id = %s",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return result['subscription_end'] if result and result['subscription_end'] else None
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def get_user_language(self, user_id: int) -> str:
|
||||||
|
"""Получение языка пользователя"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return "ru"
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT language_code FROM user_languages WHERE user_id = %s",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return result['language_code'] if result else "ru"
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return "ru"
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def set_user_language(self, user_id: int, language: str) -> bool:
|
||||||
|
"""Установка языка пользователя"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""INSERT INTO user_languages (user_id, language_code)
|
||||||
|
VALUES (%s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE language_code = %s""",
|
||||||
|
(user_id, language, language)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def create_ticket(self, user_id: int, username: str, message: str) -> Optional[int]:
|
||||||
|
"""Создание тикета"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""INSERT INTO tickets (user_id, username, message, status)
|
||||||
|
VALUES (%s, %s, %s, 'open')""",
|
||||||
|
(user_id, username, message)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
ticket_id = cursor.lastrowid
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return ticket_id
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def get_user_tickets(self, user_id: int) -> list:
|
||||||
|
"""Получение всех тикетов пользователя"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
cursor.execute(
|
||||||
|
"""SELECT id, message, status, admin_response, created_at
|
||||||
|
FROM tickets
|
||||||
|
WHERE user_id = %s
|
||||||
|
ORDER BY created_at DESC""",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
tickets = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return tickets
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def get_ticket(self, ticket_id: int) -> Optional[dict]:
|
||||||
|
"""Получение информации о тикете"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT * FROM tickets WHERE id = %s",
|
||||||
|
(ticket_id,)
|
||||||
|
)
|
||||||
|
ticket = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return ticket
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def update_ticket_response(self, ticket_id: int, admin_response: str, status: str = 'answered') -> bool:
|
||||||
|
"""Обновление ответа администратора в тикете"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""UPDATE tickets
|
||||||
|
SET admin_response = %s, status = %s
|
||||||
|
WHERE id = %s""",
|
||||||
|
(admin_response, status, ticket_id)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def close_ticket(self, ticket_id, closed_by):
|
||||||
|
"""Закрытие тикета"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""UPDATE tickets
|
||||||
|
SET status = 'closed', closed_by = %s, closed_at = %s
|
||||||
|
WHERE id = %s""",
|
||||||
|
(closed_by, datetime.now(), ticket_id)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def append_ticket_message(self, ticket_id: int, user_message: str) -> bool:
|
||||||
|
"""Добавление сообщения к тикету"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
# Получаем текущее сообщение
|
||||||
|
cursor.execute("SELECT message FROM tickets WHERE id = %s", (ticket_id,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if result:
|
||||||
|
current_message = result[0]
|
||||||
|
new_message = f"{current_message}\n\n➕ Дополнение:\n{user_message}"
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE tickets SET message = %s WHERE id = %s",
|
||||||
|
(new_message, ticket_id)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def get_all_users_paginated(self, page, per_page=10):
|
||||||
|
"""
|
||||||
|
Получение всех пользователей с пагинацией
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Номер страницы (начиная с 1)
|
||||||
|
per_page: Количество пользователей на странице
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {'users': [...], 'total': int, 'total_pages': int, 'current_page': int}
|
||||||
|
"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return {'users': [], 'total': 0, 'total_pages': 0, 'current_page': page}
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# Получаем общее количество пользователей
|
||||||
|
cursor.execute("SELECT COUNT(*) as count FROM subscriptions")
|
||||||
|
total = cursor.fetchone()['count']
|
||||||
|
|
||||||
|
# Вычисляем общее количество страниц
|
||||||
|
total_pages = (total + per_page - 1) // per_page if total > 0 else 0
|
||||||
|
|
||||||
|
# Ограничиваем page в допустимых пределах
|
||||||
|
page = max(1, min(page, total_pages if total_pages > 0 else 1))
|
||||||
|
|
||||||
|
# Получаем пользователей с пагинацией
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
cursor.execute(
|
||||||
|
"""SELECT id, user_id, username, is_active, subscription_start, subscription_end, created_at
|
||||||
|
FROM subscriptions
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT %s OFFSET %s""",
|
||||||
|
(per_page, offset)
|
||||||
|
)
|
||||||
|
users = cursor.fetchall()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'users': users,
|
||||||
|
'total': total,
|
||||||
|
'total_pages': total_pages,
|
||||||
|
'current_page': page
|
||||||
|
}
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return {'users': [], 'total': 0, 'total_pages': 0, 'current_page': page}
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def get_users_count(self):
|
||||||
|
"""
|
||||||
|
Получение количества пользователей по статусам
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {'total': int, 'active': int, 'inactive': int}
|
||||||
|
"""
|
||||||
|
connection = self.get_connection()
|
||||||
|
if not connection:
|
||||||
|
return {'total': 0, 'active': 0, 'inactive': 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) as count FROM subscriptions")
|
||||||
|
total = cursor.fetchone()['count']
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) as count FROM subscriptions WHERE is_active = TRUE")
|
||||||
|
active = cursor.fetchone()['count']
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total': total,
|
||||||
|
'active': active,
|
||||||
|
'inactive': total - active
|
||||||
|
}
|
||||||
|
except Error as e:
|
||||||
|
print(f"Ошибка БД: {e}")
|
||||||
|
return {'total': 0, 'active': 0, 'inactive': 0}
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр базы данных
|
||||||
|
db = Database()
|
||||||
439
core/remnawave.py
Normal file
439
core/remnawave.py
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
import aiohttp
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
|
||||||
|
class RemnawaveAPI:
|
||||||
|
"""Клиент для работы с Remnawave API"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.api_url = config.remwave_api_url
|
||||||
|
self.api_key = config.remwave_api_key
|
||||||
|
self.headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
async def create_subscription(self, user_id: int, days: int, tariff: str) -> dict:
|
||||||
|
"""
|
||||||
|
Создание пользователя (подписки) в Remnawave
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: Telegram user ID
|
||||||
|
days: Количество дней подписки
|
||||||
|
tariff: Название тарифа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результат создания подписки
|
||||||
|
"""
|
||||||
|
# Эндпоинт для создания пользователя
|
||||||
|
url = f"{self.api_url}/api/users"
|
||||||
|
|
||||||
|
# Генерируем случайный username и пароль
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
random_username = f"subs{user_id}"
|
||||||
|
random_password = ''.join(random.choices(string.ascii_letters + string.digits, k=12))
|
||||||
|
|
||||||
|
# Вычисляем дату истечения
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
expire_at = (datetime.now() + timedelta(days=days)).isoformat()
|
||||||
|
|
||||||
|
# Данные для создания пользователя (формат Remnawave)
|
||||||
|
data = {
|
||||||
|
"username": random_username,
|
||||||
|
"password": random_password,
|
||||||
|
"telegramId": user_id, # Число, не строка
|
||||||
|
"expireAt": expire_at # ISO формат даты
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.post(url, json=data, headers=self.headers) as response:
|
||||||
|
result = await response.json()
|
||||||
|
|
||||||
|
if response.status in [200, 201]:
|
||||||
|
print(f"✅ Подписка создана для user {user_id}")
|
||||||
|
print(f"📋 Ответ API: {result}")
|
||||||
|
|
||||||
|
# Получаем ссылку на подключение
|
||||||
|
# API возвращает в формате: {'response': {'subscriptionUrl': '...'}}
|
||||||
|
subscription_url = None
|
||||||
|
if isinstance(result, dict):
|
||||||
|
# Проверяем response.subscriptionUrl (основной вариант)
|
||||||
|
response_data = result.get("response", {})
|
||||||
|
if isinstance(response_data, dict):
|
||||||
|
subscription_url = response_data.get("subscriptionUrl")
|
||||||
|
|
||||||
|
# Если не нашли, проверяем другие варианты
|
||||||
|
if not subscription_url:
|
||||||
|
subscription_url = (
|
||||||
|
result.get("subscriptionUrl") or
|
||||||
|
result.get("subscription") or
|
||||||
|
result.get("link") or
|
||||||
|
result.get("url") or
|
||||||
|
result.get("data", {}).get("subscriptionUrl") or
|
||||||
|
result.get("data", {}).get("subscription") or
|
||||||
|
result.get("data", {}).get("link")
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"🔗 Subscription URL: {subscription_url}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": result,
|
||||||
|
"subscription_url": subscription_url,
|
||||||
|
"username": random_username
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка создания подписки: {response.status} - {result}")
|
||||||
|
return {"success": False, "error": result, "status": response.status}
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
print(f"❌ Ошибка подключения к Remnawave API: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
async def get_user_subscription(self, user_id: int) -> dict:
|
||||||
|
"""
|
||||||
|
Получение информации о подписке пользователя
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: Telegram user ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Информация о подписке
|
||||||
|
"""
|
||||||
|
url = f"{self.api_url}/api/users/telegram/{user_id}"
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.get(url, headers=self.headers) as response:
|
||||||
|
result = await response.json()
|
||||||
|
|
||||||
|
if response.status == 200:
|
||||||
|
return {"success": True, "data": result}
|
||||||
|
else:
|
||||||
|
return {"success": False, "error": result, "status": response.status}
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
async def extend_subscription(self, user_id: int, days: int) -> dict:
|
||||||
|
"""
|
||||||
|
Продление подписки
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: Telegram user ID
|
||||||
|
days: Количество дней для продления
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результат продления
|
||||||
|
"""
|
||||||
|
url = f"{self.api_url}/api/users/telegram/{user_id}/extend"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"durationDays": days
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.post(url, json=data, headers=self.headers) as response:
|
||||||
|
result = await response.json()
|
||||||
|
|
||||||
|
if response.status == 200:
|
||||||
|
print(f"✅ Подписка продлена для user {user_id} на {days} дней")
|
||||||
|
return {"success": True, "data": result}
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка продления подписки: {response.status} - {result}")
|
||||||
|
return {"success": False, "error": result, "status": response.status}
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
print(f"❌ Ошибка подключения к Remnawave API: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
async def get_all_users(self, page: int = 0, size: int = 500) -> dict:
|
||||||
|
"""
|
||||||
|
Получение всех пользователей из Remnawave с пагинацией
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Номер страницы (начиная с 0)
|
||||||
|
size: Размер страницы (макс 500)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Список пользователей
|
||||||
|
"""
|
||||||
|
url = f"{self.api_url}/api/users"
|
||||||
|
params = {"start": page, "size": min(size, 500)}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.get(url, headers=self.headers, params=params) as response:
|
||||||
|
result = await response.json()
|
||||||
|
print(f"📋 Remnawave API ответ: status={response.status}, keys={list(result.keys()) if isinstance(result, dict) else type(result)}")
|
||||||
|
|
||||||
|
if response.status == 200:
|
||||||
|
# Remnawave возвращает {'response': {'content': [...], 'total': N}}
|
||||||
|
users_list = []
|
||||||
|
|
||||||
|
if isinstance(result, dict):
|
||||||
|
# Пробуем разные варианты извлечения списка
|
||||||
|
if 'response' in result and isinstance(result['response'], dict):
|
||||||
|
response_data = result['response']
|
||||||
|
if 'content' in response_data and isinstance(response_data['content'], list):
|
||||||
|
users_list = response_data['content']
|
||||||
|
print(f"✅ Получено {len(users_list)} пользователей (response.content)")
|
||||||
|
elif 'users' in response_data and isinstance(response_data['users'], list):
|
||||||
|
users_list = response_data['users']
|
||||||
|
print(f"✅ Получено {len(users_list)} пользователей (response.users)")
|
||||||
|
elif 'content' in result and isinstance(result['content'], list):
|
||||||
|
users_list = result['content']
|
||||||
|
print(f"✅ Получено {len(users_list)} пользователей (content)")
|
||||||
|
elif 'data' in result and isinstance(result['data'], list):
|
||||||
|
users_list = result['data']
|
||||||
|
print(f"✅ Получено {len(users_list)} пользователей (data)")
|
||||||
|
elif 'users' in result and isinstance(result['users'], list):
|
||||||
|
users_list = result['users']
|
||||||
|
print(f"✅ Получено {len(users_list)} пользователей (users)")
|
||||||
|
|
||||||
|
if users_list:
|
||||||
|
return {"success": True, "data": {"content": users_list, "total": len(users_list)}}
|
||||||
|
|
||||||
|
print(f"⚠️ Не найдено пользователей в ответе")
|
||||||
|
return {"success": True, "data": {"content": [], "total": 0}}
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка получения пользователей: {response.status} - {result}")
|
||||||
|
return {"success": False, "error": result, "status": response.status}
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
print(f"❌ Ошибка подключения к Remnawave API: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
async def sync_user(self, user_id: int, username: str, days: int = 30) -> dict:
|
||||||
|
"""
|
||||||
|
Синхронизация пользователя (создание если нет в Remnawave)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: Telegram user ID
|
||||||
|
username: Имя пользователя
|
||||||
|
days: Количество дней
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результат синхронизации
|
||||||
|
"""
|
||||||
|
# Сначала пробуем найти пользователя по Telegram ID через API
|
||||||
|
print(f"🔍 Поиск пользователя по Telegram ID: {user_id}")
|
||||||
|
check_result = await self.get_user_subscription(user_id)
|
||||||
|
|
||||||
|
if check_result.get("success") and check_result.get("data"):
|
||||||
|
# Пользователь уже есть в Remnawave с этим telegramId
|
||||||
|
print(f"✅ Пользователь {user_id} уже найден в Remnawave по Telegram ID")
|
||||||
|
return {"success": True, "exists": True, "data": check_result.get("data")}
|
||||||
|
|
||||||
|
# Если не нашли по telegramId, пробуем найти по username (subs{user_id})
|
||||||
|
remnawave_username = f"subs{user_id}"
|
||||||
|
print(f"🔍 Не найдено по Telegram ID, поиск по username: {remnawave_username}")
|
||||||
|
|
||||||
|
# Получаем всех пользователей и ищем по username
|
||||||
|
all_users_result = await self.get_all_users(page=0, size=500)
|
||||||
|
|
||||||
|
if all_users_result.get("success"):
|
||||||
|
users_data = all_users_result.get("data", {})
|
||||||
|
users_list = users_data.get("content", [])
|
||||||
|
|
||||||
|
print(f"📋 Получено {len(users_list)} пользователей из Remnawave")
|
||||||
|
|
||||||
|
# Логируем первые несколько для отладки
|
||||||
|
if users_list and len(users_list) > 0:
|
||||||
|
first_users = [(u.get("username"), u.get("telegramId")) for u in users_list[:5] if isinstance(u, dict)]
|
||||||
|
print(f"📋 Первые пользователи: {first_users}")
|
||||||
|
|
||||||
|
# Ищем по username
|
||||||
|
for user in users_list:
|
||||||
|
if isinstance(user, dict) and user.get("username") == remnawave_username:
|
||||||
|
user_uuid = user.get("uuid")
|
||||||
|
print(f"✅ Найден пользователь {remnawave_username} с UUID {user_uuid}")
|
||||||
|
if user_uuid:
|
||||||
|
update_result = await self._update_user_telegram_id(user_uuid, user_id)
|
||||||
|
if update_result.get("success"):
|
||||||
|
print(f"✅ Обновлён telegramId для user {user_id}")
|
||||||
|
return {"success": True, "exists": True, "updated": True, "data": update_result.get("data")}
|
||||||
|
else:
|
||||||
|
return update_result
|
||||||
|
|
||||||
|
# Создаём нового пользователя
|
||||||
|
print(f"➕ Создание нового пользователя {remnawave_username}")
|
||||||
|
create_result = await self.create_subscription(user_id, days, remnawave_username)
|
||||||
|
|
||||||
|
# Если ошибка "username already exists" — пользователь уже есть, это нормально
|
||||||
|
if not create_result.get("success"):
|
||||||
|
error_data = create_result.get("error", {})
|
||||||
|
if isinstance(error_data, dict) and error_data.get("message") == "User username already exists":
|
||||||
|
print(f"ℹ️ Пользователь {remnawave_username} уже существует в Remnawave (не удалось обновить telegramId)")
|
||||||
|
# Возвращаем успех с флагом exists
|
||||||
|
return {"success": True, "exists": True, "updated": False, "data": None}
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка создания: {error_data}")
|
||||||
|
|
||||||
|
return create_result
|
||||||
|
|
||||||
|
async def _find_and_update_by_username(self, username: str, telegram_id: int) -> dict:
|
||||||
|
"""
|
||||||
|
Поиск пользователя по username и обновление telegramId
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: Имя пользователя в Remnawave
|
||||||
|
telegram_id: Telegram user ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результат
|
||||||
|
"""
|
||||||
|
all_users_result = await self.get_all_users(page=0, size=500)
|
||||||
|
|
||||||
|
if not all_users_result.get("success"):
|
||||||
|
return all_users_result
|
||||||
|
|
||||||
|
users_data = all_users_result.get("data", {})
|
||||||
|
users_list = users_data.get("content", []) if isinstance(users_data, dict) else (users_data if isinstance(users_data, list) else [])
|
||||||
|
|
||||||
|
for user in users_list:
|
||||||
|
if user.get("username") == username:
|
||||||
|
user_uuid = user.get("uuid")
|
||||||
|
if user_uuid:
|
||||||
|
return await self._update_user_telegram_id(user_uuid, telegram_id)
|
||||||
|
|
||||||
|
return {"success": False, "error": "User not found"}
|
||||||
|
|
||||||
|
async def _update_user_telegram_id(self, user_uuid: str, telegram_id: int) -> dict:
|
||||||
|
"""
|
||||||
|
Обновление telegramId у существующего пользователя
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_uuid: UUID пользователя в Remnawave
|
||||||
|
telegram_id: Telegram user ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результат обновления
|
||||||
|
"""
|
||||||
|
# Пробуем несколько вариантов обновления
|
||||||
|
urls_to_try = [
|
||||||
|
(f"{self.api_url}/api/users/{user_uuid}/telegram", "PATCH"),
|
||||||
|
(f"{self.api_url}/api/users/{user_uuid}", "PUT"),
|
||||||
|
(f"{self.api_url}/api/users/{user_uuid}/telegram-id", "POST"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for url, method in urls_to_try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
data = {"telegramId": telegram_id}
|
||||||
|
|
||||||
|
if method == "PATCH":
|
||||||
|
async with session.patch(url, json=data, headers=self.headers) as response:
|
||||||
|
result = await response.json()
|
||||||
|
if response.status in [200, 204]:
|
||||||
|
print(f"✅ Обновлён telegramId для user {telegram_id} (PATCH)")
|
||||||
|
return {"success": True, "data": result}
|
||||||
|
elif method == "PUT":
|
||||||
|
async with session.put(url, json=data, headers=self.headers) as response:
|
||||||
|
result = await response.json()
|
||||||
|
if response.status in [200, 204]:
|
||||||
|
print(f"✅ Обновлён telegramId для user {telegram_id} (PUT)")
|
||||||
|
return {"success": True, "data": result}
|
||||||
|
elif method == "POST":
|
||||||
|
async with session.post(url, json=data, headers=self.headers) as response:
|
||||||
|
result = await response.json()
|
||||||
|
if response.status in [200, 201]:
|
||||||
|
print(f"✅ Обновлён telegramId для user {telegram_id} (POST)")
|
||||||
|
return {"success": True, "data": result}
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"❌ Не удалось обновить telegramId - API не поддерживает обновление")
|
||||||
|
return {"success": False, "error": "API does not support user update"}
|
||||||
|
|
||||||
|
async def sync_all_users(self) -> dict:
|
||||||
|
"""
|
||||||
|
Синхронизация всех пользователей: Remnawave → Локальная БД
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результаты синхронизации
|
||||||
|
"""
|
||||||
|
from core import db as local_db
|
||||||
|
|
||||||
|
# Получаем всех пользователей из Remnawave
|
||||||
|
print("🔄 Получение пользователей из Remnawave...")
|
||||||
|
remnawave_result = await self.get_all_users(page=0, size=500)
|
||||||
|
|
||||||
|
if not remnawave_result.get("success"):
|
||||||
|
return {"success": False, "error": "Не удалось получить пользователей из Remnawave"}
|
||||||
|
|
||||||
|
users_data = remnawave_result.get("data", {})
|
||||||
|
users_list = users_data.get("content", [])
|
||||||
|
|
||||||
|
print(f"📋 Получено {len(users_list)} пользователей из Remnawave")
|
||||||
|
|
||||||
|
total = len(users_list)
|
||||||
|
imported = 0
|
||||||
|
updated = 0
|
||||||
|
errors = 0
|
||||||
|
error_details = []
|
||||||
|
|
||||||
|
connection = local_db.get_connection()
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
for user in users_list:
|
||||||
|
if not isinstance(user, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
user_id = user.get("telegramId")
|
||||||
|
username = user.get("username", "")
|
||||||
|
is_active = user.get("status") == "ACTIVE"
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
# Нет Telegram ID - пропускаем
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Проверяем, есть ли пользователь в БД
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM subscriptions WHERE user_id = %s",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
existing = cursor.fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# Обновляем существующего
|
||||||
|
cursor.execute(
|
||||||
|
"""UPDATE subscriptions
|
||||||
|
SET username = %s, is_active = %s
|
||||||
|
WHERE user_id = %s""",
|
||||||
|
(username, is_active, user_id)
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
print(f"🔄 Обновлён пользователь {user_id} ({username})")
|
||||||
|
else:
|
||||||
|
# Создаём нового
|
||||||
|
cursor.execute(
|
||||||
|
"""INSERT INTO subscriptions (user_id, username, is_active)
|
||||||
|
VALUES (%s, %s, %s)""",
|
||||||
|
(user_id, username, is_active)
|
||||||
|
)
|
||||||
|
imported += 1
|
||||||
|
print(f"➕ Импортирован пользователь {user_id} ({username})")
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors += 1
|
||||||
|
error_details.append(f"User {user_id}: {str(e)}")
|
||||||
|
print(f"❌ Ошибка импорта пользователя {user_id}: {e}")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"total": total,
|
||||||
|
"imported": imported,
|
||||||
|
"updated": updated,
|
||||||
|
"errors": errors,
|
||||||
|
"error_details": error_details[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр API
|
||||||
|
remnawave = RemnawaveAPI()
|
||||||
38
core/update_tickets.sql
Normal file
38
core/update_tickets.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- Обновление таблицы tickets для добавления полей закрытия
|
||||||
|
USE botyobshik;
|
||||||
|
|
||||||
|
-- Проверяем и добавляем колонку closed_by
|
||||||
|
SET @dbname = DATABASE();
|
||||||
|
SET @tablename = 'tickets';
|
||||||
|
SET @columnname = 'closed_by';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE
|
||||||
|
(table_name = @tablename)
|
||||||
|
AND (table_schema = @dbname)
|
||||||
|
AND (column_name = @columnname)
|
||||||
|
) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
CONCAT('ALTER TABLE ', @tablename, ' ADD COLUMN ', @columnname, ' VARCHAR(255) AFTER admin_response')
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
|
||||||
|
-- Проверяем и добавляем колонку closed_at
|
||||||
|
SET @columnname = 'closed_at';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE
|
||||||
|
(table_name = @tablename)
|
||||||
|
AND (table_schema = @dbname)
|
||||||
|
AND (column_name = @columnname)
|
||||||
|
) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
CONCAT('ALTER TABLE ', @tablename, ' ADD COLUMN ', @columnname, ' DATETIME AFTER closed_by')
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
1793
handlers/__init__.py
Normal file
1793
handlers/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
150
keyboards/__init__.py
Normal file
150
keyboards/__init__.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
from locales import localization
|
||||||
|
|
||||||
|
|
||||||
|
class Keyboards:
|
||||||
|
"""Класс для создания клавиатур"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_main_keyboard(locale: str = "ru") -> InlineKeyboardMarkup:
|
||||||
|
"""Основная клавиатура бота"""
|
||||||
|
keyboard = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("buttons.buy_subscription", locale),
|
||||||
|
callback_data="buy_subscription"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("buttons.support", locale),
|
||||||
|
callback_data="support"
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("buttons.rules", locale),
|
||||||
|
callback_data="rules"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("buttons.language", locale),
|
||||||
|
callback_data="language"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_buy_subscription_keyboard(locale: str = "ru") -> InlineKeyboardMarkup:
|
||||||
|
"""Клавиатура для покупки подписки"""
|
||||||
|
keyboard = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("tariffs.14_days", locale),
|
||||||
|
callback_data="buy_14_days"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("tariffs.30_days", locale),
|
||||||
|
callback_data="buy_30_days"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("tariffs.60_days", locale),
|
||||||
|
callback_data="buy_60_days"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("tariffs.90_days", locale),
|
||||||
|
callback_data="buy_90_days"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("tariffs.180_days", locale),
|
||||||
|
callback_data="buy_180_days"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("tariffs.360_days", locale),
|
||||||
|
callback_data="buy_360_days"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("buttons.back", locale),
|
||||||
|
callback_data="back_to_main"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_language_keyboard(locale: str = "ru") -> InlineKeyboardMarkup:
|
||||||
|
"""Клавиатура выбора языка"""
|
||||||
|
keyboard = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("languages.ru", locale),
|
||||||
|
callback_data="lang_ru"
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("languages.en", locale),
|
||||||
|
callback_data="lang_en"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("languages.kz", locale),
|
||||||
|
callback_data="lang_kz"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("buttons.back", locale),
|
||||||
|
callback_data="back_to_main"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_support_keyboard(locale: str = "ru") -> InlineKeyboardMarkup:
|
||||||
|
"""Клавиатура техподдержки"""
|
||||||
|
keyboard = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="📝 Создать тикет",
|
||||||
|
callback_data="support_create_ticket"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="📋 Мои тикеты",
|
||||||
|
callback_data="support_my_tickets"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=localization.get("buttons.back", locale),
|
||||||
|
callback_data="back_to_main"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return keyboard
|
||||||
24
keyboards/payment.py
Normal file
24
keyboards/payment.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentKeyboards:
|
||||||
|
"""Клавиатуры для оплаты"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_payment_approval_keyboard(user_id: int, tariff: str, days: int) -> InlineKeyboardMarkup:
|
||||||
|
"""Клавиатура для подтверждения оплаты (в канал)"""
|
||||||
|
keyboard = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="✅ Аппрув",
|
||||||
|
callback_data=f"payment_approve_{user_id}_{tariff}_{days}"
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="❌ Отклонить",
|
||||||
|
callback_data=f"payment_decline_{user_id}_{tariff}"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return keyboard
|
||||||
91
locales/__init__.py
Normal file
91
locales/__init__.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Localization:
|
||||||
|
"""Класс для управления локализацией"""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
_locales = {}
|
||||||
|
_default_locale = "ru"
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if not self._locales:
|
||||||
|
self.load_locales()
|
||||||
|
|
||||||
|
def load_locales(self):
|
||||||
|
"""Загрузка всех файлов локализации"""
|
||||||
|
locales_dir = Path(__file__).parent
|
||||||
|
for locale_file in locales_dir.glob("*.json"):
|
||||||
|
locale_code = locale_file.stem
|
||||||
|
with open(locale_file, "r", encoding="utf-8") as f:
|
||||||
|
self._locales[locale_code] = json.load(f)
|
||||||
|
|
||||||
|
def get(self, key: str, locale: str = None, **kwargs) -> str:
|
||||||
|
"""
|
||||||
|
Получение локализованной строки
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Ключ строки (например, "welcome", "buttons.buy_subscription")
|
||||||
|
locale: Код языка (ru, en, kz)
|
||||||
|
**kwargs: Параметры для форматирования строки
|
||||||
|
"""
|
||||||
|
if locale is None:
|
||||||
|
locale = self._default_locale
|
||||||
|
|
||||||
|
if locale not in self._locales:
|
||||||
|
locale = self._default_locale
|
||||||
|
|
||||||
|
keys = key.split(".")
|
||||||
|
value = self._locales.get(locale, {})
|
||||||
|
|
||||||
|
for k in keys:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value.get(k)
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
break
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
# Пытаемся найти в default locale
|
||||||
|
value = self._locales.get(self._default_locale, {})
|
||||||
|
for k in keys:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value.get(k)
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
break
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return key
|
||||||
|
|
||||||
|
if kwargs:
|
||||||
|
try:
|
||||||
|
value = value.format(**kwargs)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_locale_name(self, locale: str) -> str:
|
||||||
|
"""Получение названия языка на родном языке"""
|
||||||
|
return self.get(f"languages.{locale}", locale)
|
||||||
|
|
||||||
|
def set_default_locale(self, locale: str):
|
||||||
|
"""Установка языка по умолчанию"""
|
||||||
|
if locale in self._locales:
|
||||||
|
self._default_locale = locale
|
||||||
|
|
||||||
|
def get_available_locales(self) -> list:
|
||||||
|
"""Получение списка доступных языков"""
|
||||||
|
return list(self._locales.keys())
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр локализации
|
||||||
|
localization = Localization()
|
||||||
45
locales/en.json
Normal file
45
locales/en.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"welcome": "Hello, {username}!",
|
||||||
|
"subscription_status": "Your subscription: {status}",
|
||||||
|
"select_action": "Select an action:",
|
||||||
|
"subscription_active": "Active",
|
||||||
|
"subscription_inactive": "Inactive",
|
||||||
|
"buttons": {
|
||||||
|
"buy_subscription": "💳 Buy Subscription",
|
||||||
|
"support": "📞 Support",
|
||||||
|
"rules": "📜 Service Rules",
|
||||||
|
"language": "🌐 Language",
|
||||||
|
"back": "🔙 Back"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"support": "Support: {username}",
|
||||||
|
"support_title": "📞 Support",
|
||||||
|
"support_description": "This is the ticket center: create requests, view responses and history.\n\n📝 Create ticket - describe your problem or question.\n📋 My tickets - status and correspondence\n\nTry to use tickets - this way we can help faster and nothing gets lost.",
|
||||||
|
"rules_title": "📜 VPN Rules",
|
||||||
|
"rules_text": "1. By using the bot, you agree to the rules and use the service lawfully.\n2. VPN is designed to protect data and secure internet access.\n3. Prohibited: hacking, spam, fraud, viruses and illegal content 🚫\n4. User is solely responsible for their actions.\n5. Technical data may be collected (not shared with third parties, except by law).\n6. Access may be restricted without refund for violations.\n\n⚠️ VPN does not guarantee complete anonymity.",
|
||||||
|
"select_tariff": "Select tariff:",
|
||||||
|
"tariff_selected": "You selected: {tariff}",
|
||||||
|
"select_language": "Выберите язык / Select language:",
|
||||||
|
"language_changed": "Language changed to {language}",
|
||||||
|
"payment_instruction_title": "Hello! 👋",
|
||||||
|
"payment_instruction_text": "To purchase a subscription, follow these steps:\n\n1️⃣ Transfer via SBP to: {phone}\n2️⃣ After payment, send the receipt or screenshot to this chat\n\nAfter payment confirmation, we will activate your subscription.\n\nThank you! 😊",
|
||||||
|
"payment_receipt_sent": "✅ Receipt received! Wait for payment confirmation."
|
||||||
|
},
|
||||||
|
"subscription": {
|
||||||
|
"standard_title": "Standard Subscription",
|
||||||
|
"standard_description": "Basic tariff plan"
|
||||||
|
},
|
||||||
|
"tariffs": {
|
||||||
|
"14_days": "14 days - 150₽",
|
||||||
|
"30_days": "30 days - 300₽",
|
||||||
|
"60_days": "60 days - 600₽",
|
||||||
|
"90_days": "90 days - 900₽",
|
||||||
|
"180_days": "180 days - 1800₽",
|
||||||
|
"360_days": "360 days - 3600₽"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"ru": "🇷🇺 Русский",
|
||||||
|
"en": "🇬🇧 English",
|
||||||
|
"kz": "🇰🇿 Қазақша"
|
||||||
|
}
|
||||||
|
}
|
||||||
45
locales/kz.json
Normal file
45
locales/kz.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"welcome": "Сәлем, {username}!",
|
||||||
|
"subscription_status": "Сіздің жазылымыңыз: {status}",
|
||||||
|
"select_action": "Әрекетті таңдаңыз:",
|
||||||
|
"subscription_active": "Белсенді",
|
||||||
|
"subscription_inactive": "Белсенді емес",
|
||||||
|
"buttons": {
|
||||||
|
"buy_subscription": "💳 Жазылуды сатып алу",
|
||||||
|
"support": "📞 Қолдау қызметі",
|
||||||
|
"rules": "📜 Қызмет ережелері",
|
||||||
|
"language": "🌐 Тіл",
|
||||||
|
"back": "🔙 Артқа"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"support": "Қолдау қызметі: {username}",
|
||||||
|
"support_title": "📞 Қолдау",
|
||||||
|
"support_description": "Бұл тикет орталығы: өтініштер жасау, жауаптарды және тарихты қарау.\n\n📝 Тикет жасау - мәселеңізді немесе сұрағыңызды сипаттаңыз.\n📋 Менің тикеттерім - мәртебе және хат алмасу\n\nТикеттерді пайдаланыңыз - біз жылдамырақ көмектесеміз және ештеңе жоғалмайды.",
|
||||||
|
"rules_title": "📜 VPN ережелері",
|
||||||
|
"rules_text": "1. Ботты пайдалана отырып, сіз ережелермен келісесіз және сервисді заңды түрде пайдаланасыз.\n2. VPN деректерді қорғау және қауіпсіз интернетке қол жеткізу үшін арналған.\n3. Тыйым салынады: бұзу, спам, алаяқтық, вирустар және заңсыз контент 🚫\n4. Пайдаланушы өз әрекеттері үшін дербес жауап береді.\n5. Техникалық деректер жиналуы мүмкін (заңды жағдайларды қоспағанда, үшінші тұлғаларға берілмейді).\n6. Ережелерді бұзған жағдайда қол жеткізу қайтарусыз шектелуі мүмкін.\n\n⚠️ VPN толық анонимділікті кепілдік бермейді.",
|
||||||
|
"select_tariff": "Тарифті таңдаңыз:",
|
||||||
|
"tariff_selected": "Сіз таңдадыңыз: {tariff}",
|
||||||
|
"select_language": "Выберите язык / Select language:",
|
||||||
|
"language_changed": "Тіл өзгертілді: {language}",
|
||||||
|
"payment_instruction_title": "Сәлем! 👋",
|
||||||
|
"payment_instruction_text": "Жазылымды сатып алу үшін мына әрекеттерді орындаңыз:\n\n1️⃣ СБП арқылы {phone} нөміріне аударыңыз\n2️⃣ Төлемнен кейін чекті немесе скриншотты осы чатқа жіберіңіз\n\nТөлем расталғаннан кейін жазылымыңызды белсендіреміз.\n\nРақмет! 😊",
|
||||||
|
"payment_receipt_sent": "✅ Чек қабылданды! Төлемнің расталуын күтіңіз."
|
||||||
|
},
|
||||||
|
"subscription": {
|
||||||
|
"standard_title": "Стандартты жазылым",
|
||||||
|
"standard_description": "Негізгі тарифтік жоспар"
|
||||||
|
},
|
||||||
|
"tariffs": {
|
||||||
|
"14_days": "14 күн - 150₽",
|
||||||
|
"30_days": "30 күн - 300₽",
|
||||||
|
"60_days": "60 күн - 600₽",
|
||||||
|
"90_days": "90 күн - 900₽",
|
||||||
|
"180_days": "180 күн - 1800₽",
|
||||||
|
"360_days": "360 күн - 3600₽"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"ru": "🇷🇺 Русский",
|
||||||
|
"en": "🇬🇧 English",
|
||||||
|
"kz": "🇰🇿 Қазақша"
|
||||||
|
}
|
||||||
|
}
|
||||||
45
locales/ru.json
Normal file
45
locales/ru.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"welcome": "Привет, {username}!",
|
||||||
|
"subscription_status": "Твоя подписка: {status}",
|
||||||
|
"select_action": "Выберите действие:",
|
||||||
|
"subscription_active": "Активна",
|
||||||
|
"subscription_inactive": "Не активна",
|
||||||
|
"buttons": {
|
||||||
|
"buy_subscription": "💳 Купить подписку",
|
||||||
|
"support": "📞 Техподдержка",
|
||||||
|
"rules": "📜 Правила сервиса",
|
||||||
|
"language": "🌐 Язык",
|
||||||
|
"back": "🔙 Назад"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"support": "Техподдержка: {username}",
|
||||||
|
"support_title": "📞 Поддержка",
|
||||||
|
"support_description": "Это центр тикетов: создавайте обращения, просматривайте ответы и историю.\n\n📝 Создать тикет - опишите проблему или вопрос.\n📋 Мои тикеты - статус и переписка\n\nСтарайтесь использовать тикеты - так мы быстрее поможем и ничего не потеряется.",
|
||||||
|
"rules_title": "📜 Правила VPN",
|
||||||
|
"rules_text": "1. Используя бота, вы соглашаетесь с правилами и используете сервис законно.\n2. VPN предназначен для защиты данных и безопасного доступа к интернету.\n3. Запрещены: взломы, спам, мошенничество, вирусы и незаконный контент 🚫\n4. Пользователь сам несёт ответственность за свои действия.\n5. Возможен сбор технических данных (без передачи третьим лицам, кроме закона).\n6. За нарушения доступ может быть ограничен без возврата средств.\n\n⚠️ VPN не гарантирует полной анонимности.",
|
||||||
|
"select_tariff": "Выберите тариф:",
|
||||||
|
"tariff_selected": "Вы выбрали тариф: {tariff}",
|
||||||
|
"select_language": "Выберите язык / Select language:",
|
||||||
|
"language_changed": "Язык изменён на {language}",
|
||||||
|
"payment_instruction_title": "Здравствуйте! 👋",
|
||||||
|
"payment_instruction_text": "Чтобы приобрести подписку, выполните следующие шаги:\n\n1️⃣ Переведите сумму через СБП по номеру: {phone}\n2️⃣ После оплаты отправьте чек или скриншот перевода в этот чат\n\nПосле подтверждения оплаты мы активируем вашу подписку.\n\nСпасибо! 😊",
|
||||||
|
"payment_receipt_sent": "✅ Чек получен! Ожидайте подтверждения оплаты."
|
||||||
|
},
|
||||||
|
"subscription": {
|
||||||
|
"standard_title": "Стандартная подписка",
|
||||||
|
"standard_description": "Базовый тарифный план"
|
||||||
|
},
|
||||||
|
"tariffs": {
|
||||||
|
"14_days": "14 дней - 150₽",
|
||||||
|
"30_days": "30 дней - 300₽",
|
||||||
|
"60_days": "60 дней - 600₽",
|
||||||
|
"90_days": "90 дней - 900₽",
|
||||||
|
"180_days": "180 дней - 1800₽",
|
||||||
|
"360_days": "360 дней - 3600₽"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"ru": "🇷🇺 Русский",
|
||||||
|
"en": "🇬🇧 English",
|
||||||
|
"kz": "🇰🇿 Қазақша"
|
||||||
|
}
|
||||||
|
}
|
||||||
41
main.py
Normal file
41
main.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
|
||||||
|
from config import config
|
||||||
|
from core import db
|
||||||
|
from handlers import main_router, admin_router
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Инициализация бота и диспетчера
|
||||||
|
bot = Bot(token=config.bot_token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||||
|
dp = Dispatcher()
|
||||||
|
|
||||||
|
# Регистрация роутеров
|
||||||
|
dp.include_router(main_router)
|
||||||
|
dp.include_router(admin_router)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_startup():
|
||||||
|
"""Инициализация при запуске"""
|
||||||
|
# Создаём БД и таблицы если нет
|
||||||
|
await db.initialize()
|
||||||
|
print("Бот запущен...")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Запуск бота"""
|
||||||
|
await on_startup()
|
||||||
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
aiogram==3.3.0
|
||||||
|
mysql-connector-python==8.3.0
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
aiohttp==3.9.3
|
||||||
Reference in New Issue
Block a user