587 строки
25 KiB
Python
587 строки
25 KiB
Python
import os
|
|
import subprocess
|
|
import configparser
|
|
import json
|
|
import pytz
|
|
import socket
|
|
import logging
|
|
import tempfile
|
|
import shutil
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, List, Tuple, Dict, Any
|
|
|
|
EXPIRATIONS_FILE = 'files/expirations.json'
|
|
PAYMENTS_FILE = 'files/payments.json'
|
|
ADMINS_FILE = 'files/admins.json'
|
|
MODERATORS_FILE = 'files/moderators.json'
|
|
PROMOCODES_FILE = 'files/promocodes.json'
|
|
USER_TELEGRAM_IDS_FILE = 'files/user_telegram_ids.json'
|
|
CONFIG_FILE = 'files/setting.ini'
|
|
BACKUP_DIR = 'files/backups'
|
|
LOG_FILE = 'files/db.log'
|
|
UTC = pytz.UTC
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.FileHandler(LOG_FILE),
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def ensure_files_exist():
|
|
"""Создаёт все необходимые файлы, если они отсутствуют."""
|
|
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
|
os.makedirs(os.path.dirname(EXPIRATIONS_FILE), exist_ok=True)
|
|
os.makedirs(os.path.dirname(BACKUP_DIR), exist_ok=True)
|
|
|
|
for file_path in [EXPIRATIONS_FILE, PAYMENTS_FILE, ADMINS_FILE, MODERATORS_FILE, PROMOCODES_FILE, USER_TELEGRAM_IDS_FILE]:
|
|
if not os.path.exists(file_path):
|
|
with open(file_path, 'w') as f:
|
|
json.dump({}, f) if file_path != PAYMENTS_FILE else json.dump([], f)
|
|
|
|
def get_amnezia_container() -> str:
|
|
"""Получает имя запущенного Docker-контейнера Amnezia AWG."""
|
|
cmd = "docker ps --filter 'name=amnezia-awg' --format '{{.Names}}'"
|
|
try:
|
|
output = subprocess.check_output(cmd, shell=True).decode().strip()
|
|
if output:
|
|
return output
|
|
else:
|
|
logger.error("Docker-контейнер 'amnezia-awg' не найден или не запущен.")
|
|
raise RuntimeError("Контейнер не найден")
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Ошибка при поиске контейнера: {e}")
|
|
raise
|
|
|
|
def create_config(path: str = CONFIG_FILE) -> None:
|
|
"""Создаёт конфигурационный файл с настройками бота."""
|
|
ensure_files_exist()
|
|
config = configparser.ConfigParser()
|
|
config.add_section("setting")
|
|
|
|
print("Настройка конфигурации бота...")
|
|
bot_token = input('Введите токен Telegram бота: ').strip()
|
|
admin_ids_input = input('Введите Telegram ID администраторов через запятую (например, 12345,67890): ').strip()
|
|
admin_ids = [admin_id.strip() for admin_id in admin_ids_input.split(',') if admin_id.strip()]
|
|
moderator_ids_input = input('Введите Telegram ID модераторов через запятую (опционально): ').strip()
|
|
moderator_ids = [mod_id.strip() for mod_id in moderator_ids_input.split(',') if mod_id.strip()]
|
|
yoomoney_token = input('Введите токен YooMoney: ').strip()
|
|
yoomoney_wallet = input('Введите номер кошелька YooMoney (15-18 цифр): ').strip()
|
|
pricing = {
|
|
'1_month': 1000.0,
|
|
'3_months': 2500.0,
|
|
'6_months': 4500.0,
|
|
'12_months': 8000.0
|
|
}
|
|
|
|
try:
|
|
docker_container = get_amnezia_container()
|
|
except RuntimeError:
|
|
docker_container = input('Введите имя Docker-контейнера Amnezia AWG: ').strip()
|
|
|
|
config.set("setting", "bot_token", bot_token)
|
|
config.set("setting", "admin_ids", ",".join(admin_ids))
|
|
config.set("setting", "moderator_ids", ",".join(moderator_ids))
|
|
config.set("setting", "wg_config_file", "/root/amnezia-bot/amnezia-awg.conf")
|
|
config.set("setting", "docker_container", docker_container)
|
|
config.set("setting", "endpoint", socket.gethostbyname(socket.gethostname()))
|
|
config.set("setting", "yoomoney_token", yoomoney_token)
|
|
config.set("setting", "yoomoney_wallet", yoomoney_wallet)
|
|
config.set("setting", "pricing", json.dumps(pricing))
|
|
|
|
with open(path, 'w') as configfile:
|
|
config.write(configfile)
|
|
|
|
with open(ADMINS_FILE, 'w') as f:
|
|
json.dump(admin_ids, f)
|
|
with open(MODERATORS_FILE, 'w') as f:
|
|
json.dump(moderator_ids, f)
|
|
|
|
logger.info("Конфигурация успешно создана.")
|
|
|
|
def get_config(path: str = CONFIG_FILE) -> Dict[str, Any]:
|
|
"""Читает конфигурацию из файла."""
|
|
if not os.path.exists(path):
|
|
create_config(path)
|
|
|
|
config = configparser.ConfigParser()
|
|
config.read(path)
|
|
|
|
settings = {
|
|
'bot_token': config.get("setting", "bot_token", fallback=""),
|
|
'admin_ids': [id for id in config.get("setting", "admin_ids", fallback="").split(",") if id],
|
|
'moderator_ids': [id for id in config.get("setting", "moderator_ids", fallback="").split(",") if id],
|
|
'wg_config_file': config.get("setting", "wg_config_file", fallback=""),
|
|
'docker_container': config.get("setting", "docker_container", fallback=""),
|
|
'endpoint': config.get("setting", "endpoint", fallback=""),
|
|
'yoomoney_token': config.get("setting", "yoomoney_token", fallback=""),
|
|
'yoomoney_wallet': config.get("setting", "yoomoney_wallet", fallback=""),
|
|
'pricing': json.loads(config.get(
|
|
"setting", "pricing",
|
|
fallback='{"1_month": 1000.0, "3_months": 2500.0, "6_months": 4500.0, "12_months": 8000.0}'
|
|
))
|
|
}
|
|
|
|
return settings
|
|
|
|
def set_yoomoney_config(token: Optional[str] = None, wallet: Optional[str] = None, path: str = CONFIG_FILE) -> None:
|
|
"""Обновляет настройки YooMoney в конфигурации."""
|
|
config = configparser.ConfigParser()
|
|
config.read(path)
|
|
|
|
if token:
|
|
config.set("setting", "yoomoney_token", token)
|
|
if wallet:
|
|
config.set("setting", "yoomoney_wallet", wallet)
|
|
|
|
with open(path, 'w') as configfile:
|
|
config.write(configfile)
|
|
logger.info(f"YooMoney settings updated: token={'***' if token else 'unchanged'}, wallet={wallet or 'unchanged'}")
|
|
|
|
def set_pricing(period: str, price: float, path: str = CONFIG_FILE) -> None:
|
|
"""Обновляет цену для указанного периода подписки."""
|
|
config = configparser.ConfigParser()
|
|
config.read(path)
|
|
|
|
pricing = json.loads(config.get(
|
|
"setting", "pricing",
|
|
fallback='{"1_month": 1000.0, "3_months": 2500.0, "6_months": 4500.0, "12_months": 8000.0}'
|
|
))
|
|
pricing[period] = float(price)
|
|
config.set("setting", "pricing", json.dumps(pricing))
|
|
|
|
with open(path, 'w') as configfile:
|
|
config.write(configfile)
|
|
logger.info(f"Pricing updated: {period} set to ₽{price}")
|
|
|
|
def root_add(user_name: str, ipv6: bool = False) -> bool:
|
|
"""Добавляет нового пользователя через скрипт newclient.sh."""
|
|
cmd = ["/root/amnezia-bot/newclient.sh", user_name]
|
|
if not ipv6:
|
|
cmd.append("no-ipv6")
|
|
try:
|
|
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
logger.info(f"Пользователь {user_name} успешно добавлен: {result.stdout}")
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Ошибка при добавлении пользователя {user_name}: {e.stderr}")
|
|
return False
|
|
|
|
def deactive_user_db(user_name: str) -> bool:
|
|
"""Деактивирует пользователя через скрипт removeclient.sh."""
|
|
cmd = ["/root/amnezia-bot/removeclient.sh", user_name]
|
|
try:
|
|
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
logger.info(f"Пользователь {user_name} успешно деактивирован: {result.stdout}")
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Ошибка при деактивации пользователя {user_name}: {e.stderr}")
|
|
return False
|
|
|
|
def get_client_list() -> List[Tuple[str, str, str]]:
|
|
"""Получает список всех клиентов из WireGuard."""
|
|
cmd = "docker exec $(docker ps -q -f name=amnezia-awg) awg show all dump"
|
|
try:
|
|
output = subprocess.check_output(cmd, shell=True, text=True).strip()
|
|
clients = []
|
|
for line in output.splitlines()[1:]:
|
|
parts = line.split()
|
|
if len(parts) >= 5:
|
|
clients.append((parts[0], parts[2], parts[3]))
|
|
return clients
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Ошибка при получении списка клиентов: {e}")
|
|
return []
|
|
|
|
def get_active_list() -> List[Tuple[str, str, str, str]]:
|
|
"""Получает список активных клиентов с информацией о последнем подключении."""
|
|
cmd = "docker exec $(docker ps -q -f name=amnezia-awg) awg show all latest-handshakes transfer peer"
|
|
try:
|
|
output = subprocess.check_output(cmd, shell=True, text=True).strip()
|
|
active_clients = []
|
|
for line in output.splitlines()[1:]:
|
|
parts = line.split()
|
|
if len(parts) >= 4:
|
|
active_clients.append((parts[0], parts[1], parts[2], parts[3]))
|
|
return active_clients
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Ошибка при получении активных клиентов: {e}")
|
|
return []
|
|
|
|
def set_user_expiration(user_name: str, expiration_date: Optional[datetime], traffic_limit: str, path: str = EXPIRATIONS_FILE) -> None:
|
|
"""Устанавливает срок действия и лимит трафика для пользователя."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
expirations = json.load(f)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
expirations = {}
|
|
|
|
expirations[user_name] = {
|
|
'expiration_date': expiration_date.isoformat() if expiration_date else None,
|
|
'traffic_limit': traffic_limit
|
|
}
|
|
|
|
with open(path, 'w') as f:
|
|
json.dump(expirations, f, indent=2)
|
|
logger.info(f"Expiration set for {user_name}: {expiration_date}")
|
|
|
|
def get_user_expiration(user_name: str, path: str = EXPIRATIONS_FILE) -> Optional[datetime]:
|
|
"""Получает срок действия подписки пользователя."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
expirations = json.load(f)
|
|
user_data = expirations.get(user_name, {})
|
|
expiration = user_data.get('expiration_date')
|
|
return datetime.fromisoformat(expiration) if expiration else None
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return None
|
|
|
|
def remove_user_expiration(user_name: str, path: str = EXPIRATIONS_FILE) -> None:
|
|
"""Удаляет информацию о сроке действия пользователя."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
expirations = json.load(f)
|
|
expirations.pop(user_name, None)
|
|
with open(path, 'w') as f:
|
|
json.dump(expirations, f, indent=2)
|
|
logger.info(f"Expiration removed for {user_name}")
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
pass
|
|
|
|
def clear_old_keys(before_date: str, path: str = EXPIRATIONS_FILE) -> bool:
|
|
"""Удаляет пользователей с истёкшими ключами до указанной даты."""
|
|
try:
|
|
before = datetime.fromisoformat(before_date)
|
|
with open(path, 'r') as f:
|
|
expirations = json.load(f)
|
|
|
|
users_to_remove = [
|
|
user for user, data in expirations.items()
|
|
if data.get('expiration_date') and datetime.fromisoformat(data['expiration_date']) < before
|
|
]
|
|
|
|
for user in users_to_remove:
|
|
deactive_user_db(user)
|
|
expirations.pop(user, None)
|
|
remove_user_telegram_id(user)
|
|
user_dir = os.path.join('users', user)
|
|
if os.path.exists(user_dir):
|
|
shutil.rmtree(user_dir, ignore_errors=True)
|
|
|
|
with open(path, 'w') as f:
|
|
json.dump(expirations, f, indent=2)
|
|
logger.info(f"Removed {len(users_to_remove)} expired keys before {before_date}")
|
|
return bool(users_to_remove)
|
|
except Exception as e:
|
|
logger.error(f"Ошибка при очистке старых ключей: {str(e)}")
|
|
return False
|
|
|
|
def add_payment(user_id: int, payment_id: str, amount: float, status: str, path: str = PAYMENTS_FILE) -> None:
|
|
"""Добавляет запись о платеже."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
payments = json.load(f)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
payments = []
|
|
|
|
payments.append({
|
|
'user_id': user_id,
|
|
'payment_id': payment_id,
|
|
'amount': amount,
|
|
'status': status,
|
|
'created_at': datetime.now(UTC).isoformat()
|
|
})
|
|
|
|
with open(path, 'w') as f:
|
|
json.dump(payments, f, indent=2)
|
|
logger.info(f"Payment added: {payment_id} for user {user_id}")
|
|
|
|
def update_payment_status(payment_id: str, status: str, path: str = PAYMENTS_FILE) -> None:
|
|
"""Обновляет статус платежа."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
payments = json.load(f)
|
|
for payment in payments:
|
|
if payment['payment_id'] == payment_id:
|
|
payment['status'] = status
|
|
payment['updated_at'] = datetime.now(UTC).isoformat()
|
|
break
|
|
with open(path, 'w') as f:
|
|
json.dump(payments, f, indent=2)
|
|
logger.info(f"Payment status updated: {payment_id} to {status}")
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
pass
|
|
|
|
def get_pending_payments(path: str = PAYMENTS_FILE) -> List[Tuple[int, str, float, str]]:
|
|
"""Получает список незавершённых платежей."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
payments = json.load(f)
|
|
return [
|
|
(p['user_id'], p['payment_id'], p['amount'], p['created_at'])
|
|
for p in payments if p['status'] == 'pending'
|
|
]
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return []
|
|
|
|
def add_admin(admin_id: int, path: str = ADMINS_FILE) -> None:
|
|
"""Добавляет нового администратора."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
admins = json.load(f)
|
|
admin_id_str = str(admin_id)
|
|
if admin_id_str not in admins:
|
|
admins.append(admin_id_str)
|
|
with open(path, 'w') as f:
|
|
json.dump(admins, f, indent=2)
|
|
logger.info(f"Admin added: {admin_id}")
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
with open(path, 'w') as f:
|
|
json.dump([str(admin_id)], f)
|
|
logger.info(f"Admin file created and admin added: {admin_id}")
|
|
|
|
def remove_admin(admin_id: int, path: str = ADMINS_FILE) -> None:
|
|
"""Удаляет администратора."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
admins = json.load(f)
|
|
admin_id_str = str(admin_id)
|
|
if admin_id_str in admins:
|
|
admins.remove(admin_id_str)
|
|
with open(path, 'w') as f:
|
|
json.dump(admins, f, indent=2)
|
|
logger.info(f"Admin removed: {admin_id}")
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
pass
|
|
|
|
def add_moderator(moderator_id: int, path: str = MODERATORS_FILE) -> None:
|
|
"""Добавляет нового модератора."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
moderators = json.load(f)
|
|
moderator_id_str = str(moderator_id)
|
|
if moderator_id_str not in moderators:
|
|
moderators.append(moderator_id_str)
|
|
with open(path, 'w') as f:
|
|
json.dump(moderators, f, indent=2)
|
|
logger.info(f"Moderator added: {moderator_id}")
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
with open(path, 'w') as f:
|
|
json.dump([str(moderator_id)], f)
|
|
logger.info(f"Moderator file created and moderator added: {moderator_id}")
|
|
|
|
def remove_moderator(moderator_id: int, path: str = MODERATORS_FILE) -> None:
|
|
"""Удаляет модератора."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
moderators = json.load(f)
|
|
moderator_id_str = str(moderator_id)
|
|
if moderator_id_str in moderators:
|
|
moderators.remove(moderator_id_str)
|
|
with open(path, 'w') as f:
|
|
json.dump(moderators, f, indent=2)
|
|
logger.info(f"Moderator removed: {moderator_id}")
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
pass
|
|
|
|
def add_promocode(code: str, discount: float, expires_at: Optional[datetime], max_uses: Optional[int], subscription_period: Optional[str], path: str = PROMOCODES_FILE) -> bool:
|
|
"""Добавляет новый промокод."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
promocodes = json.load(f)
|
|
if code in promocodes:
|
|
logger.warning(f"Promocode {code} already exists")
|
|
return False
|
|
promocodes[code] = {
|
|
'discount': float(discount),
|
|
'expires_at': expires_at.isoformat() if expires_at else None,
|
|
'max_uses': max_uses,
|
|
'uses': 0,
|
|
'subscription_period': subscription_period
|
|
}
|
|
with open(path, 'w') as f:
|
|
json.dump(promocodes, f, indent=2)
|
|
logger.info(f"Promocode added: {code}")
|
|
return True
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
promocodes = {code: {
|
|
'discount': float(discount),
|
|
'expires_at': expires_at.isoformat() if expires_at else None,
|
|
'max_uses': max_uses,
|
|
'uses': 0,
|
|
'subscription_period': subscription_period
|
|
}}
|
|
with open(path, 'w') as f:
|
|
json.dump(promocodes, f, indent=2)
|
|
logger.info(f"Promocode file created and promocode added: {code}")
|
|
return True
|
|
|
|
def remove_promocode(code: str, path: str = PROMOCODES_FILE) -> bool:
|
|
"""Удаляет промокод."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
promocodes = json.load(f)
|
|
if code in promocodes:
|
|
del promocodes[code]
|
|
with open(path, 'w') as f:
|
|
json.dump(promocodes, f, indent=2)
|
|
logger.info(f"Promocode removed: {code}")
|
|
return True
|
|
return False
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return False
|
|
|
|
def get_promocodes(path: str = PROMOCODES_FILE) -> Dict[str, Dict]:
|
|
"""Получает список всех промокодов."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
return json.load(f)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return {}
|
|
|
|
def apply_promocode(code: str, path: str = PROMOCODES_FILE) -> Optional[Dict[str, Any]]:
|
|
"""Применяет промокод и возвращает его данные, если он действителен."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
promocodes = json.load(f)
|
|
promo = promocodes.get(code)
|
|
if not promo:
|
|
return None
|
|
expires_at = datetime.fromisoformat(promo['expires_at']) if promo['expires_at'] else None
|
|
if expires_at and expires_at < datetime.now(UTC):
|
|
logger.info(f"Promocode {code} expired")
|
|
return None
|
|
if promo['max_uses'] is not None and promo['uses'] >= promo['max_uses']:
|
|
logger.info(f"Promocode {code} max uses reached")
|
|
return None
|
|
promo['uses'] += 1
|
|
with open(path, 'w') as f:
|
|
json.dump(promocodes, f, indent=2)
|
|
logger.info(f"Promocode applied: {code}")
|
|
return {
|
|
'discount': promo['discount'],
|
|
'subscription_period': promo['subscription_period']
|
|
}
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return None
|
|
|
|
def set_user_telegram_id(user_name: str, telegram_id: Optional[int], path: str = USER_TELEGRAM_IDS_FILE) -> None:
|
|
"""Сопоставляет имя пользователя с Telegram ID."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
user_ids = json.load(f)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
user_ids = {}
|
|
|
|
user_ids[user_name] = telegram_id
|
|
with open(path, 'w') as f:
|
|
json.dump(user_ids, f, indent=2)
|
|
logger.info(f"Telegram ID {telegram_id} set for user {user_name}")
|
|
|
|
def get_user_telegram_id(user_name: str, path: str = USER_TELEGRAM_IDS_FILE) -> Optional[int]:
|
|
"""Получает Telegram ID пользователя."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
user_ids = json.load(f)
|
|
return user_ids.get(user_name)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return None
|
|
|
|
def remove_user_telegram_id(user_name: str, path: str = USER_TELEGRAM_IDS_FILE) -> None:
|
|
"""Удаляет Telegram ID пользователя."""
|
|
try:
|
|
with open(path, 'r') as f:
|
|
user_ids = json.load(f)
|
|
user_ids.pop(user_name, None)
|
|
with open(path, 'w') as f:
|
|
json.dump(user_ids, f, indent=2)
|
|
logger.info(f"Telegram ID removed for user {user_name}")
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
pass
|
|
|
|
def create_backup() -> str:
|
|
"""Создаёт резервную копию всех данных."""
|
|
backup_filename = os.path.join(BACKUP_DIR, f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip")
|
|
os.makedirs(BACKUP_DIR, exist_ok=True)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
temp_files_dir = os.path.join(temp_dir, 'files')
|
|
temp_users_dir = os.path.join(temp_dir, 'users')
|
|
shutil.copytree('files', temp_files_dir, ignore=shutil.ignore_patterns('backups'))
|
|
if os.path.exists('users'):
|
|
shutil.copytree('users', temp_users_dir)
|
|
|
|
with zipfile.ZipFile(backup_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
for root, _, files in os.walk(temp_dir):
|
|
for file in files:
|
|
file_path = os.path.join(root, file)
|
|
arcname = os.path.relpath(file_path, temp_dir)
|
|
zipf.write(file_path, arcname)
|
|
|
|
logger.info(f"Backup created: {backup_filename}")
|
|
return backup_filename
|
|
|
|
def get_user_traffic(user_name: str) -> Tuple[int, int]:
|
|
"""Получает данные о трафике пользователя (входящий/исходящий)."""
|
|
active_clients = get_active_list()
|
|
for client in active_clients:
|
|
if client[0] == user_name and len(client) > 2:
|
|
try:
|
|
incoming, outgoing = client[2].split('/')
|
|
incoming_bytes = humanize.parse_bytes(incoming.strip())
|
|
outgoing_bytes = humanize.parse_bytes(outgoing.strip())
|
|
return incoming_bytes, outgoing_bytes
|
|
except:
|
|
pass
|
|
return 0, 0
|
|
|
|
def get_user_status(user_name: str) -> str:
|
|
"""Получает статус пользователя (онлайн/офлайн)."""
|
|
active_clients = get_active_list()
|
|
for client in active_clients:
|
|
if client[0] == user_name and len(client) > 1:
|
|
last_handshake = client[1]
|
|
if last_handshake.lower() in ['never', 'нет данных', '-']:
|
|
return "offline"
|
|
try:
|
|
handshake_time = parse_relative_time(last_handshake)
|
|
if (datetime.now(UTC) - handshake_time).total_seconds() <= 60:
|
|
return "online"
|
|
except:
|
|
pass
|
|
return "offline"
|
|
|
|
def parse_relative_time(relative_str: str) -> datetime:
|
|
"""Парсит относительное время (например, '2 minutes ago') в datetime."""
|
|
if not isinstance(relative_str, str) or not relative_str.strip():
|
|
return datetime.now(UTC)
|
|
try:
|
|
relative_str = relative_str.lower().replace(' ago', '')
|
|
delta = 0
|
|
for part in relative_str.split(', '):
|
|
num, unit = part.split()
|
|
num = int(num)
|
|
if 'minute' in unit:
|
|
delta += num * 60
|
|
elif 'hour' in unit:
|
|
delta += num * 3600
|
|
elif 'day' in unit:
|
|
delta += num * 86400
|
|
elif 'week' in unit:
|
|
delta += num * 604800
|
|
elif 'month' in unit:
|
|
delta += num * 2592000
|
|
return datetime.now(UTC) - timedelta(seconds=delta)
|
|
except Exception as e:
|
|
logger.error(f"Ошибка в parse_relative_time: {str(e)}")
|
|
return datetime.now(UTC)
|
|
|
|
if __name__ == "__main__":
|
|
# Тестирование или инициализация
|
|
ensure_files_exist()
|
|
if not os.path.exists(CONFIG_FILE):
|
|
create_config()
|