Compare commits

...

48 Commits

Author SHA1 Message Date
Fr1ngg 6d9a5fb578 Update README.md 2025-08-07 08:23:07 +03:00
Fr1ngg 44d19245c4 Update admin_handlers.py 2025-08-07 08:19:54 +03:00
Fr1ngg ce36c268cc Update main.py 2025-08-07 07:53:09 +03:00
Fr1ngg 1357b07921 Update middlewares.py 2025-08-07 07:52:49 +03:00
Fr1ngg d4bfdcb749 Update config.py 2025-08-07 07:52:33 +03:00
Fr1ngg 39621706c8 Update subscription_monitor.py 2025-08-07 07:52:01 +03:00
Fr1ngg 4684e7a1e5 Update utils.py 2025-08-07 07:51:36 +03:00
Fr1ngg 16bb3c8ad3 Update translations.py 2025-08-07 07:51:15 +03:00
Fr1ngg 16fbdcc655 Create api_error_handlers.py 2025-08-07 07:50:56 +03:00
Fr1ngg 416d268352 Update remnawave_api.py 2025-08-07 07:50:30 +03:00
Fr1ngg 44fb88f368 Update keyboards.py 2025-08-07 07:50:06 +03:00
Fr1ngg f050c12253 Update database.py 2025-08-07 07:49:48 +03:00
Fr1ngg 82486d121e Update handlers.py 2025-08-07 07:49:25 +03:00
Fr1ngg 2bf382dbac Update handlers.py 2025-08-07 07:49:06 +03:00
Fr1ngg ab562e0df0 Update handlers.py 2025-08-07 07:48:48 +03:00
Fr1ngg 02e8fba9d0 Update admin_handlers.py 2025-08-07 07:48:21 +03:00
Fr1ngg 530df33ba4 Update README.md 2025-08-05 15:41:41 +03:00
Fr1ngg 8b2e8cb681 Update README.md 2025-08-05 15:40:27 +03:00
Fr1ngg 2cb8c937e1 Update README.md 2025-08-05 15:39:33 +03:00
Fr1ngg ea5c08fb58 Update README.md 2025-08-05 15:35:42 +03:00
Fr1ngg 2507168916 Update README.md 2025-08-05 15:31:45 +03:00
Fr1ngg ec6e08f0d3 Update middlewares.py 2025-08-05 15:25:23 +03:00
Fr1ngg ceee7cc977 Update remnawave_api.py 2025-08-05 15:25:06 +03:00
Fr1ngg ece05cd9e9 Update main.py 2025-08-05 15:24:35 +03:00
Fr1ngg 2345016aa3 Update main.py 2025-08-05 15:24:16 +03:00
Fr1ngg d349352812 Update utils.py 2025-08-05 15:23:57 +03:00
Fr1ngg e002ca2cb2 Update handlers.py 2025-08-05 15:23:35 +03:00
Fr1ngg 849afb4aa4 Update admin_handlers.py 2025-08-05 15:23:15 +03:00
Fr1ngg 943628e6fe Update keyboards.py 2025-08-05 15:22:50 +03:00
Fr1ngg 6621de95c0 Update database.py
Fix admin_history_payments
2025-08-05 15:22:15 +03:00
Fr1ngg 87797153be Update README.md 2025-08-05 07:37:15 +03:00
Fr1ngg 4e833f9b8c Create docker-build.yml 2025-08-05 07:30:52 +03:00
Fr1ngg 560a2c362f Delete .github/workflows/build.yml 2025-08-05 07:22:59 +03:00
Fr1ngg 71e9e1ebe2 Create build.yml 2025-08-05 07:17:51 +03:00
Fr1ngg 73ed24aecf Update README.md 2025-08-05 05:41:43 +03:00
Fr1ngg 09eff6cf8e Update README.md 2025-08-05 05:38:04 +03:00
Fr1ngg ae7a36523b Create Dockerfile 2025-08-05 05:32:03 +03:00
Fr1ngg 48a111da76 Create Makefile 2025-08-05 05:31:27 +03:00
Fr1ngg 0f94a7cdcd Update requirements.txt 2025-08-05 05:30:43 +03:00
Fr1ngg 79761b16bf Update docker-compose.yml 2025-08-05 05:30:01 +03:00
Fr1ngg 7235caae0b Delete Dockerfile 2025-08-05 04:26:57 +03:00
Fr1ngg 2f294cdd21 Update run.sh 2025-08-05 04:23:26 +03:00
Fr1ngg 33a124c1d2 Update run.sh 2025-08-05 04:23:11 +03:00
Fr1ngg 91ed42ff08 Update README.md 2025-08-05 04:21:03 +03:00
Fr1ngg a2ffb3396e Update README.md 2025-08-05 04:19:13 +03:00
Fr1ngg 6846d58d46 Update README.md 2025-08-05 04:17:58 +03:00
Fr1ngg 0177ae064f Update README.md 2025-08-05 03:42:25 +03:00
Fr1ngg 72caed0138 Update README.md 2025-08-05 03:37:39 +03:00
15 changed files with 6212 additions and 353 deletions
+31
View File
@@ -0,0 +1,31 @@
name: BedolagaBot
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: fr1ngg/remnawave-bedolaga-telegram-bot:latest
+31 -15
View File
@@ -1,30 +1,46 @@
# Используем официальный Python образ
# Use Python 3.11 slim image
FROM python:3.11-slim
# Устанавливаем рабочую директорию
# Set working directory
WORKDIR /app
# Устанавливаем системные зависимости
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
libpq-dev \
curl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Копируем файл requirements.txt
# Create non-root user
RUN groupadd -r botuser && useradd -r -g botuser botuser
# Copy requirements first for better caching
COPY requirements.txt .
# Устанавливаем Python зависимости
RUN pip install --no-cache-dir -r requirements.txt
# Install Python dependencies
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
pip install --no-cache-dir -r requirements.txt --prefer-binary
# Копируем исходный код
# Copy application code
COPY . .
# Создаем пользователя для безопасности
RUN useradd --create-home --shell /bin/bash app \
&& chown -R app:app /app
USER app
# Create necessary directories
RUN mkdir -p /app/logs /app/data && \
chown -R botuser:botuser /app
# Открываем порт (если нужен для веб-хуков)
EXPOSE 8000
# Switch to non-root user
USER botuser
# Команда запуска
CMD ["python", "main.py"]
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD python -c "import asyncio; import sys; sys.exit(0)"
# Default command
CMD ["python3", "main.py"]
+96
View File
@@ -0,0 +1,96 @@
# RemnaWave Bot Docker Management
.PHONY: help build up down restart logs clean db-backup db-restore
# Default target
help:
@echo "Available commands:"
@echo " build - Build all Docker images"
@echo " up - Start all services"
@echo " up-min - Start only bot and database (minimal setup)"
@echo " up-full - Start all services including nginx and redis"
@echo " down - Stop all services"
@echo " restart - Restart all services"
@echo " logs - Show logs for all services"
@echo " logs-bot - Show logs for bot service only"
@echo " logs-db - Show logs for database service only"
@echo " clean - Remove all containers, networks, and volumes"
@echo " db-backup - Create database backup"
@echo " db-restore - Restore database from backup"
@echo " shell-bot - Open shell in bot container"
@echo " shell-db - Open shell in database container"
# Build all images
build:
docker compose build
# Start minimal services (bot + database)
up-min: setup-dirs
docker compose up -d postgres bot
# Start all services including optional ones
up-full: setup-dirs
docker compose --profile with-nginx up -d
# Start main services (default)
up: setup-dirs
docker compose up -d postgres redis bot
# Setup required directories
setup-dirs:
mkdir -p logs data backups
# Stop all services
down:
docker compose down
# Restart all services
restart:
docker compose restart
# Show logs for all services
logs:
docker compose logs -f
# Show logs for bot only
logs-bot:
docker compose logs -f bot
# Show logs for database only
logs-db:
docker compose logs -f postgres
# Clean up everything (DANGEROUS - removes all data)
clean:
@echo "This will remove all containers, networks, and volumes. Are you sure? [y/N]"
@read answer && [ "$$answer" = "y" ] || [ "$$answer" = "Y" ]
docker compose down -v --remove-orphans
docker system prune -f
# Database backup
db-backup:
@mkdir -p backups
docker compose exec postgres pg_dump -U remnawave_user remnawave_bot > backups/backup_$(shell date +%Y%m%d_%H%M%S).sql
@echo "Backup created in backups/ directory"
# Database restore (use: make db-restore BACKUP=backup_20231201_120000.sql)
db-restore:
@if [ -z "$(BACKUP)" ]; then echo "Usage: make db-restore BACKUP=backup_file.sql"; exit 1; fi
docker compose exec -T postgres psql -U remnawave_user -d remnawave_bot < backups/$(BACKUP)
@echo "Database restored from $(BACKUP)"
# Open shell in bot container
shell-bot:
docker compose exec bot /bin/bash
# Open shell in database container
shell-db:
docker compose exec postgres psql -U remnawave_user -d remnawave_bot
# Check services status
status:
docker compose ps
# View service resource usage
stats:
docker stats --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
+67 -32
View File
@@ -1,8 +1,11 @@
<img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 15 13" src="https://github.com/user-attachments/assets/91098622-1bce-4f27-afef-60a3c5b5061f" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 22" src="https://github.com/user-attachments/assets/46b87e75-b420-4ac6-91b9-8c7e9bcffb2a" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 39" src="https://github.com/user-attachments/assets/ca97811f-ca00-4133-a120-1c11f0efa0fc" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 45" src="https://github.com/user-attachments/assets/258e1adb-2c39-4126-82a7-7791b56d42db" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 53" src="https://github.com/user-attachments/assets/073455fc-f42d-4d70-839d-59042add2d94" /><img width="906" height="316" alt="Снимок экрана 2025-08-05 в 03 16 00" src="https://github.com/user-attachments/assets/2034dde8-a48b-4149-a23f-b788aa40e0b1" /><img width="906" height="366" alt="Снимок экрана 2025-08-05 в 03 16 18" src="https://github.com/user-attachments/assets/3a3d1e0a-92fc-4573-a48c-f36481d6d0de" /><img width="906" height="842" alt="Снимок экрана 2025-08-05 в 03 17 24" src="https://github.com/user-attachments/assets/8b407f69-6861-4810-822e-c3f7b8f63629" /><img width="906" height="274" alt="Снимок экрана 2025-08-05 в 03 17 43" src="https://github.com/user-attachments/assets/923a945a-5ef8-4dcb-9804-fffc37ab8887" /><img width="936" height="364" alt="Снимок экрана 2025-08-05 в 03 20 03" src="https://github.com/user-attachments/assets/1faecdfe-f80c-4ac2-ad38-81a30fc6623d" />
<img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 15 13" src="https://github.com/user-attachments/assets/91098622-1bce-4f27-afef-60a3c5b5061f" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 22" src="https://github.com/user-attachments/assets/46b87e75-b420-4ac6-91b9-8c7e9bcffb2a" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 39" src="https://github.com/user-attachments/assets/ca97811f-ca00-4133-a120-1c11f0efa0fc" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 45" src="https://github.com/user-attachments/assets/258e1adb-2c39-4126-82a7-7791b56d42db" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 53" src="https://github.com/user-attachments/assets/073455fc-f42d-4d70-839d-59042add2d94" /><img width="906" height="316" alt="Снимок экрана 2025-08-05 в 03 16 00" src="https://github.com/user-attachments/assets/2034dde8-a48b-4149-a23f-b788aa40e0b1" /><img width="894" height="317" alt="Снимок экрана 2025-08-05 в 15 32 32" src="https://github.com/user-attachments/assets/a96337cf-f58a-488e-9600-c94a92bdbfc2" /><img width="906" height="366" alt="Снимок экрана 2025-08-05 в 03 16 18" src="https://github.com/user-attachments/assets/3a3d1e0a-92fc-4573-a48c-f36481d6d0de" /><img width="906" height="842" alt="Снимок экрана 2025-08-05 в 03 17 24" src="https://github.com/user-attachments/assets/8b407f69-6861-4810-822e-c3f7b8f63629" /><img width="906" height="274" alt="Снимок экрана 2025-08-05 в 03 17 43" src="https://github.com/user-attachments/assets/923a945a-5ef8-4dcb-9804-fffc37ab8887" /><img width="936" height="364" alt="Снимок экрана 2025-08-05 в 03 20 03" src="https://github.com/user-attachments/assets/1faecdfe-f80c-4ac2-ad38-81a30fc6623d" /><img width="892" height="486" alt="Снимок экрана 2025-08-07 в 07 43 47" src="https://github.com/user-attachments/assets/0dd6cb8e-fd2f-4a98-8920-aadceee09fd0" /><img width="892" height="762" alt="Снимок экрана 2025-08-07 в 07 44 20" src="https://github.com/user-attachments/assets/d7c95e3e-cf04-40bc-9422-d7289447625d" /><img width="892" height="823" alt="Снимок экрана 2025-08-07 в 07 46 45" src="https://github.com/user-attachments/assets/9ab2c378-0abc-447d-9e95-a3ab8dab2f18" />
<img width="892" height="501" alt="Снимок экрана 2025-08-07 в 07 42 07" src="https://github.com/user-attachments/assets/839c02da-4461-4127-894a-772e66175e23" /><img width="892" height="805" alt="Снимок экрана 2025-08-07 в 07 57 01" src="https://github.com/user-attachments/assets/bc35f79d-0b0d-4c81-8623-696b708642ad" /><img width="892" height="834" alt="Снимок экрана 2025-08-07 в 07 41 09" src="https://github.com/user-attachments/assets/d4731a79-0171-4254-aa78-2e4c7305829b" /><img width="631" height="606" alt="Снимок экрана 2025-08-06 в 18 48 31" src="https://github.com/user-attachments/assets/c44548b0-f27b-4f67-b3c0-2c002ae33979" />
#Описание
RemnaWave Telegram Bot — это многофункциональный бот для управления подписками(Для каждой подписки возможно назначить свой сквад со своими инбаундами - нововведение Remnawave 2.0.0+), балансом, промокодами, тестовой подпиской и рассылками пользователям через Telegram.
RemnaWave Bedolaga Telegram Bot — это многофункциональный бот для управления подписками(Для каждой подписки возможно назначить свой сквад со своими инбаундами - нововведение Remnawave 2.0.0+), балансом, промокодами, тестовой подпиской и рассылками пользователям через Telegram.
Бот интегрирован с системой RemnaWave версии 2.0.8
@@ -12,7 +15,7 @@ RemnaWave Telegram Bot — это многофункциональный бот
Создание и покупка подписок с управлением трафиком, длительностью и ценой
Бесплатная тестовая подписка с ограничениями
Бесплатная тестовая подписка с заданными ограничениями(срок, лимит трафика, назначение сквада)
Пополнение баланса: 1) Через саппорт в ручную 2) Отправка заявки с суммой админу (С возможность подтвердить/отклонить заявку)
@@ -28,7 +31,11 @@ RemnaWave Telegram Bot — это многофункциональный бот
Интеграция с RemnaWave API для управления подписками и пользователями RemnaWave
История платежей(Не работает, в доработке) и управление платежами (подтверждение, отклонение)
Полная синхранизация Remnawave <--> Bot - Перенос подписок из панели Remnawave в бот по Telegram id
Управление системой Remnawave (NEW)
Управление платежами (подтверждение, отклонение) + История платежей(Все действия с балансом и подписками в постраничной истории)
#Требования
@@ -47,21 +54,12 @@ URL и токен RemnaWave API
#Установка
1) Клонируйте репозиторий:
1. Клонируйте репозиторий:
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot
cd remnawave-bedolaga-telegram-bot
2) Установите python3 python pip
sudo apt install pip
sudo apt install python3
3) Установите зависимости:
pip install -r requirements.txt
4) Создайте файл .env в корне проекта и заполните его необходимыми переменными окружения. Пример:
2. Создайте файл .env в корне проекта и заполните его необходимыми переменными окружения. Пример:
BOT_TOKEN=ваш_telegram_bot_token
REMNAWAVE_URL=https://your-remnawave-url.ru
@@ -77,25 +75,46 @@ URL и токен RemnaWave API
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=19bd5bde-5eea-4368-809c-6ba1ffb93897
TRIAL_PRICE=0.0
MONITOR_CHECK_INTERVAL=3600
MONITOR_DAILY_CHECK_HOUR=10
MONITOR_WARNING_DAYS=2
5) Запустите бота:
1) Хлебный - создание службы автозапуска, проверка файлов, запуск бота
4. Соберите образ (Makefile Dockerfile docker-compose):
chmod +x run.sh
./run.sh
make build
Если создали службу через скрипт, то запустить бота можно командой
5. Запуск:
sudo systemctl start remnawave-bot
Запуск минимальной конфигурации (бот + база данных):
make up-min
Выключить
Или запуск с Redis:
make up
sudo systemctl stop remnawave-bot
Или запуск со всеми сервисами включая Nginx:
make up-full
3) Для мужчин (Службу там поднять самому, докерфайл собрать или под скрином развернуть - уже твое дело)
5. Управление
python main.py
Просмотр логов:
make logs-bot
Статус сервисов:
make status
Перезапуск:
make restart
Остановка:
make down
#Конфигурация
@@ -138,7 +157,17 @@ MONITOR_WARNING_DAYS=2 (За сколько дней слать уведомле
#Использование
/start
/start - запуск
#Синхронизация подписок
Вы можете перенести свои существующие подписки из панели Remnawave прямо в бота всего одним кликом.
Для этого в админ панеле реализован соостветствующий пункт: Админ панель - Система Remnawave - Синхронизация с Remnawave - Импорт всех по Telegram ID. После нажатия подтянет всех пользователей в бота, подпискам из панели будет назначено имя "Старая подписка" - такую подписку невозможно продлить.
ДОПОЛНИТЕЛЬНО:
Реализована возможность зачистки импортированных из панели подписок по тг айди Админ панель - Система Remnawave - Синхронизация с Remnawave - Просмотрт планов - Удалалить импортированные
Остальное трогать без понимания кода - не рекомендую.
#Структура проекта
@@ -188,12 +217,18 @@ run.sh — скрипт установки и управления ботом (
Мониторинг подписок (Проверка статуса службы, принудитедьный запуск, деактивация истекщих подписок(на случай падения базы), персональный тест(можно отправить уведомления юзеру по tg id)
Просмотр краткой статистики
Просмотр статистики
Управление систеой Remnawave (Ноды, пользователи, синхронизация и импорт подписок из базы Remnawave в бот)
#ToDo
1) Код колхозный и не без вайбкодинга тут обошлось, но будет допиливаться, текущая реализация работает - уже хорошо
Код колхозный и не без вайбкодинга тут обошлось, но будет допиливаться, текущая реализация работает - уже хорошо
1) Дописать службу для оповещения об истечении срока подписки и контроля - Done v1.1.0
2) Подключить различные шлюзы для пополнения баланса
3) Дописать службу для оповещения об истечении срока подписки и контроля
4) Синхранизацию с Remnawave между пользователями по тг id
5) Полнофункциональную панель упарвления
3) Синхранизацию с Remnawave между пользователями по тг id
4) Полнофункциональную панель упарвления
5) Добавить возможность удаление промокодов - In progress
6) Доработать алгоритм удаления подписок ибо удаление(А НЕ деактивация) сейчас - скроект эту подписку у всех юзеров которые ее купили, так что удаляйте на свой страх и риск я предупредил) - In progress
8) Отправка уведомлений административных в другие чаты-топики
9) Рефка (как по мне беспонтовая штука, сервера нормальные хостите, сервис нормальный делайте и будут клиенты - не ебите мозги, но если будет не лень, то допилю)
+4322 -17
View File
File diff suppressed because it is too large Load Diff
+233
View File
@@ -0,0 +1,233 @@
# api_error_handlers.py - Дополнительные утилиты для обработки ошибок API
import logging
from typing import Optional, Dict, Any, Callable
from aiogram.types import CallbackQuery
from aiogram import Router
from remnawave_api import RemnaWaveAPI
from translations import t
logger = logging.getLogger(__name__)
class APIErrorHandler:
"""Класс для обработки ошибок API и предоставления пользователю понятной информации"""
@staticmethod
async def handle_api_error(callback: CallbackQuery, error: Exception,
operation: str, user_language: str = 'ru',
fallback_keyboard=None) -> bool:
"""
Обработка ошибок API с отправкой понятного сообщения пользователю
Returns:
bool: True если ошибка была обработана, False если нужно перепробросить
"""
error_message = str(error).lower()
if "timeout" in error_message or "connection" in error_message:
text = "⏱ Таймаут подключения к API\n\n"
text += "Возможные причины:\n"
text += "• Медленный интернет\n"
text += "• Перегрузка сервера RemnaWave\n"
text += "• Временные проблемы с сетью\n\n"
text += "🔄 Попробуйте повторить операцию через несколько секунд"
elif "401" in error_message or "unauthorized" in error_message:
text = "🔐 Ошибка авторизации API\n\n"
text += "Токен доступа недействителен или истек.\n"
text += "Обратитесь к администратору для обновления токена."
elif "404" in error_message or "not found" in error_message:
text = f"❌ Ресурс не найден\n\n"
text += f"Операция: {operation}\n"
text += "Возможно, запрашиваемый объект был удален или не существует."
elif "500" in error_message or "internal server error" in error_message:
text = "🔥 Внутренняя ошибка сервера RemnaWave\n\n"
text += "Сервер временно недоступен.\n"
text += "Попробуйте повторить операцию позже."
else:
text = f"❌ Ошибка API операции: {operation}\n\n"
text += f"Детали: {str(error)[:100]}{'...' if len(str(error)) > 100 else ''}\n\n"
text += "Обратитесь к администратору если проблема повторяется."
try:
await callback.message.edit_text(
text,
reply_markup=fallback_keyboard or error_recovery_keyboard(operation, user_language)
)
return True
except Exception as edit_error:
logger.error(f"Failed to edit message with error info: {edit_error}")
try:
await callback.answer(f"❌ Ошибка: {operation}", show_alert=True)
return True
except:
return False
@staticmethod
async def safe_api_call(api_method: Callable, *args, **kwargs) -> tuple[bool, Any]:
"""
Безопасный вызов метода API с обработкой ошибок
Returns:
tuple: (success: bool, result: Any)
"""
try:
result = await api_method(*args, **kwargs)
return True, result
except Exception as e:
logger.error(f"API call failed: {api_method.__name__} - {e}")
return False, str(e)
# Дополнительные обработчики для исправления конкретных проблем
def create_error_recovery_keyboard(error_context: str, language: str = 'ru'):
"""Создание клавиатуры для восстановления после ошибки"""
from keyboards import error_recovery_keyboard
return error_recovery_keyboard(error_context, language)
# Улучшенные функции для работы с RemnaWave API
async def safe_get_nodes(api: RemnaWaveAPI) -> tuple[bool, list]:
"""Безопасное получение списка нод"""
try:
logger.info("Attempting to fetch nodes from API...")
nodes = await api.get_all_nodes()
if nodes is None:
logger.warning("API returned None for nodes")
return False, []
if not isinstance(nodes, list):
logger.warning(f"API returned non-list for nodes: {type(nodes)}")
return False, []
logger.info(f"Successfully fetched {len(nodes)} nodes")
return True, nodes
except Exception as e:
logger.error(f"Error fetching nodes: {e}")
return False, []
async def safe_get_system_users(api: RemnaWaveAPI) -> tuple[bool, list]:
"""Безопасное получение списка пользователей системы"""
try:
logger.info("Attempting to fetch system users from API...")
users = await api.get_all_system_users_full()
if users is None:
logger.warning("API returned None for users")
return False, []
if not isinstance(users, list):
logger.warning(f"API returned non-list for users: {type(users)}")
return False, []
logger.info(f"Successfully fetched {len(users)} users")
return True, users
except Exception as e:
logger.error(f"Error fetching system users: {e}")
return False, []
async def safe_restart_nodes(api: RemnaWaveAPI, all_nodes: bool = True, node_id: str = None) -> tuple[bool, str]:
"""Безопасная перезагрузка нод"""
try:
if all_nodes:
logger.info("Attempting to restart all nodes...")
result = await api.restart_all_nodes()
else:
logger.info(f"Attempting to restart node {node_id}...")
result = await api.restart_node(node_id)
if result:
message = "Команда перезагрузки отправлена успешно"
logger.info(f"Restart command sent successfully")
return True, message
else:
message = "API вернул отрицательный результат"
logger.warning("API returned negative result for restart")
return False, message
except Exception as e:
logger.error(f"Error restarting nodes: {e}")
return False, str(e)
# Функции для проверки состояния API
async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
"""Проверка состояния API"""
health_info = {
'api_available': False,
'nodes_accessible': False,
'users_accessible': False,
'system_stats_accessible': False,
'errors': []
}
if api is None:
health_info['errors'].append("API instance is None")
return health_info
# Проверяем доступность API
try:
# Простая проверка через получение нод (обычно быстрая операция)
success, nodes = await safe_get_nodes(api)
if success:
health_info['api_available'] = True
health_info['nodes_accessible'] = True
else:
health_info['errors'].append("Cannot fetch nodes")
except Exception as e:
health_info['errors'].append(f"Nodes check failed: {e}")
# Проверяем доступность пользователей
try:
success, users = await safe_get_system_users(api)
if success:
health_info['users_accessible'] = True
else:
health_info['errors'].append("Cannot fetch users")
except Exception as e:
health_info['errors'].append(f"Users check failed: {e}")
# Проверяем системную статистику
try:
stats = await api.get_system_stats()
if stats:
health_info['system_stats_accessible'] = True
else:
health_info['errors'].append("Cannot fetch system stats")
except Exception as e:
health_info['errors'].append(f"System stats check failed: {e}")
return health_info
# Декоратор для автоматической обработки ошибок API
def handle_api_errors(operation_name: str):
"""Декоратор для автоматической обработки ошибок API в handler'ах"""
def decorator(func):
async def wrapper(callback: CallbackQuery, user, *args, **kwargs):
try:
return await func(callback, user, *args, **kwargs)
except Exception as e:
logger.error(f"Error in {func.__name__}: {e}")
# Получаем API из kwargs если есть
api = kwargs.get('api')
fallback_keyboard = None
# Создаем fallback клавиатуру в зависимости от операции
if 'nodes' in operation_name.lower():
from keyboards import admin_system_keyboard
fallback_keyboard = admin_system_keyboard(user.language)
elif 'users' in operation_name.lower():
from keyboards import system_users_keyboard
fallback_keyboard = system_users_keyboard(user.language)
# Обрабатываем ошибку
await APIErrorHandler.handle_api_error(
callback, e, operation_name, user.language, fallback_keyboard
)
return wrapper
return decorator
+225 -20
View File
@@ -1,6 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text
from datetime import datetime
from typing import Optional, List
import logging
@@ -38,6 +38,7 @@ class Subscription(Base):
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
is_trial: Mapped[bool] = mapped_column(Boolean, default=False)
is_imported: Mapped[bool] = mapped_column(Boolean, default=False)
class UserSubscription(Base):
__tablename__ = 'user_subscriptions'
@@ -48,7 +49,9 @@ class UserSubscription(Base):
short_uuid: Mapped[str] = mapped_column(String(255)) # УБРАНО unique=True
expires_at: Mapped[datetime] = mapped_column(DateTime)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
traffic_limit_gb: Mapped[Optional[int]] = mapped_column(Integer) # Добавлено поле
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, onupdate=datetime.utcnow) # Добавлено поле
class Payment(Base):
__tablename__ = 'payments'
@@ -100,6 +103,10 @@ class Database:
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Выполняем миграции
await self.migrate_user_subscriptions()
await self.migrate_subscription_imported_field()
async def close(self):
await self.engine.dispose()
@@ -166,7 +173,7 @@ class Database:
return False
# Subscription methods
async def get_all_subscriptions(self, include_inactive: bool = False, exclude_trial: bool = True) -> List[Subscription]:
async def get_all_subscriptions(self, include_inactive: bool = False, exclude_trial: bool = True, exclude_imported: bool = True) -> List[Subscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
@@ -175,12 +182,39 @@ class Database:
query = query.where(Subscription.is_active == True)
if exclude_trial:
query = query.where(Subscription.is_trial == False)
if exclude_imported:
query = query.where(Subscription.is_imported == False) # Исключаем импортированные
result = await session.execute(query)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting subscriptions: {e}")
return []
async def get_all_subscriptions_admin(self) -> List[Subscription]:
"""Get all subscriptions including imported ones (for admin purposes)"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(select(Subscription))
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting admin subscriptions: {e}")
return []
async def migrate_subscription_imported_field(self):
"""Add is_imported field to subscriptions table"""
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("""
ALTER TABLE subscriptions
ADD COLUMN IF NOT EXISTS is_imported BOOLEAN DEFAULT FALSE
"""))
logger.info("Successfully added is_imported field to subscriptions table")
except Exception as e:
logger.info(f"Migration may have already been applied: {e}")
except Exception as e:
logger.error(f"Error during subscription migration: {e}")
async def get_subscription_by_id(self, subscription_id: int) -> Optional[Subscription]:
async with self.session_factory() as session:
@@ -196,7 +230,7 @@ class Database:
async def create_subscription(self, name: str, description: str, price: float,
duration_days: int, traffic_limit_gb: int,
squad_uuid: str) -> Subscription:
squad_uuid: str, is_imported: bool = False) -> Subscription:
async with self.session_factory() as session:
try:
subscription = Subscription(
@@ -205,7 +239,8 @@ class Database:
price=price,
duration_days=duration_days,
traffic_limit_gb=traffic_limit_gb,
squad_uuid=squad_uuid
squad_uuid=squad_uuid,
is_imported=is_imported # Добавляем поддержку is_imported
)
session.add(subscription)
await session.commit()
@@ -253,26 +288,47 @@ class Database:
logger.error(f"Error getting user subscriptions for {user_id}: {e}")
return []
async def create_user_subscription(self, user_id: int, subscription_id: int,
short_uuid: str, expires_at: datetime) -> UserSubscription:
async def create_user_subscription(self, user_id: int, subscription_id: int,
short_uuid: str, expires_at: datetime,
is_active: bool = True, traffic_limit_gb: int = None) -> Optional[UserSubscription]:
"""Create user subscription with proper error handling"""
async with self.session_factory() as session:
try:
user_sub = UserSubscription(
# Проверяем что подписка не существует
from sqlalchemy import select
existing = await session.execute(
select(UserSubscription).where(
UserSubscription.user_id == user_id,
UserSubscription.short_uuid == short_uuid
)
)
existing_sub = existing.scalar_one_or_none()
if existing_sub:
logger.warning(f"Subscription with short_uuid {short_uuid} already exists for user {user_id}")
return existing_sub
# Создаем новую подписку
new_subscription = UserSubscription(
user_id=user_id,
subscription_id=subscription_id,
short_uuid=short_uuid,
expires_at=expires_at
expires_at=expires_at,
is_active=is_active
)
session.add(user_sub)
session.add(new_subscription)
await session.commit()
await session.refresh(user_sub)
return user_sub
await session.refresh(new_subscription)
return new_subscription
except Exception as e:
logger.error(f"Error creating user subscription: {e}")
await session.rollback()
raise
return None
# Payment methods
# Payment methods
async def create_payment(self, user_id: int, amount: float, payment_type: str,
description: str, status: str = 'pending') -> Payment:
async with self.session_factory() as session:
@@ -467,17 +523,55 @@ class Database:
except Exception as e:
logger.error(f"Error getting trial subscriptions: {e}")
return []
async def update_user_subscription(self, user_sub: UserSubscription) -> UserSubscription:
async def get_user_subscription_by_short_uuid(self, user_id: int, short_uuid: str) -> Optional[UserSubscription]:
"""Get user subscription by short_uuid"""
async with self.session_factory() as session:
try:
await session.merge(user_sub)
await session.commit()
return user_sub
from sqlalchemy import select
result = await session.execute(
select(UserSubscription).where(
UserSubscription.user_id == user_id,
UserSubscription.short_uuid == short_uuid
)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error updating user subscription {user_sub.id}: {e}")
logger.error(f"Error getting user subscription by short_uuid: {e}")
return None
async def update_user_subscription(self, user_subscription: UserSubscription) -> bool:
"""Update user subscription"""
async with self.session_factory() as session:
try:
# Устанавливаем время обновления
user_subscription.updated_at = datetime.utcnow()
# Обновляем подписку
await session.merge(user_subscription)
await session.commit()
return True
except Exception as e:
logger.error(f"Error updating user subscription: {e}")
await session.rollback()
raise
return False
async def migrate_user_subscriptions(self):
"""Migrate user_subscriptions table to add missing columns"""
try:
async with self.engine.begin() as conn:
# Проверяем существование столбцов и добавляем их если нет
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN IF NOT EXISTS traffic_limit_gb INTEGER,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP
"""))
logger.info("Successfully migrated user_subscriptions table")
except Exception as e:
logger.info(f"Migration may have already been applied or error occurred: {e}")
except Exception as e:
logger.error(f"Error during migration: {e}")
async def get_expiring_subscriptions(self, user_id: int, days_threshold: int = 3) -> List[UserSubscription]:
async with self.session_factory() as session:
@@ -528,3 +622,114 @@ class Database:
logger.error(f"Error marking trial used for user {user_id}: {e}")
await session.rollback()
return False
async def get_all_payments_paginated(self, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
"""Get all payments with pagination"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
# Получаем общее количество записей
count_result = await session.execute(
select(func.count(Payment.id))
)
total_count = count_result.scalar()
# Получаем платежи с пагинацией
result = await session.execute(
select(Payment)
.order_by(desc(Payment.created_at))
.offset(offset)
.limit(limit)
)
payments = list(result.scalars().all())
return payments, total_count
except Exception as e:
logger.error(f"Error getting paginated payments: {e}")
return [], 0
async def get_payments_by_type_paginated(self, payment_type: str, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
"""Get payments by type with pagination"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
# Получаем общее количество записей
count_result = await session.execute(
select(func.count(Payment.id)).where(Payment.payment_type == payment_type)
)
total_count = count_result.scalar()
# Получаем платежи с пагинацией
result = await session.execute(
select(Payment)
.where(Payment.payment_type == payment_type)
.order_by(desc(Payment.created_at))
.offset(offset)
.limit(limit)
)
payments = list(result.scalars().all())
return payments, total_count
except Exception as e:
logger.error(f"Error getting paginated payments by type: {e}")
return [], 0
async def get_payments_by_status_paginated(self, status: str, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
"""Get payments by status with pagination"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
# Получаем общее количество записей
count_result = await session.execute(
select(func.count(Payment.id)).where(Payment.status == status)
)
total_count = count_result.scalar()
# Получаем платежи с пагинацией
result = await session.execute(
select(Payment)
.where(Payment.status == status)
.order_by(desc(Payment.created_at))
.offset(offset)
.limit(limit)
)
payments = list(result.scalars().all())
return payments, total_count
except Exception as e:
logger.error(f"Error getting paginated payments by status: {e}")
return [], 0
async def get_user_subscriptions_by_plan_id(self, plan_id: int) -> List[UserSubscription]:
"""Get all user subscriptions for a specific plan"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(UserSubscription).where(UserSubscription.subscription_id == plan_id)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting user subscriptions for plan {plan_id}: {e}")
return []
async def delete_user_subscription(self, user_subscription_id: int) -> bool:
"""Delete user subscription by ID"""
async with self.session_factory() as session:
try:
from sqlalchemy import delete
result = await session.execute(
delete(UserSubscription).where(UserSubscription.id == user_subscription_id)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error deleting user subscription {user_subscription_id}: {e}")
await session.rollback()
return False
+101
View File
@@ -1 +1,102 @@
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
ports:
- "5432:5432"
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
build:
context: .
dockerfile: Dockerfile
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
# Override database URL to use PostgreSQL container
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Redis (optional, for caching and session storage)
redis:
image: redis:7-alpine
container_name: remnawave_bot_redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass redis_password_123
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- bot_network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
# Nginx (optional, for serving static files or reverse proxy)
nginx:
image: nginx:alpine
container_name: remnawave_bot_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- ./static:/usr/share/nginx/html:ro
networks:
- bot_network
depends_on:
- bot
profiles:
- with-nginx
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
+24 -3
View File
@@ -22,28 +22,49 @@ class BotStates(StatesGroup):
waiting_language = State()
waiting_amount = State()
waiting_promocode = State()
waiting_topup_amount = State()
# Admin states
# Admin subscription management
admin_create_sub_name = State()
admin_create_sub_desc = State()
admin_create_sub_price = State()
admin_create_sub_days = State()
admin_create_sub_traffic = State()
admin_create_sub_squad = State()
admin_create_sub_squad_select = State()
admin_edit_sub_value = State()
# Admin balance management
admin_add_balance_user = State()
admin_add_balance_amount = State()
admin_payment_history_page = State()
# Admin promocode management
admin_create_promo_code = State()
admin_create_promo_discount = State()
admin_create_promo_limit = State()
admin_edit_sub_value = State()
# Admin messaging
admin_send_message_user = State()
admin_send_message_text = State()
admin_broadcast_text = State()
admin_create_sub_squad_select = State()
# Admin user management
admin_search_user_uuid = State()
admin_search_user_any = State()
admin_edit_user_expiry = State()
admin_edit_user_traffic = State()
# Admin monitoring
admin_test_monitor_user = State()
admin_sync_single_user = State()
admin_debug_user_structure = State()
admin_rename_plans_confirm = State()
router = Router()
# Start command
+157 -23
View File
@@ -1,6 +1,6 @@
from database import Subscription
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from typing import List, Optional
from typing import List, Optional, Dict
from translations import t
def language_keyboard() -> InlineKeyboardMarkup:
@@ -177,9 +177,14 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
InlineKeyboardButton(text="💰 " + t('manage_balance', lang), callback_data="admin_balance"),
InlineKeyboardButton(text="🎁 " + t('manage_promocodes', lang), callback_data="admin_promocodes")
],
# Третий ряд - коммуникации и аналитика
# Третий ряд - коммуникации и система
[
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system") # НОВОЕ!
],
# Четвертый ряд - мониторинг и статистика
[
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor"),
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
],
# Назад
@@ -347,27 +352,156 @@ def admin_monitor_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
])
return keyboard
def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin menu keyboard"""
def admin_system_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin system management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
# Первый ряд - управление контентом
[
InlineKeyboardButton(text="📦 " + t('manage_subscriptions', lang), callback_data="admin_subscriptions"),
InlineKeyboardButton(text="👥 " + t('manage_users', lang), callback_data="admin_users")
],
# Второй ряд - финансы
[
InlineKeyboardButton(text="💰 " + t('manage_balance', lang), callback_data="admin_balance"),
InlineKeyboardButton(text="🎁 " + t('manage_promocodes', lang), callback_data="admin_promocodes")
],
# Третий ряд - коммуникации и аналитика
[
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
],
# Четвертый ряд - мониторинг (НОВОЕ!)
[InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor")],
# Назад
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
[InlineKeyboardButton(text="📊 Системная статистика", callback_data="system_stats")],
[InlineKeyboardButton(text="🖥 Управление нодами", callback_data="nodes_management")],
[InlineKeyboardButton(text="👥 Пользователи системы", callback_data="system_users")],
[InlineKeyboardButton(text="🔄 Синхронизация с RemnaWave", callback_data="sync_remnawave")],
[InlineKeyboardButton(text="🔍 Отладка API", callback_data="debug_api_comprehensive")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def system_stats_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""System statistics keyboard with refresh"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить статистику", callback_data="refresh_system_stats")],
[InlineKeyboardButton(text="🖥 Ноды", callback_data="nodes_management")],
[InlineKeyboardButton(text="👥 Системные пользователи", callback_data="system_users")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")]
])
return keyboard
def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: int = None) -> InlineKeyboardMarkup:
"""Improved nodes management keyboard"""
buttons = []
if nodes:
# Statistics row
online_count = len([n for n in nodes if n.get('status') == 'online'])
total_count = len(nodes)
buttons.append([
InlineKeyboardButton(
text=f"📊 Ноды: {online_count}/{total_count} онлайн",
callback_data="noop"
)
])
# Show first 5 nodes with improved display
for i, node in enumerate(nodes[:5]):
status = node.get('status', 'unknown')
# Status emoji based on actual status
if status == 'online':
status_emoji = "🟢"
elif status == 'disabled':
status_emoji = ""
elif status == 'disconnected':
status_emoji = "🔴"
elif status == 'xray_stopped':
status_emoji = "🟡"
else:
status_emoji = ""
node_name = node.get('name', f'Node-{i+1}')
node_id = node.get('id', node.get('uuid'))
# Truncate long names
if len(node_name) > 20:
display_name = node_name[:17] + "..."
else:
display_name = node_name
# CPU/Memory usage if available
usage_info = ""
if node.get('cpuUsage'):
usage_info += f" CPU:{node['cpuUsage']:.0f}%"
if node.get('memUsage'):
usage_info += f" MEM:{node['memUsage']:.0f}%"
buttons.append([
InlineKeyboardButton(
text=f"{status_emoji} {display_name}{usage_info}",
callback_data=f"node_details_{node_id}"
),
InlineKeyboardButton(
text="🔄",
callback_data=f"restart_node_{node_id}"
),
InlineKeyboardButton(
text="⚙️",
callback_data=f"node_settings_{node_id}"
)
])
if len(nodes) > 5:
buttons.append([
InlineKeyboardButton(
text=f"... и еще {len(nodes) - 5} нод",
callback_data="show_all_nodes"
)
])
else:
buttons.append([
InlineKeyboardButton(
text="❌ Ноды не найдены",
callback_data="noop"
)
])
# Action buttons
buttons.append([
InlineKeyboardButton(text="🔄 Перезагрузить все", callback_data="restart_all_nodes"),
InlineKeyboardButton(text="📊 Статистика", callback_data="nodes_statistics")
])
# Refresh button
refresh_callback = f"refresh_nodes_stats_{timestamp}" if timestamp else "refresh_nodes_stats"
buttons.append([
InlineKeyboardButton(text="🔄 Обновить", callback_data=refresh_callback)
])
# Back button
buttons.append([
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def system_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""System users management keyboard - ИСПРАВЛЕНО"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="users_statistics")],
[InlineKeyboardButton(text="👥 Список всех пользователей", callback_data="list_all_system_users")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user_uuid")],
[InlineKeyboardButton(text="🔍 Отладка API пользователей", callback_data="debug_users_api")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_system")]
])
return keyboard
def bulk_operations_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Bulk operations keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="bulk_reset_traffic")],
[InlineKeyboardButton(text="❌ Отключить пользователей", callback_data="bulk_disable_users")],
[InlineKeyboardButton(text="✅ Включить пользователей", callback_data="bulk_enable_users")],
[InlineKeyboardButton(text="🗑 Удалить пользователей", callback_data="bulk_delete_users")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="system_users")]
])
return keyboard
def confirm_restart_keyboard(node_id: str = None, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Confirmation keyboard for node restart"""
action = f"confirm_restart_node_{node_id}" if node_id else "confirm_restart_all_nodes"
back_action = f"node_details_{node_id}" if node_id else "nodes_management"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, перезагрузить", callback_data=action),
InlineKeyboardButton(text="❌ Отмена", callback_data=back_action)
]
])
return keyboard
+850 -228
View File
File diff suppressed because it is too large Load Diff
+38 -8
View File
@@ -1,8 +1,38 @@
aiogram==3.4.1
aiohttp==3.9.3
asyncpg==0.29.0
sqlalchemy[asyncio]==2.0.25
alembic==1.13.1
python-dotenv==1.0.1
aiosqlite==0.19.0
psycopg2-binary==2.9.9
# Telegram Bot Framework
aiogram>=3.4.0
# Database
SQLAlchemy>=2.0.0
alembic>=1.12.0
# PostgreSQL driver
asyncpg>=0.28.0
psycopg2-binary>=2.9.0
# SQLite driver (fallback)
aiosqlite>=0.19.0
# HTTP Client
aiohttp>=3.8.0
aiofiles>=23.0.0
# Redis (optional)
redis>=4.5.0
aioredis>=2.0.0
# Utilities
python-dotenv>=1.0.0
pydantic>=2.0.0
pydantic-settings>=2.0.0
# Logging and monitoring
structlog>=23.0.0
# Date and time
python-dateutil>=2.8.0
# Cryptography
cryptography>=42.0.0
# JSON handling
orjson>=3.9.0
+2 -2
View File
@@ -200,7 +200,7 @@ User=$CURRENT_USER
WorkingDirectory=$CURRENT_DIR
Environment=PATH=$CURRENT_DIR/$VENV_DIR/bin
EnvironmentFile=$CURRENT_DIR/.env
ExecStart=$CURRENT_DIR/$VENV_DIR/bin/python $BOT_FILE
ExecStart=$CURRENT_DIR/$VENV_DIR/bin/python3 $BOT_FILE
Restart=always
RestartSec=10
@@ -215,4 +215,4 @@ fi
msg completed
msg start_prompt
python "$BOT_FILE"
python3 "$BOT_FILE"
+25 -2
View File
@@ -16,7 +16,9 @@ TRANSLATIONS = {
'trial_not_available': '❌ Тестовая подписка недоступна',
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе "Мои подписки".',
'trial_error': '❌ Ошибка при создании тестовой подписки',
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nТариф действует 3 дня!\n\nОграничение трафика - 2гб!',
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
'subscriptions_list': '📋 Список подписок в продаже:',
# Balance menu
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
@@ -25,6 +27,17 @@ TRANSLATIONS = {
'topup_card': 'Пополнение картой',
'topup_support': 'Через саппорт',
'back': 'Назад',
'system_management': 'Управление системой',
'nodes_management': 'Управление нодами',
'system_users': 'Системные пользователи',
'system_statistics': 'Системная статистика',
'restart_nodes': 'Перезагрузить ноды',
'bulk_operations': 'Массовые операции',
'search_user': 'Поиск пользователя',
'user_details': 'Детали пользователя',
'reset_traffic': 'Сбросить трафик',
'disable_user': 'Отключить пользователя',
'enable_user': 'Включить пользователя',
'send_message': 'Отправить сообщение',
'send_to_user': 'Отправить пользователю',
@@ -145,7 +158,17 @@ TRANSLATIONS = {
'enter_user_id_message': 'Enter user id message',
'enter_message_text': 'Enter message text',
'trial_subscription': 'Trial subscription',
'system_management': 'System Management',
'nodes_management': 'Nodes Management',
'system_users': 'System Users',
'system_statistics': 'System Statistics',
'restart_nodes': 'Restart Nodes',
'bulk_operations': 'Bulk Operations',
'search_user': 'Search User',
'user_details': 'User Details',
'reset_traffic': 'Reset Traffic',
'disable_user': 'Disable User',
'enable_user': 'Enable User',
# Balance menu
'your_balance': '💰 Your balance: ${balance:.2f}',
+10 -3
View File
@@ -209,9 +209,16 @@ def get_subscription_connection_url(base_url: str, short_uuid: str) -> str:
"""Generate subscription connection URL"""
return f"{base_url.rstrip('/')}/api/sub/{short_uuid}"
def log_user_action(user_id: int, action: str, details: str = ""):
"""Log user action"""
logger.info(f"User {user_id} - {action}: {details}")
def log_user_action(telegram_id: int, action: str, details: str = None):
"""Log user action for audit"""
import logging
logger = logging.getLogger(__name__)
log_message = f"Admin action by {telegram_id}: {action}"
if details:
log_message += f" - {details}"
logger.info(log_message)
def format_subscription_status(expires_at: datetime, lang: str = 'ru') -> str:
"""Format subscription status with emoji"""