Compare commits

...

54 Commits

Author SHA1 Message Date
Egor 4e3a183e2b Update Dockerfile 2025-09-13 11:07:33 +03:00
Egor 0879855d3f Update docker-registry.yml 2025-09-13 11:07:20 +03:00
Egor 2358552961 Update docker-hub.yml 2025-09-13 11:07:04 +03:00
Egor 35b461b60c Update README.md 2025-09-13 11:06:28 +03:00
Egor 1b5cfc3920 Update README.md 2025-09-13 10:23:10 +03:00
Egor 4a179ccc1e Update README.md 2025-09-13 10:22:18 +03:00
Egor 4921578af3 Update .env.example 2025-09-13 10:21:31 +03:00
Egor 55fb1f2044 Update CONTRIBUTING.md 2025-09-13 10:19:21 +03:00
Egor ad38aae7b0 Update issue templates 2025-09-13 10:16:27 +03:00
Egor 7e0840ef09 Create SECURITY.md 2025-09-13 10:15:33 +03:00
Egor 03fcb5f2f7 Create CONTRIBUTING.md 2025-09-13 10:12:53 +03:00
Egor 0c4ea86e03 Update users.py 2025-09-13 09:29:26 +03:00
Egor 209bb9763e Merge pull request #66 from Fr1ngg/dev
Исправлена ошибок с устаревшими callback queries при перезапуске бота
2025-09-13 09:27:31 +03:00
Egor 187de6807e Update decorators.py 2025-09-13 09:22:56 +03:00
Egor b8c9302018 Add files via upload 2025-09-13 09:21:32 +03:00
Egor a86fccffc0 Update bot.py 2025-09-13 09:19:55 +03:00
Egor a448adec93 Create global_error.py 2025-09-13 09:18:55 +03:00
Egor d45ff62a57 Update decorators.py 2025-09-13 09:07:22 +03:00
Egor 3c50fc6a71 Update README.md 2025-09-13 08:02:29 +03:00
Egor 292a120f74 Merge pull request #65 from Fr1ngg/dev
X-Api-key заголовки + TinyAuth
2025-09-13 07:55:55 +03:00
Egor a7868787a4 Update remnawave_service.py 2025-09-13 07:53:39 +03:00
Egor 5275f4fbeb Update maintenance_service.py 2025-09-13 07:52:28 +03:00
Egor ba25ec7ad0 Update subscription_service.py 2025-09-13 07:44:16 +03:00
Egor 301b4124ef Update .env.example 2025-09-13 07:36:17 +03:00
Egor a936a97e5e Update remnawave_service.py 2025-09-13 07:34:44 +03:00
Egor c390d18399 Update config.py 2025-09-13 07:34:16 +03:00
Egor 43df9bff48 Update remnawave_api.py 2025-09-13 07:33:29 +03:00
Egor c15a9ea7cd Update README.md 2025-09-13 06:57:19 +03:00
Egor 271742b306 Update main.py 2025-09-13 06:47:50 +03:00
Egor ebdadd81c0 Update Dockerfile 2025-09-13 05:50:25 +03:00
Egor 1364855303 Update docker-registry.yml 2025-09-13 05:50:14 +03:00
Egor b5d862b231 Update docker-hub.yml 2025-09-13 05:49:57 +03:00
Egor 048a8adaa1 Update README.md 2025-09-13 05:49:23 +03:00
Egor 4b7b999d43 Правка статистики серверов 2025-09-13 05:47:57 +03:00
Egor d02fa83af5 Merge pull request #64 from Fr1ngg/dev
Исправление синка счетчика юзеров на серверах в админ панели в разделе с подписками юзеров
2025-09-13 05:43:13 +03:00
Egor 89045095b1 Исправление синка счетчика юзеров на серверах в админ панели в разделе с подписками юзеров 2025-09-13 05:42:27 +03:00
Egor 0de721e67d Merge pull request #63 from Fr1ngg/dev
Изменение отображения юзеров в админке
2025-09-13 05:16:03 +03:00
Egor c7c171a1d3 Update users.py 2025-09-13 05:15:04 +03:00
Egor a2500148fd Merge pull request #62 from Fr1ngg/dev
Возможность скрыть ссылку на подписку в разделе с подпиской
2025-09-13 05:03:47 +03:00
Egor 99b8a3726b Update README.md 2025-09-13 05:02:44 +03:00
Egor 8b426e0f6c Update .env.example 2025-09-13 05:01:37 +03:00
Egor caca561997 Update subscription.py 2025-09-13 04:58:38 +03:00
Egor 385bd78021 Update .env.example 2025-09-13 04:57:33 +03:00
Egor 8ca127e93f Update config.py 2025-09-13 04:57:00 +03:00
Egor ac9b8aaf68 Merge pull request #61 from Fr1ngg/dev
Правки в админском меню
2025-09-13 04:49:29 +03:00
Egor 4939f56f1b Update states.py 2025-09-13 04:47:50 +03:00
Egor 659e950722 Update states.py 2025-09-13 04:44:30 +03:00
Egor 5186ae4334 Update main.py 2025-09-13 04:42:42 +03:00
Egor 64cd38e2c3 Update admin.py 2025-09-13 04:41:46 +03:00
Egor cc29c887c5 Merge pull request #60 from Legacyyy777/DEV
Первый раз в первый класс
2025-09-13 04:28:48 +03:00
Egor 7fd7dd7cc8 Delete .github/workflows/deploy.yaml 2025-09-13 04:28:09 +03:00
Legacyyy777 9ac3088b72 Добавлено отображение текущего количества онлайн-пользователей VPN в админ-панели. Обработаны возможные ошибки при получении статистики. 2025-09-13 04:17:29 +05:00
Legacyyy777 9745885f1b Create deploy.yaml 2025-09-13 03:37:04 +05:00
Egor 2ccaaebfc9 Update Dockerfile 2025-09-13 00:45:49 +03:00
26 changed files with 1711 additions and 181 deletions
+15 -4
View File
@@ -34,9 +34,17 @@ REDIS_URL=redis://redis:6379/0
# ===== REMNAWAVE API =====
REMNAWAVE_API_URL=https://panel.example.com
REMNAWAVE_API_KEY=
# Для панелей установленных скриптом eGames прописывать ключ в формате XXXXXXX:DDDDDDDD - https://panel.example.com/auth/login?XXXXXXX=DDDDDDDD
REMNAWAVE_SECRET_KEY=your_secret_key_here
REMNAWAVE_API_KEY=your_api_key_here
# Тип авторизации: "api_key", "basic_auth"
REMNAWAVE_AUTH_TYPE=api_key
# Для панелей с Basic Auth (опционально)
REMNAWAVE_USERNAME=
REMNAWAVE_PASSWORD=
# Для панелей установленных скриптом eGames прописывать ключ в формате XXXXXXX:DDDDDDDD
REMNAWAVE_SECRET_KEY=
# ========= ПОДПИСКИ =========
# ===== ТРИАЛ ПОДПИСКА =====
@@ -183,7 +191,7 @@ CRYPTOBOT_WEBHOOK_SECRET=your_webhook_secret_here
CRYPTOBOT_BASE_URL=https://pay.crypt.bot
CRYPTOBOT_TESTNET=false
CRYPTOBOT_WEBHOOK_PATH=/cryptobot-webhook
CRYPTOBOT_WEBHOOK_PORT=8083
CRYPTOBOT_WEBHOOK_PORT=8081
CRYPTOBOT_DEFAULT_ASSET=USDT
CRYPTOBOT_ASSETS=USDT,TON,BTC,ETH,LTC,BNB,TRX,USDC
CRYPTOBOT_INVOICE_EXPIRES_HOURS=24
@@ -194,6 +202,9 @@ CRYPTOBOT_INVOICE_EXPIRES_HOURS=24
ENABLE_LOGO_MODE=true
LOGO_FILE=vpn_logo.png
# Скрыть блок с ссылкой подключения в разделе с информацией о подписке
HIDE_SUBSCRIPTION_LINK=false
# Режим работы кнопки "Подключиться"
# guide - открывает гайд подключения (режим 1)
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+10
View File
@@ -0,0 +1,10 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+3 -3
View File
@@ -36,15 +36,15 @@ jobs:
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🏷️ Собираем релизную версию: $VERSION"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
VERSION="v2.2.9-$(git rev-parse --short HEAD)"
VERSION="v2.3.1-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🚀 Собираем версию из main: $VERSION"
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
VERSION="v2.2.9-dev-$(git rev-parse --short HEAD)"
VERSION="v2.3.1-dev-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🧪 Собираем dev версию: $VERSION"
else
VERSION="v2.2.9-pr-$(git rev-parse --short HEAD)"
VERSION="v2.3.1-pr-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)"
echo "🔀 Собираем PR версию: $VERSION"
fi
+3 -3
View File
@@ -52,13 +52,13 @@ jobs:
VERSION=${GITHUB_REF#refs/tags/}
echo "🏷️ Собираем релизную версию: $VERSION"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
VERSION="v2.2.9"
VERSION="v2.3.1"
echo "🚀 Собираем версию из main: $VERSION"
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
VERSION="v2.2.9-dev-$(git rev-parse --short HEAD)"
VERSION="v2.3.1-dev-$(git rev-parse --short HEAD)"
echo "🧪 Собираем dev версию: $VERSION"
else
VERSION="v2.2.9-pr-$(git rev-parse --short HEAD)"
VERSION="v2.3.1-pr-$(git rev-parse --short HEAD)"
echo "🔀 Собираем PR версию: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
+480
View File
@@ -0,0 +1,480 @@
# 🤝 Contributing to Remnawave Bedolaga Bot
Спасибо за интерес к развитию проекта! Этот документ содержит правила и рекомендации для контрибьюторов.
## 📋 Содержание
- [Кодекс поведения](#-кодекс-поведения)
- [Как помочь проекту](#-как-помочь-проекту)
- [Настройка среды разработки](#-настройка-среды-разработки)
- [Стандарты кода](#-стандарты-кода)
- [Работа с Git](#-работа-с-git)
- [Тестирование](#-тестирование)
- [Процесс ревью](#-процесс-ревью)
- [Документация](#-документация)
## 📜 Кодекс поведения
### Основные принципы
- **Уважение** - относитесь к участникам проекта с уважением
- **Конструктивность** - предлагайте решения, а не только критику
- **Открытость** - будьте открыты к обратной связи и новым идеям
- **Профессионализм** - поддерживайте высокий уровень обсуждений
### Недопустимое поведение
- Оскорбления и личные атаки
- Спам и офф-топик сообщения
- Публикация приватной информации
- Любые формы дискриминации
## 🚀 Как помочь проекту
### 🐛 Сообщения о багах
Перед созданием issue проверьте:
- [ ] Аналогичная проблема не была описана ранее
- [ ] Вы используете актуальную версию бота
- [ ] Проблема воспроизводится стабильно
**Шаблон для багрепорта:**
```markdown
## 🐛 Описание бага
Краткое описание проблемы
## 🔄 Шаги воспроизведения
1. Перейти к '...'
2. Нажать на '...'
3. Увидеть ошибку
## ✅ Ожидаемое поведение
Что должно было произойти
## ❌ Фактическое поведение
Что произошло на самом деле
## 🌍 Окружение
- Версия бота: [например, 1.0.0]
- Python версия: [например, 3.11.7]
- ОС: [например, Ubuntu 22.04]
- Версия Docker: [если используется]
## 📋 Логи
```
Вставьте соответствующие логи
```
## 📷 Скриншоты
Если применимо, добавьте скриншоты
```
### 💡 Предложения функций
Используйте лейбл `enhancement` и опишите:
- **Проблему**, которую решает функция
- **Предлагаемое решение**
- **Альтернативы**, которые вы рассматривали
- **Дополнительную информацию** и контекст
### 📝 Улучшение документации
- Исправление опечаток
- Дополнение примеров
- Перевод на другие языки
- Улучшение структуры
## 🛠 Настройка среды разработки
### Требования
- Python 3.11+
- Docker и Docker Compose
- Git
- PostgreSQL 15+ (опционально для локальной разработки)
- Redis (опционально для локальной разработки)
### Установка
1. **Форкните и клонируйте репозиторий:**
```bash
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot.git
cd remnawave-bedolaga-telegram-bot
```
2. **Создайте виртуальное окружение:**
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# или
venv\Scripts\activate # Windows
```
3. **Установите зависимости:**
```bash
pip install -r requirements.txt
pip install -r requirements-dev.txt # если есть dev зависимости
```
4. **Настройте окружение:**
```bash
cp .env.example .env
# Отредактируйте .env файл с вашими настройками
```
5. **Запустите через Docker (рекомендуется):**
```bash
docker compose up -d postgres redis
python main.py
```
### Структура проекта
```
bedolaga_bot/
├── app/ # Основной код приложения
│ ├── handlers/ # Обработчики сообщений
│ ├── services/ # Бизнес-логика
│ ├── database/ # Модели и CRUD операции
│ ├── utils/ # Утилиты
│ ├── middlewares/ # Middleware
│ └── external/ # Внешние API
├── migrations/ # Миграции БД (если нужны)
├── tests/ # Тесты (создать при необходимости)
├── docs/ # Документация
└── requirements.txt # Зависимости
```
## 🎨 Стандарты кода
### Python стиль
Мы следуем **PEP 8** с некоторыми исключениями:
```python
# ✅ Хорошо
async def get_user_subscription(user_id: int) -> Optional[Subscription]:
"""Получает активную подписку пользователя."""
async with get_session() as session:
result = await session.execute(
select(Subscription)
.where(Subscription.user_id == user_id)
.where(Subscription.is_active == True)
)
return result.scalar_one_or_none()
# ❌ Плохо
async def getUserSub(uid):
session = get_session()
sub = session.query(Subscription).filter(Subscription.user_id==uid,Subscription.is_active==True).first()
return sub
```
### Правила именования
- **Функции и переменные**: `snake_case`
- **Классы**: `PascalCase`
- **Константы**: `UPPER_CASE`
- **Приватные методы**: `_leading_underscore`
### Типизация
Обязательно используйте type hints:
```python
from typing import Optional, List, Dict, Any
async def create_subscription(
user_id: int,
duration_days: int,
traffic_limit_gb: Optional[int] = None
) -> Subscription:
"""Создает новую подписку."""
# implementation
```
### Документация кода
```python
async def calculate_subscription_price(
period_days: int,
traffic_gb: int,
devices_count: int,
servers_count: int
) -> int:
"""
Рассчитывает стоимость подписки.
Args:
period_days: Период подписки в днях
traffic_gb: Лимит трафика в ГБ (0 = безлимит)
devices_count: Количество устройств
servers_count: Количество серверов
Returns:
Стоимость в копейках
Raises:
ValueError: Если переданы некорректные параметры
"""
# implementation
```
### Обработка ошибок
```python
# ✅ Хорошо
try:
subscription = await subscription_service.create_subscription(user_id, data)
await message.answer("✅ Подписка создана успешно!")
except RemnaWaveAPIError as e:
logger.error(f"RemnaWave API error: {e}")
await message.answer("❌ Ошибка при создании подписки. Попробуйте позже.")
except ValidationError as e:
logger.warning(f"Validation error: {e}")
await message.answer("❌ Некорректные данные для создания подписки.")
# ❌ Плохо
try:
subscription = await subscription_service.create_subscription(user_id, data)
await message.answer("✅ Подписка создана успешно!")
except:
await message.answer("Ошибка")
```
### Логирование
```python
import logging
logger = logging.getLogger(__name__)
# Уровни логирования
logger.debug("Детальная информация для отладки")
logger.info("Общая информация о работе")
logger.warning("Предупреждение о потенциальной проблеме")
logger.error("Ошибка, которая не прерывает работу")
logger.critical("Критическая ошибка")
```
## 🔄 Работа с Git
### Ветки
- `main` - стабильная версия
- `dev` - разработка новых функций
### Коммиты
Используйте [Conventional Commits](https://www.conventionalcommits.org/):
```bash
# Типы коммитов
feat: добавить поддержку CryptoBot платежей
fix: исправить ошибку с расчетом цены подписки
docs: обновить документацию по настройке
style: исправить форматирование кода
refactor: рефакторинг сервиса платежей
test: добавить тесты для subscription_service
chore: обновить зависимости
# Примеры
git commit -m "feat(payments): добавить поддержку YooKassa webhook"
git commit -m "fix(subscription): исправить расчет стоимости устройств"
git commit -m "docs(readme): обновить инструкцию по установке"
```
### Pull Request процесс
1. **Создайте ветку** от `dev`:
```bash
git checkout develop
git pull origin develop
git checkout -b feature/new-payment-method
```
2. **Разработайте функцию** с соблюдением стандартов
3. **Протестируйте** изменения локально
4. **Создайте Pull Request** с описанием:
```markdown
## 📝 Описание
Краткое описание изменений
## 🎯 Мотивация
Почему эти изменения нужны?
## 🔧 Тип изменений
- [ ] Bug fix (исправление)
- [ ] New feature (новая функция)
- [ ] Breaking change (ломающие изменения)
- [ ] Documentation update (обновление документации)
## ✅ Чеклист
- [ ] Код соответствует стандартам проекта
- [ ] Добавлены/обновлены тесты
- [ ] Документация обновлена
- [ ] Проверена работа в Docker
- [ ] Проверена совместимость с существующим API
## 🧪 Тестирование
Как тестировались изменения:
- [ ] Локальное тестирование
- [ ] Тестирование с реальным Remnawave API
- [ ] Тестирование платежных систем
```
## 🧪 Тестирование
### Локальное тестирование
```bash
# Запуск с тестовой базой
export DATABASE_URL="sqlite:///test.db"
export BOT_TOKEN="test_token"
python main.py
```
### Тестирование компонентов
```python
# tests/test_subscription_service.py
import pytest
from app.services.subscription_service import SubscriptionService
@pytest.mark.asyncio
async def test_calculate_price():
price = await SubscriptionService.calculate_subscription_price(
period_days=30,
traffic_gb=100,
devices_count=3,
servers_count=1
)
assert price > 0
assert isinstance(price, int)
```
### Integration тесты
Тестируйте интеграцию с:
- Remnawave API (с тестовыми данными)
- Базой данных
- Платежными системами (sandbox режим)
## 👀 Процесс ревью
### Требования для ревьюера
- Проверить соответствие стандартам кода
- Убедиться в работоспособности
- Проверить безопасность (особенно для платежных функций)
- Оценить производительность
- Проверить совместимость с существующим API
### Требования для автора
- Ответить на все комментарии
- Исправить замечания
- Обновить документацию при необходимости
- Убедиться в прохождении всех проверок
## 📚 Документация
### Обновление документации
При добавлении новых функций обновляйте:
- `README.md` - если изменился API или конфигурация
- Комментарии в коде
- Примеры использования
- Changelog (если есть)
### Стиль документации
- Используйте ясный и понятный язык
- Добавляйте примеры кода
- Указывайте возможные ошибки и их решения
- Используйте эмодзи для улучшения читаемости
## 🏷 Лейблы Issues и PR
### Приоритет
- `priority:high` - высокий приоритет
- `priority:medium` - средний приоритет
- `priority:low` - низкий приоритет
### Тип
- `bug` - ошибка
- `enhancement` - улучшение
- `feature` - новая функция
- `documentation` - документация
- `question` - вопрос
### Область
- `payments` - платежные системы
- `api` - Remnawave API
- `database` - база данных
- `ui/ux` - интерфейс пользователя
- `admin` - админ панель
## 🔐 Безопасность
### Сообщения о уязвимостях
Для сообщений о критических уязвимостях безопасности:
- Свяжитесь с [@fringg](https://t.me/fringg) напрямую
- Не создавайте публичные issues для уязвимостей
- Дайте время на исправление перед публичным раскрытием
### Рекомендации по безопасности
- Никогда не коммитьте API ключи и пароли
- Используйте переменные окружения для чувствительных данных
- Валидируйте все пользовательские данные
- Используйте HTTPS для всех внешних запросов
## 📞 Получение помощи
### Каналы связи
- **💬 Telegram Group:** [Bedolaga Chat](https://t.me/+wTdMtSWq8YdmZmVi) - общие вопросы
- **🐛 GitHub Issues:** Технические вопросы и баги
- **📧 Прямой контакт:** [@fringg](https://t.me/fringg) - только критические вопросы
### Часто задаваемые вопросы
**Q: Как настроить локальную разработку без Docker?**
A: Установите PostgreSQL и Redis локально, обновите DATABASE_URL и REDIS_URL в .env
**Q: Можно ли использовать SQLite для разработки?**
A: Да, установите DATABASE_MODE=sqlite в .env
**Q: Как тестировать платежные системы?**
A: Используйте тестовые/sandbox режимы платежных систем
**Q: Что делать, если тесты не проходят?**
A: Проверьте конфигурацию .env и убедитесь, что все сервисы запущены
## 📋 Чеклист для контрибьюторов
Перед отправкой PR убедитесь:
- [ ] ✅ Код соответствует стандартам проекта
- [ ] 📝 Добавлены комментарии и docstrings
- [ ] 🧪 Функциональность протестирована
- [ ] 📚 Документация обновлена
- [ ] 🔒 Нет чувствительных данных в коде
- [ ] 🐳 Изменения работают в Docker
- [ ] 📋 PR создан с подробным описанием
- [ ] 🏷 Проставлены соответствующие лейблы
## 🎉 Благодарности
Спасибо всем, кто вносит вклад в развитие проекта! Ваша помощь делает Bedolaga Bot лучше для всего сообщества.
---
**Made with ❤️ by community**
+2 -2
View File
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
FROM python:3.11-slim
ARG VERSION="v2.2.9"
ARG VERSION="v2.3.1"
ARG BUILD_DATE
ARG VCS_REF
@@ -45,7 +45,7 @@ ENV PYTHONPATH=/app \
BUILD_DATE=${BUILD_DATE} \
VCS_REF=${VCS_REF}
EXPOSE 8081 8082
EXPOSE 8081 8082 8083
LABEL org.opencontainers.image.title="Bedolaga RemnaWave Bot" \
org.opencontainers.image.description="Telegram bot for RemnaWave VPN service" \
+81 -10
View File
@@ -55,6 +55,17 @@
- 🛡️ **Защита панели** - поддержка [remnawave-reverse-proxy](https://github.com/eGamesAPI/remnawave-reverse-proxy)
- 🗄️ **Бекапы/Восстановление** - автобекапы и восстановление бд прямо в боте с уведомления в топики
### 📚 Поддерживаемые методы авторизации
Метод | Заголовок | Описание
-- | -- | --
API Key | X-Api-Key: your_api_key | Стандартный API ключ
Bearer Token | Authorization: Bearer token | Классический Bearer token
Basic Auth | X-Api-Key: Basic base64(user:pass) | Basic Authentication
eGames Cookies | Cookies в формате key:value | Для панелей eGames
---
## 🚀 Быстрый старт
@@ -246,9 +257,17 @@ REDIS_URL=redis://redis:6379/0
# ===== REMNAWAVE API =====
REMNAWAVE_API_URL=https://panel.example.com
REMNAWAVE_API_KEY=
# Для панелей установленных скриптом eGames прописывать ключ в формате XXXXXXX:DDDDDDDD - https://panel.example.com/auth/login?XXXXXXX=DDDDDDDD
REMNAWAVE_SECRET_KEY=your_secret_key_here
REMNAWAVE_API_KEY=your_api_key_here
# Тип авторизации: "api_key", "basic_auth"
REMNAWAVE_AUTH_TYPE=api_key
# Для панелей с Basic Auth (опционально)
REMNAWAVE_USERNAME=
REMNAWAVE_PASSWORD=
# Для панелей установленных скриптом eGames прописывать ключ в формате XXXXXXX:DDDDDDDD
REMNAWAVE_SECRET_KEY=
# ========= ПОДПИСКИ =========
# ===== ТРИАЛ ПОДПИСКА =====
@@ -395,23 +414,35 @@ CRYPTOBOT_WEBHOOK_SECRET=your_webhook_secret_here
CRYPTOBOT_BASE_URL=https://pay.crypt.bot
CRYPTOBOT_TESTNET=false
CRYPTOBOT_WEBHOOK_PATH=/cryptobot-webhook
CRYPTOBOT_WEBHOOK_PORT=8083
CRYPTOBOT_WEBHOOK_PORT=8081
CRYPTOBOT_DEFAULT_ASSET=USDT
CRYPTOBOT_ASSETS=USDT,TON,BTC,ETH,LTC,BNB,TRX,USDC
CRYPTOBOT_INVOICE_EXPIRES_HOURS=24
# ===== ИНТЕРФЕЙС И UX =====
# Включить логотип для всех сообщений (true - с изображением, false - только текст)
ENABLE_LOGO_MODE=true
LOGO_FILE=vpn_logo.png
# Скрыть блок с ссылкой подключения в разделе с информацией о подписке
HIDE_SUBSCRIPTION_LINK=false
# Режим работы кнопки "Подключиться"
# guide - открывает гайд подключения (режим 1)
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
# link - открывает ссылку подписки напрямую (режим 4)
# link - Открывает ссылку напрямую в браузере (режим 4)
CONNECT_BUTTON_MODE=guide
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
MINIAPP_CUSTOM_URL=
# Пропустить принятие правил использования бота
SKIP_RULES_ACCEPT=false
# Пропустить запрос реферального кода
SKIP_REFERRAL_CODE=false
# ===== МОНИТОРИНГ И УВЕДОМЛЕНИЯ =====
MONITORING_INTERVAL=60
INACTIVE_USER_DELETE_MONTHS=3
@@ -448,6 +479,12 @@ BACKUP_COMPRESSION=true
BACKUP_INCLUDE_LOGS=false
BACKUP_LOCATION=/app/data/backups
# Отправка бэкапов в телеграм
BACKUP_SEND_ENABLED=true
BACKUP_SEND_CHAT_ID=-100123456789 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА!
# ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
BACKUP_SEND_TOPIC_ID=123 # Опционально: ID топика
# ===== ПРОВЕРКА ОБНОВЛЕНИЙ БОТА =====
VERSION_CHECK_ENABLED=true
VERSION_CHECK_REPO=fr1ngg/remnawave-bedolaga-telegram-bot
@@ -459,8 +496,9 @@ LOG_FILE=logs/bot.log
# ===== РАЗРАБОТКА =====
DEBUG=false
WEBHOOK_URL=
WEBHOOK_URL= # Укажите домен вебхука
WEBHOOK_PATH=/webhook
```
</details>
@@ -585,7 +623,7 @@ services:
POSTGRES_DB: ${POSTGRES_DB:-remnawave_bot}
POSTGRES_USER: ${POSTGRES_USER:-remnawave_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password_123}
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
@@ -601,7 +639,7 @@ services:
image: redis:7-alpine
container_name: remnawave_bot_redis
restart: unless-stopped
command: redis-server --appendonly yes
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
@@ -624,22 +662,46 @@ services:
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-remnawave_user}:${POSTGRES_PASSWORD:-secure_password_123}@postgres:5432/${POSTGRES_DB:-remnawave_bot}
REDIS_URL: redis://redis:6379/0
DOCKER_ENV: "true"
DATABASE_MODE: "auto"
POSTGRES_HOST: "postgres"
POSTGRES_PORT: "5432"
POSTGRES_DB: "${POSTGRES_DB:-remnawave_bot}"
POSTGRES_USER: "${POSTGRES_USER:-remnawave_user}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-secure_password_123}"
REDIS_URL: "redis://redis:6379/0"
TZ: "Europe/Moscow"
volumes:
# Логи
- ./logs:/app/logs:rw
# Данные приложения (для SQLite в случае переключения)
- ./data:/app/data:rw
# Конфигурация приложения
# - ./app-config.json:/app/app-config.json:ro
# Timezone
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
# Логотип для сообщений
- ./vpn_logo.png:/app/vpn_logo.png:ro
ports:
- "${TRIBUTE_WEBHOOK_PORT:-8081}:8081"
- "${YOOKASSA_WEBHOOK_PORT:-8082}:8082"
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8081/health\", timeout=5)' || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
bot_network:
@@ -647,6 +709,7 @@ networks:
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
```
### 🚀 Команды управления
@@ -829,6 +892,7 @@ bedolaga_bot/
│ │ ├── ✅ subscription_utils.py # Проверка подписок
│ │ ├── 📄 pagination.py # Пагинация
│ │ ├── 📄 pricing_utils.py # Цены
│ │ ├── ‼️ global_error.py # Обработка ошибок
│ │ ├── 👤 user_utils.py # Утилиты для пользователей
│ │ ├── 🫰 currency_converter.py # Курсы для CryptoBota
│ │ └── ⚡ cache.py # Кеширование
@@ -1089,6 +1153,13 @@ REMNAWAVE_SECRET_KEY=XXXXXXX:DDDDDDDD
<td>За поддержку и доверие</td>
</tr>
<tr>
<td>5</td>
<td><strong>@Nav1_0</strong></td>
<td>₽1,000</td>
<td>За поддержку и доверие</td>
</tr>
<tr>
<td>6</td>
<td><strong>@kr33njee</strong></td>
+384
View File
@@ -0,0 +1,384 @@
# 🔐 Security Policy
## Supported Versions
Мы обеспечиваем поддержку безопасности для следующих версий:
| Version | Supported |
| ------- | ------------------ |
| 2.x.x | ✅ Yes |
| 1.x.x | ❌ No |
| < 1.0 | ❌ No |
## Reporting a Vulnerability
### 🚨 Критические уязвимости
Для сообщений о критических уязвимостях безопасности:
**НЕ создавайте публичные Issues или Pull Requests**
Вместо этого:
1. Свяжитесь с [@fringg](https://t.me/fringg) напрямую в Telegram
2. Укажите в сообщении "SECURITY VULNERABILITY" в начале
3. Опишите уязвимость детально
4. Дайте время на исправление (обычно 90 дней)
### 📧 Контактная информация
- **Telegram**: [@fringg](https://t.me/fringg)
- **Response Time**: 24-48 часов
- **Fix Timeline**: 7-30 дней (в зависимости от критичности)
### 🔍 Что считается уязвимостью безопасности
**Критические уязвимости:**
- SQL инъекции
- Remote Code Execution (RCE)
- Обход аутентификации
- Несанкционированный доступ к данным пользователей
- Утечка API ключей или токенов
- Bypass платежных проверок
**Средние уязвимости:**
- Cross-Site Scripting (XSS) в веб-компонентах
- Небезопасная десериализация
- Недостаточная валидация входных данных
- Rate limiting bypass
**Низкие уязвимости:**
- Information disclosure
- DoS атаки
- Проблемы с логированием чувствительных данных
### 📝 Информация для включения в отчет
1. **Описание уязвимости**
- Тип уязвимости
- Затронутые компоненты
- Потенциальное воздействие
2. **Шаги воспроизведения**
- Детальные инструкции
- Скриншоты или видео (если применимо)
- Тестовые данные
3. **Proof of Concept**
- Работающий эксплойт (если безопасно)
- Логи или вывод команд
4. **Предлагаемые исправления**
- Возможные решения
- Альтернативные подходы
## Security Measures
### 🛡️ Текущие меры защиты
**Аутентификация и авторизация:**
- Проверка админ ID через ADMIN_IDS
- Валидация всех пользовательских данных
- Защита от несанкционированного доступа к админ функциям
**Защита API:**
- Поддержка различных методов аутентификации Remnawave
- Безопасная передача куки для защищенных панелей
- Rate limiting для предотвращения злоупотреблений
- Timeout для внешних запросов
**Защита данных:**
- Хеширование чувствительной информации
- Безопасное хранение API ключей
- Логирование без записи чувствительных данных
- Регулярные бекапы с возможностью восстановления
**Платежная безопасность:**
- Валидация webhook подписей
- Проверка сумм платежей
- Защита от duplicate payments
- Безопасная обработка платежных данных
**Инфраструктурная безопасность:**
- Docker контейнеризация
- Изоляция сетевого трафика
- Минимальные права доступа
- Регулярные обновления зависимостей
### 🔒 Конфиденциальные данные
**Что НЕ логируется:**
- API ключи и токены
- Пароли и секретные ключи
- Персональные данные пользователей
- Детали платежных транзакций
- Webhook секреты
**Защита в коде:**
```python
# ✅ Правильно
logger.info(f"User {user_id} created subscription")
logger.debug(f"Payment webhook received for order {order_id}")
# ❌ Неправильно
logger.info(f"API key: {api_key}")
logger.debug(f"Webhook payload: {webhook_data}")
```
### 🌐 Сетевая безопасность
**HTTPS обязателен:**
- Все внешние API вызовы через HTTPS
- Webhook endpoints защищены TLS
- Проверка SSL сертификатов
**Firewall конфигурация:**
```bash
# Открыть только необходимые порты
ufw allow 80/tcp # HTTP (redirect to HTTPS)
ufw allow 443/tcp # HTTPS
ufw deny 8081/tcp # Webhook порты (только через reverse proxy)
ufw deny 8082/tcp
```
### 📊 Мониторинг безопасности
**Автоматические проверки:**
- Мониторинг неудачных попыток аутентификации
- Отслеживание подозрительной активности
- Проверка целостности платежей
- Мониторинг состояния внешних сервисов
**Алерты:**
- Множественные неудачные попытки входа
- Неожиданные изменения в конфигурации
- Критические ошибки в платежных системах
- Недоступность Remnawave API
## Best Practices for Developers
### 🔐 Безопасное программирование
**Input Validation:**
```python
# ✅ Правильно
def validate_user_id(user_id: str) -> int:
if not user_id.isdigit():
raise ValueError("Invalid user ID format")
uid = int(user_id)
if uid <= 0:
raise ValueError("User ID must be positive")
return uid
# ❌ Неправильно
def get_user(user_id):
return session.execute(f"SELECT * FROM users WHERE id = {user_id}")
```
**SQL Injection Prevention:**
```python
# ✅ Правильно - используйте SQLAlchemy ORM
async def get_user_subscriptions(user_id: int):
result = await session.execute(
select(Subscription).where(Subscription.user_id == user_id)
)
return result.scalars().all()
# ❌ Неправильно - raw SQL без параметров
async def get_user_subscriptions(user_id: int):
result = await session.execute(
f"SELECT * FROM subscriptions WHERE user_id = {user_id}"
)
```
**API Keys Management:**
```python
# ✅ Правильно
class Config:
REMNAWAVE_API_KEY: str = os.getenv("REMNAWAVE_API_KEY", "")
def validate_required_env(self):
if not self.REMNAWAVE_API_KEY:
raise ValueError("REMNAWAVE_API_KEY is required")
# ❌ Неправильно
API_KEY = "your_api_key_here" # Никогда не хардкодьте ключи
```
**Error Handling:**
```python
# ✅ Правильно - не раскрывайте внутреннюю информацию
try:
result = await remnawave_api.create_subscription(data)
except RemnaWaveAPIError as e:
logger.error(f"RemnaWave API error: {e}")
await message.answer("Временные проблемы с сервисом. Попробуйте позже.")
# ❌ Неправильно - раскрытие внутренних деталей
except Exception as e:
await message.answer(f"Error: {str(e)}")
```
### 🔍 Code Review Security Checklist
- [ ] Нет хардкода API ключей или паролей
- [ ] Все пользовательские данные валидируются
- [ ] SQL запросы используют параметризированные запросы
- [ ] Ошибки логируются без раскрытия чувствительной информации
- [ ] Внешние запросы используют таймауты
- [ ] Webhook подписи проверяются
- [ ] Rate limiting применяется где необходимо
- [ ] Авторизация проверяется для защищенных операций
### 📋 Environment Variables Security
**Обязательные переменные:**
```env
# Критически важные - должны быть установлены
BOT_TOKEN=your_bot_token
REMNAWAVE_API_KEY=your_api_key
ADMIN_IDS=123456789,987654321
# Webhook секреты - генерируйте случайные значения
YOOKASSA_WEBHOOK_SECRET=random_secret_here
TRIBUTE_WEBHOOK_SECRET=another_random_secret
CRYPTOBOT_WEBHOOK_SECRET=yet_another_secret
```
**Генерация безопасных секретов:**
```bash
# Генерация случайного секрета
openssl rand -hex 32
# Или используйте Python
python -c "import secrets; print(secrets.token_hex(32))"
```
### 🐳 Docker Security
**Dockerfile best practices:**
```dockerfile
# Не запускайтесь под root
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# Минимальные права на файлы
COPY --chown=appuser:appuser . /app
# Удаляйте ненужные пакеты
RUN apt-get remove --purge -y build-essential && \
apt-get autoremove -y && \
apt-get clean
```
**docker-compose.yml security:**
```yaml
services:
bot:
# Ограничьте ресурсы
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
# Не пробрасывайте порты наружу без необходимости
ports:
- "127.0.0.1:8081:8081" # Только localhost
# Read-only root filesystem
read_only: true
tmpfs:
- /tmp
```
## Security Updates
### 📅 График обновлений
- **Критические уязвимости**: Исправляются в течение 24-48 часов
- **Высокий приоритет**: Исправляются в течение 7 дней
- **Средний приоритет**: Исправляются в течение 30 дней
- **Низкий приоритет**: Исправляются в следующем планируемом релизе
### 🔄 Процесс обновления
1. **Уведомление**: Администраторы получают уведомление о доступных обновлениях
2. **Тестирование**: Обновления тестируются в изолированной среде
3. **Развертывание**: Постепенное развертывание с возможностью отката
4. **Мониторинг**: Наблюдение за стабильностью после обновления
### 📢 Уведомления о безопасности
Подпишитесь на уведомления:
- **Telegram**: [Bedolaga Chat](https://t.me/+wTdMtSWq8YdmZmVi)
- **GitHub**: Watch repository для получения уведомлений о релизах
- **RSS**: Следите за GitHub Releases
## Compliance and Standards
### 📜 Соответствие стандартам
**GDPR Compliance:**
- Минимизация сбора данных
- Право на удаление данных
- Уведомление о нарушениях безопасности
- Согласие на обработку данных
**PCI DSS (для платежей):**
- Не храним данные банковских карт
- Используем сертифицированные платежные провайдеры
- Шифрование чувствительных данных
- Регулярные аудиты безопасности
### 🛡️ Third-party Security
**Используемые сервисы:**
- **Telegram API**: Официальный API с высоким уровнем безопасности
- **Remnawave**: VPN панель с собственными мерами безопасности
- **YooKassa**: PCI DSS сертифицированный платежный провайдер
- **CryptoBot**: Криптовалютные платежи с блокчейн безопасностью
**Регулярные проверки:**
- Мониторинг безопасности dependencies
- Обновление библиотек и зависимостей
- Проверка CVE для используемых компонентов
## Incident Response
### 🚨 В случае инцидента безопасности
1. **Немедленно**:
- Изолируйте затронутую систему
- Сохраните логи и доказательства
- Уведомите команду разработки
2. **В течение часа**:
- Оцените масштаб инцидента
- Определите затронутых пользователей
- Начните процедуры восстановления
3. **В течение дня**:
- Устраните уязвимость
- Восстановите нормальную работу
- Уведомите пользователей (если необходимо)
4. **После инцидента**:
- Проведите post-mortem анализ
- Обновите процедуры безопасности
- Внедрите дополнительные защитные меры
### 📊 Отчетность
**Внутренние отчеты:**
- Еженедельные отчеты о безопасности
- Ежемесячные аудиты логов
- Квартальные обзоры политик безопасности
**Внешние уведомления:**
- Уведомление пользователей о критических обновлениях
- Публикация Security Advisories при необходимости
- Сотрудничество с исследователями безопасности
---
**Помните**: Безопасность - это постоянный процесс, а не одноразовая настройка. Регулярно обновляйте зависимости, следите за новостями безопасности и не стесняйтесь сообщать о подозрительной активности.
+5 -4
View File
@@ -5,6 +5,7 @@ from aiogram.fsm.storage.memory import MemoryStorage
import redis.asyncio as redis
from app.config import settings
from app.middlewares.global_error import GlobalErrorMiddleware
from app.middlewares.auth import AuthMiddleware
from app.middlewares.logging import LoggingMiddleware
from app.middlewares.throttling import ThrottlingMiddleware
@@ -75,6 +76,9 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
dp = Dispatcher(storage=storage)
dp.message.middleware(GlobalErrorMiddleware())
dp.callback_query.middleware(GlobalErrorMiddleware())
dp.pre_checkout_query.middleware(GlobalErrorMiddleware())
dp.message.middleware(LoggingMiddleware())
dp.callback_query.middleware(LoggingMiddleware())
dp.message.middleware(AuthMiddleware())
@@ -86,7 +90,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
dp.callback_query.middleware(ThrottlingMiddleware())
dp.message.middleware(SubscriptionStatusMiddleware())
dp.callback_query.middleware(SubscriptionStatusMiddleware())
start.register_handlers(dp)
menu.register_handlers(dp)
subscription.register_handlers(dp)
@@ -94,7 +97,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
promocode.register_handlers(dp)
referral.register_handlers(dp)
support.register_handlers(dp)
admin_main.register_handlers(dp)
admin_users.register_handlers(dp)
admin_subscriptions.register_handlers(dp)
@@ -111,9 +113,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
admin_updates.register_handlers(dp)
admin_backup.register_handlers(dp)
admin_welcome_text.register_welcome_text_handlers(dp)
common.register_handlers(dp)
register_stars_handlers(dp)
logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей")
@@ -123,6 +123,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
except Exception as e:
logger.error(f"Ошибка запуска мониторинга техработ: {e}")
logger.info("🛡️ GlobalErrorMiddleware активирован - бот защищен от устаревших callback queries")
logger.info("Бот успешно настроен")
return bot, dp
+15
View File
@@ -32,6 +32,10 @@ class Settings(BaseSettings):
REMNAWAVE_API_URL: str
REMNAWAVE_API_KEY: str
REMNAWAVE_SECRET_KEY: Optional[str] = None
REMNAWAVE_USERNAME: Optional[str] = None
REMNAWAVE_PASSWORD: Optional[str] = None
REMNAWAVE_AUTH_TYPE: str = "api_key"
TRIAL_DURATION_DAYS: int = 3
TRIAL_TRAFFIC_LIMIT_GB: int = 10
@@ -138,6 +142,7 @@ class Settings(BaseSettings):
CONNECT_BUTTON_MODE: str = "guide"
MINIAPP_CUSTOM_URL: str = ""
HIDE_SUBSCRIPTION_LINK: bool = False
ENABLE_LOGO_MODE: bool = True
LOGO_FILE: str = "vpn_logo.png"
SKIP_RULES_ACCEPT: bool = False
@@ -237,6 +242,16 @@ class Settings(BaseSettings):
except (ValueError, AttributeError):
return []
def get_remnawave_auth_params(self) -> Dict[str, Optional[str]]:
return {
"base_url": self.REMNAWAVE_API_URL,
"api_key": self.REMNAWAVE_API_KEY,
"secret_key": self.REMNAWAVE_SECRET_KEY,
"username": self.REMNAWAVE_USERNAME,
"password": self.REMNAWAVE_PASSWORD,
"auth_type": self.REMNAWAVE_AUTH_TYPE
}
def get_autopay_warning_days(self) -> List[int]:
try:
+39 -20
View File
@@ -1,10 +1,10 @@
import logging
from typing import List, Optional, Tuple
from sqlalchemy import select, and_, func, update, delete
from sqlalchemy import select, and_, func, update, delete, text
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database.models import ServerSquad, SubscriptionServer
from app.database.models import ServerSquad, SubscriptionServer, Subscription
logger = logging.getLogger(__name__)
@@ -137,7 +137,7 @@ async def delete_server_squad(db: AsyncSession, server_id: int) -> bool:
connections_count = connections_result.scalar()
if connections_count > 0:
logger.warning(f" Нельзя удалить сервер {server_id}: есть активные подключения ({connections_count})")
logger.warning(f" Нельзя удалить сервер {server_id}: есть активные подключения ({connections_count})")
return False
await db.execute(
@@ -244,10 +244,23 @@ async def get_server_statistics(db: AsyncSession) -> dict:
)
available_servers = available_result.scalar()
with_connections_result = await db.execute(
select(func.count(func.distinct(SubscriptionServer.server_squad_id)))
)
servers_with_connections = with_connections_result.scalar()
servers_with_connections = 0
all_servers_result = await db.execute(select(ServerSquad.squad_uuid))
all_server_uuids = [row[0] for row in all_servers_result.fetchall()]
for squad_uuid in all_server_uuids:
count_result = await db.execute(
text("""
SELECT COUNT(s.id)
FROM subscriptions s
WHERE s.status IN ('active', 'trial')
AND s.connected_squads::text LIKE :uuid_pattern
"""),
{"uuid_pattern": f'%"{squad_uuid}"%'}
)
user_count = count_result.scalar() or 0
if user_count > 0:
servers_with_connections += 1
revenue_result = await db.execute(
select(func.coalesce(func.sum(SubscriptionServer.paid_price_kopeks), 0))
@@ -324,20 +337,26 @@ async def get_server_ids_by_uuids(
async def sync_server_user_counts(db: AsyncSession) -> int:
try:
result = await db.execute(
select(
ServerSquad.id,
ServerSquad.squad_uuid,
func.count(SubscriptionServer.id).label('actual_users')
)
.outerjoin(SubscriptionServer, ServerSquad.id == SubscriptionServer.server_squad_id)
.join(Subscription, SubscriptionServer.subscription_id == Subscription.id)
.where(Subscription.status == 'active')
.group_by(ServerSquad.id, ServerSquad.squad_uuid)
)
all_servers_result = await db.execute(select(ServerSquad.id, ServerSquad.squad_uuid))
all_servers = all_servers_result.fetchall()
logger.info(f"🔍 Найдено серверов для синхронизации: {len(all_servers)}")
updated_count = 0
for server_id, squad_uuid, actual_users in result.fetchall():
for server_id, squad_uuid in all_servers:
count_result = await db.execute(
text("""
SELECT COUNT(s.id)
FROM subscriptions s
WHERE s.status IN ('active', 'trial')
AND s.connected_squads::text LIKE :uuid_pattern
"""),
{"uuid_pattern": f'%"{squad_uuid}"%'}
)
actual_users = count_result.scalar() or 0
logger.info(f"📊 Сервер {server_id} ({squad_uuid[:8]}): {actual_users} пользователей")
await db.execute(
update(ServerSquad)
.where(ServerSquad.id == server_id)
@@ -352,4 +371,4 @@ async def sync_server_user_counts(db: AsyncSession) -> int:
except Exception as e:
logger.error(f"Ошибка синхронизации счетчиков пользователей: {e}")
await db.rollback()
return 0
return 0
+30 -10
View File
@@ -1,6 +1,7 @@
import asyncio
import json
import ssl
import base64
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Union, Any
from urllib.parse import urlparse
@@ -103,12 +104,15 @@ class RemnaWaveAPIError(Exception):
class RemnaWaveAPI:
def __init__(self, base_url: str, api_key: str, secret_key: Optional[str] = None):
def __init__(self, base_url: str, api_key: str, secret_key: Optional[str] = None,
username: Optional[str] = None, password: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.secret_key = secret_key
self.secret_key = secret_key
self.username = username
self.password = password
self.session: Optional[aiohttp.ClientSession] = None
self.authenticated = False
self.authenticated = False
def _detect_connection_type(self) -> str:
parsed = urlparse(self.base_url)
@@ -129,14 +133,9 @@ class RemnaWaveAPI:
return "local"
return "external"
async def __aenter__(self):
conn_type = self._detect_connection_type()
logger.info(f"Подключение к Remnawave: {self.base_url} (тип: {conn_type})")
def _prepare_auth_headers(self) -> Dict[str, str]:
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Forwarded-Proto': 'https',
@@ -144,6 +143,27 @@ class RemnaWaveAPI:
'X-Real-IP': '127.0.0.1'
}
if self.username and self.password:
import base64
credentials = f"{self.username}:{self.password}"
encoded_credentials = base64.b64encode(credentials.encode()).decode()
headers['X-Api-Key'] = f"Basic {encoded_credentials}"
logger.debug("Используем Basic Auth в X-Api-Key заголовке")
else:
headers['X-Api-Key'] = self.api_key
logger.debug("Используем API ключ в X-Api-Key заголовке")
headers['Authorization'] = f'Bearer {self.api_key}'
return headers
async def __aenter__(self):
conn_type = self._detect_connection_type()
logger.info(f"Подключение к Remnawave: {self.base_url} (тип: {conn_type})")
headers = self._prepare_auth_headers()
cookies = None
if self.secret_key:
if ':' in self.secret_key:
+135 -3
View File
@@ -4,7 +4,14 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.keyboards.admin import get_admin_main_keyboard
from app.keyboards.admin import (
get_admin_main_keyboard,
get_admin_users_submenu_keyboard,
get_admin_promo_submenu_keyboard,
get_admin_communications_submenu_keyboard,
get_admin_settings_submenu_keyboard,
get_admin_system_submenu_keyboard
)
from app.localization.texts import get_texts
from app.utils.decorators import admin_required, error_handler
@@ -21,6 +28,17 @@ async def show_admin_panel(
texts = get_texts(db_user.language)
admin_text = texts.ADMIN_PANEL
try:
from app.services.remnawave_service import RemnaWaveService
remnawave_service = RemnaWaveService()
stats = await remnawave_service.get_system_statistics()
users_online = stats.get("system", {}).get("users_online", 0)
admin_text = admin_text.replace(
"\n\nВыберите раздел для управления:",
f"\n\n- 🟢 Онлайн сейчас: {users_online}\n\nВыберите раздел для управления:"
)
except Exception as e:
logger.error(f"Не удалось получить статистику Remnawave для админ-панели: {e}")
await callback.message.edit_text(
admin_text,
@@ -29,9 +47,123 @@ async def show_admin_panel(
await callback.answer()
def register_handlers(dp: Dispatcher):
@admin_required
@error_handler
async def show_users_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
"👥 **Управление пользователями и подписками**\n\n"
"Выберите нужный раздел:",
reply_markup=get_admin_users_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def show_promo_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
"💰 **Промокоды и статистика**\n\n"
"Выберите нужный раздел:",
reply_markup=get_admin_promo_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def show_communications_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
"📨 **Коммуникации**\n\n"
"Управление рассылками и текстами интерфейса:",
reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def show_settings_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
"⚙️ **Настройки системы**\n\n"
"Управление Remnawave, мониторингом и другими настройками:",
reply_markup=get_admin_settings_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def show_system_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
"🛠️ **Системные функции**\n\n"
"Обновления, резервные копии и системные операции:",
reply_markup=get_admin_system_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
def register_handlers(dp: Dispatcher):
dp.callback_query.register(
show_admin_panel,
F.data == "admin_panel"
)
)
dp.callback_query.register(
show_users_submenu,
F.data == "admin_submenu_users"
)
dp.callback_query.register(
show_promo_submenu,
F.data == "admin_submenu_promo"
)
dp.callback_query.register(
show_communications_submenu,
F.data == "admin_submenu_communications"
)
dp.callback_query.register(
show_settings_submenu,
F.data == "admin_submenu_settings"
)
dp.callback_query.register(
show_system_submenu,
F.data == "admin_submenu_system"
)
+71 -27
View File
@@ -1,5 +1,5 @@
import logging
from datetime import datetime
from datetime import datetime, timedelta
from aiogram import Dispatcher, types, F
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
@@ -78,26 +78,52 @@ async def show_users_list(
return
text = f"👥 <b>Список пользователей</b> (стр. {page}/{users_data['total_pages']})\n\n"
for user in users_data["users"]:
status_emoji = "" if user.status == UserStatus.ACTIVE.value else ""
subscription_info = ""
if user.subscription:
if user.subscription.is_trial:
subscription_info = "🎁"
elif user.subscription.is_active:
subscription_info = "💎"
else:
subscription_info = ""
text += f"{status_emoji} {subscription_info} <b>{user.full_name}</b>\n"
text += f"🆔 <code>{user.telegram_id}</code>\n"
text += f"💰 {settings.format_price(user.balance_kopeks)}\n"
text += f"📅 {format_time_ago(user.created_at)}\n\n"
text += "Нажмите на пользователя для управления:"
keyboard = []
for user in users_data["users"]:
if user.status == UserStatus.ACTIVE.value:
status_emoji = ""
elif user.status == UserStatus.BLOCKED.value:
status_emoji = "🚫"
else:
status_emoji = "🗑️"
subscription_emoji = ""
if user.subscription:
if user.subscription.is_trial:
subscription_emoji = "🎁"
elif user.subscription.is_active:
subscription_emoji = "💎"
else:
subscription_emoji = ""
else:
subscription_emoji = ""
button_text = f"{status_emoji} {subscription_emoji} {user.full_name}"
if user.balance_kopeks > 0:
button_text += f" | 💰 {settings.format_price(user.balance_kopeks)}"
button_text += f" | 📅 {format_time_ago(user.created_at)}"
if len(button_text) > 60:
short_name = user.full_name
if len(short_name) > 20:
short_name = short_name[:17] + "..."
button_text = f"{status_emoji} {subscription_emoji} {short_name}"
if user.balance_kopeks > 0:
button_text += f" | 💰 {settings.format_price(user.balance_kopeks)}"
keyboard.append([
types.InlineKeyboardButton(
text=button_text,
callback_data=f"admin_user_manage_{user.id}"
)
])
if users_data["total_pages"] > 1:
pagination_row = get_admin_pagination_keyboard(
users_data["current_page"],
@@ -474,27 +500,45 @@ async def process_user_search(
return
text = f"🔍 <b>Результаты поиска:</b> '{query}'\n\n"
text += "Выберите пользователя:"
keyboard = []
for user in search_results["users"]:
status_emoji = "" if user.status == UserStatus.ACTIVE.value else ""
subscription_info = ""
if user.status == UserStatus.ACTIVE.value:
status_emoji = ""
elif user.status == UserStatus.BLOCKED.value:
status_emoji = "🚫"
else:
status_emoji = "🗑️"
subscription_emoji = ""
if user.subscription:
if user.subscription.is_trial:
subscription_info = "🎁"
subscription_emoji = "🎁"
elif user.subscription.is_active:
subscription_info = "💎"
subscription_emoji = "💎"
else:
subscription_info = ""
subscription_emoji = ""
else:
subscription_emoji = ""
text += f"{status_emoji} {subscription_info} <b>{user.full_name}</b>\n"
text += f"🆔 <code>{user.telegram_id}</code>\n"
text += f"💰 {settings.format_price(user.balance_kopeks)}\n\n"
button_text = f"{status_emoji} {subscription_emoji} {user.full_name}"
button_text += f" | 🆔 {user.telegram_id}"
if user.balance_kopeks > 0:
button_text += f" | 💰 {settings.format_price(user.balance_kopeks)}"
if len(button_text) > 60:
short_name = user.full_name
if len(short_name) > 15:
short_name = short_name[:12] + "..."
button_text = f"{status_emoji} {subscription_emoji} {short_name} | 🆔 {user.telegram_id}"
keyboard.append([
types.InlineKeyboardButton(
text=f"👤 {user.full_name}",
text=button_text,
callback_data=f"admin_user_manage_{user.id}"
)
])
+1 -1
View File
@@ -185,7 +185,7 @@ async def show_subscription_info(
message += "</blockquote>"
if hasattr(subscription, 'subscription_url') and subscription.subscription_url:
if actual_status in ['trial_active', 'paid_active']:
if actual_status in ['trial_active', 'paid_active'] and not settings.HIDE_SUBSCRIPTION_LINK:
message += f"\n\n🔗 <b>Ссылка для подключения:</b>\n<code>{subscription.subscription_url}</code>"
message += f"\n\n📱 Скопируйте ссылку и добавьте в ваше VPN приложение"
+98 -37
View File
@@ -9,33 +9,15 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=texts.ADMIN_USERS, callback_data="admin_users"),
InlineKeyboardButton(text=texts.ADMIN_SUBSCRIPTIONS, callback_data="admin_subscriptions")
InlineKeyboardButton(text="👥 Юзеры и Подписки", callback_data="admin_submenu_users"),
InlineKeyboardButton(text="💰 Промокоды и статистика", callback_data="admin_submenu_promo")
],
[
InlineKeyboardButton(text=texts.ADMIN_PROMOCODES, callback_data="admin_promocodes"),
InlineKeyboardButton(text=texts.ADMIN_MESSAGES, callback_data="admin_messages")
InlineKeyboardButton(text="📨 Коммуникации", callback_data="admin_submenu_communications"),
InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_submenu_settings")
],
[
InlineKeyboardButton(text=texts.ADMIN_MONITORING, callback_data="admin_monitoring"),
InlineKeyboardButton(text=texts.ADMIN_REFERRALS, callback_data="admin_referrals")
],
[
InlineKeyboardButton(text=texts.ADMIN_RULES, callback_data="admin_rules"),
InlineKeyboardButton(text=texts.ADMIN_REMNAWAVE, callback_data="admin_remnawave")
],
[
InlineKeyboardButton(text=texts.ADMIN_STATISTICS, callback_data="admin_statistics"),
InlineKeyboardButton(text="🔧 Техработы", callback_data="maintenance_panel")
],
[
InlineKeyboardButton(text="📢 Сообщения в меню", callback_data="user_messages_panel"),
InlineKeyboardButton(text="👋 Приветственный текст", callback_data="welcome_text_panel")
],
[
InlineKeyboardButton(text="🔄 Обновления", callback_data="admin_updates"),
InlineKeyboardButton(text="🗄️ Бекапы", callback_data="backup_panel")
InlineKeyboardButton(text="🛠️ Системные функции", callback_data="admin_submenu_system")
],
[
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
@@ -43,6 +25,86 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
])
def get_admin_users_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=texts.ADMIN_USERS, callback_data="admin_users"),
InlineKeyboardButton(text=texts.ADMIN_REFERRALS, callback_data="admin_referrals")
],
[
InlineKeyboardButton(text=texts.ADMIN_SUBSCRIPTIONS, callback_data="admin_subscriptions")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
]
])
def get_admin_promo_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=texts.ADMIN_PROMOCODES, callback_data="admin_promocodes"),
InlineKeyboardButton(text=texts.ADMIN_STATISTICS, callback_data="admin_statistics")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
]
])
def get_admin_communications_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=texts.ADMIN_MESSAGES, callback_data="admin_messages")
],
[
InlineKeyboardButton(text="👋 Приветственный текст", callback_data="welcome_text_panel"),
InlineKeyboardButton(text="📢 Сообщения в меню", callback_data="user_messages_panel")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
]
])
def get_admin_settings_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=texts.ADMIN_REMNAWAVE, callback_data="admin_remnawave"),
InlineKeyboardButton(text=texts.ADMIN_MONITORING, callback_data="admin_monitoring")
],
[
InlineKeyboardButton(text=texts.ADMIN_RULES, callback_data="admin_rules"),
InlineKeyboardButton(text="🔧 Техработы", callback_data="maintenance_panel")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
]
])
def get_admin_system_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="📄 Обновления", callback_data="admin_updates"),
InlineKeyboardButton(text="🗄️ Бекапы", callback_data="backup_panel")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
]
])
def get_admin_users_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[
@@ -54,7 +116,7 @@ def get_admin_users_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(text="🗑️ Неактивные", callback_data="admin_users_inactive")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_users")
]
])
@@ -73,7 +135,7 @@ def get_admin_subscriptions_keyboard(language: str = "ru") -> InlineKeyboardMark
InlineKeyboardButton(text="📊 Статистика", callback_data="admin_subs_stats")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_users")
]
])
@@ -88,7 +150,7 @@ def get_admin_promocodes_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(text="📊 Общая статистика", callback_data="admin_promo_general_stats")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_promo")
]
])
@@ -120,7 +182,7 @@ def get_admin_messages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(text="📋 История", callback_data="admin_msg_history")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_communications")
]
])
@@ -139,7 +201,7 @@ def get_admin_monitoring_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_mon_settings")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_settings")
]
])
@@ -152,13 +214,13 @@ def get_admin_remnawave_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
],
[
InlineKeyboardButton(text="🔄 Синхронизация", callback_data="admin_rw_sync"),
InlineKeyboardButton(text="🌍 Управление сквадами", callback_data="admin_rw_squads")
InlineKeyboardButton(text="🌐 Управление сквадами", callback_data="admin_rw_squads")
],
[
InlineKeyboardButton(text="📈 Трафик", callback_data="admin_rw_traffic")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_settings")
]
])
@@ -177,7 +239,7 @@ def get_admin_statistics_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(text="📊 Общая сводка", callback_data="admin_stats_summary")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_promo")
]
])
@@ -320,8 +382,8 @@ def get_custom_criteria_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(text="⚡ Активные сегодня", callback_data="criteria_active_today")
],
[
InlineKeyboardButton(text="👤 Неактивные 7+ дней", callback_data="criteria_inactive_week"),
InlineKeyboardButton(text="👤 Неактивные 30+ дней", callback_data="criteria_inactive_month")
InlineKeyboardButton(text="💤 Неактивные 7+ дней", callback_data="criteria_inactive_week"),
InlineKeyboardButton(text="💤 Неактивные 30+ дней", callback_data="criteria_inactive_month")
],
[
InlineKeyboardButton(text="🤝 Через рефералов", callback_data="criteria_referrals"),
@@ -651,8 +713,8 @@ def get_maintenance_keyboard(
callback_data="maintenance_panel"
),
InlineKeyboardButton(
text=" Назад",
callback_data="admin_panel"
text=" Назад",
callback_data="admin_submenu_settings"
)
])
@@ -689,9 +751,8 @@ def get_welcome_text_keyboard(language: str = "ru", is_enabled: bool = True) ->
InlineKeyboardButton(text="💡 Плейсхолдеры", callback_data="show_placeholders_help")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_communications")
]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
View File
+148
View File
@@ -0,0 +1,148 @@
import logging
from typing import Callable, Dict, Any, Awaitable
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, CallbackQuery, Message
from aiogram.exceptions import TelegramBadRequest
logger = logging.getLogger(__name__)
class GlobalErrorMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
try:
return await handler(event, data)
except TelegramBadRequest as e:
return await self._handle_telegram_error(event, e)
except Exception as e:
logger.error(f"Неожиданная ошибка в GlobalErrorMiddleware: {e}", exc_info=True)
raise
async def _handle_telegram_error(self, event: TelegramObject, error: TelegramBadRequest):
error_message = str(error).lower()
if self._is_old_query_error(error_message):
return await self._handle_old_query(event, error)
elif self._is_message_not_modified_error(error_message):
return await self._handle_message_not_modified(event, error)
elif self._is_bad_request_error(error_message):
return await self._handle_bad_request(event, error)
else:
logger.error(f"Неизвестная Telegram API ошибка: {error}")
raise error
def _is_old_query_error(self, error_message: str) -> bool:
return any(phrase in error_message for phrase in [
"query is too old",
"query id is invalid",
"response timeout expired"
])
def _is_message_not_modified_error(self, error_message: str) -> bool:
return "message is not modified" in error_message
def _is_bad_request_error(self, error_message: str) -> bool:
return any(phrase in error_message for phrase in [
"message not found",
"chat not found",
"bot was blocked by the user",
"user is deactivated"
])
async def _handle_old_query(self, event: TelegramObject, error: TelegramBadRequest):
if isinstance(event, CallbackQuery):
user_info = self._get_user_info(event)
logger.warning(f"🕐 [GlobalErrorMiddleware] Игнорируем устаревший callback '{event.data}' от {user_info}")
else:
logger.warning(f"🕐 [GlobalErrorMiddleware] Игнорируем устаревший запрос: {error}")
return None
async def _handle_message_not_modified(self, event: TelegramObject, error: TelegramBadRequest):
logger.debug(f"📝 [GlobalErrorMiddleware] Сообщение не было изменено: {error}")
if isinstance(event, CallbackQuery):
try:
await event.answer()
logger.debug("✅ Успешно ответили на callback после 'message not modified'")
except TelegramBadRequest as answer_error:
if not self._is_old_query_error(str(answer_error).lower()):
logger.error(f"❌ Ошибка при ответе на callback: {answer_error}")
return None
async def _handle_bad_request(self, event: TelegramObject, error: TelegramBadRequest):
error_message = str(error).lower()
if "bot was blocked" in error_message:
user_info = self._get_user_info(event) if hasattr(event, 'from_user') else "Unknown"
logger.info(f"🚫 [GlobalErrorMiddleware] Бот заблокирован пользователем {user_info}")
return None
elif "user is deactivated" in error_message:
user_info = self._get_user_info(event) if hasattr(event, 'from_user') else "Unknown"
logger.info(f"👻 [GlobalErrorMiddleware] Пользователь деактивирован {user_info}")
return None
elif "chat not found" in error_message or "message not found" in error_message:
logger.warning(f"🔍 [GlobalErrorMiddleware] Чат или сообщение не найдено: {error}")
return None
else:
logger.error(f"❌ [GlobalErrorMiddleware] Неизвестная bad request ошибка: {error}")
raise error
def _get_user_info(self, event: TelegramObject) -> str:
if hasattr(event, 'from_user') and event.from_user:
if event.from_user.username:
return f"@{event.from_user.username}"
else:
return f"ID:{event.from_user.id}"
return "Unknown"
class ErrorStatisticsMiddleware(BaseMiddleware):
def __init__(self):
self.error_counts = {
'old_queries': 0,
'message_not_modified': 0,
'bot_blocked': 0,
'user_deactivated': 0,
'other_errors': 0
}
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
try:
return await handler(event, data)
except TelegramBadRequest as e:
self._count_error(e)
raise
def _count_error(self, error: TelegramBadRequest):
error_message = str(error).lower()
if "query is too old" in error_message:
self.error_counts['old_queries'] += 1
elif "message is not modified" in error_message:
self.error_counts['message_not_modified'] += 1
elif "bot was blocked" in error_message:
self.error_counts['bot_blocked'] += 1
elif "user is deactivated" in error_message:
self.error_counts['user_deactivated'] += 1
else:
self.error_counts['other_errors'] += 1
def get_statistics(self) -> dict:
return self.error_counts.copy()
def reset_statistics(self):
for key in self.error_counts:
self.error_counts[key] = 0
+8 -1
View File
@@ -264,7 +264,14 @@ class MaintenanceService:
self._is_checking = True
self._status.last_check = datetime.utcnow()
api = RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY)
auth_params = settings.get_remnawave_auth_params()
api = RemnaWaveAPI(
base_url=auth_params["base_url"],
api_key=auth_params["api_key"],
secret_key=auth_params["secret_key"],
username=auth_params["username"],
password=auth_params["password"]
)
async with api:
is_connected = await test_api_connection(api)
+14 -11
View File
@@ -23,10 +23,13 @@ logger = logging.getLogger(__name__)
class RemnaWaveService:
def __init__(self):
auth_params = settings.get_remnawave_auth_params()
self.api = RemnaWaveAPI(
base_url=settings.REMNAWAVE_API_URL,
api_key=settings.REMNAWAVE_API_KEY,
secret_key=settings.REMNAWAVE_SECRET_KEY
base_url=auth_params["base_url"],
api_key=auth_params["api_key"],
secret_key=auth_params["secret_key"],
username=auth_params["username"],
password=auth_params["password"]
)
def _parse_remnawave_date(self, date_str: str) -> datetime:
@@ -351,7 +354,7 @@ class RemnaWaveService:
async def update_squad_inbounds(self, squad_uuid: str, inbound_uuids: List[str]) -> bool:
try:
async with RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY) as api:
async with self.api as api:
data = {
'uuid': squad_uuid,
'inbounds': inbound_uuids
@@ -873,7 +876,7 @@ class RemnaWaveService:
async def get_squad_details(self, squad_uuid: str) -> Optional[Dict]:
try:
async with RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY) as api:
async with self.api as api:
squad = await api.get_internal_squad_by_uuid(squad_uuid)
if squad:
return {
@@ -890,7 +893,7 @@ class RemnaWaveService:
async def add_all_users_to_squad(self, squad_uuid: str) -> bool:
try:
async with RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY) as api:
async with self.api as api:
response = await api._make_request('POST', f'/api/internal-squads/{squad_uuid}/bulk-actions/add-users')
return response.get('response', {}).get('eventSent', False)
except Exception as e:
@@ -899,7 +902,7 @@ class RemnaWaveService:
async def remove_all_users_from_squad(self, squad_uuid: str) -> bool:
try:
async with RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY) as api:
async with self.api as api:
response = await api._make_request('DELETE', f'/api/internal-squads/{squad_uuid}/bulk-actions/remove-users')
return response.get('response', {}).get('eventSent', False)
except Exception as e:
@@ -908,7 +911,7 @@ class RemnaWaveService:
async def delete_squad(self, squad_uuid: str) -> bool:
try:
async with RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY) as api:
async with self.api as api:
response = await api.delete_internal_squad(squad_uuid)
return response
except Exception as e:
@@ -917,7 +920,7 @@ class RemnaWaveService:
async def get_all_inbounds(self) -> List[Dict]:
try:
async with RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY) as api:
async with self.api as api:
response = await api._make_request('GET', '/api/config-profiles/inbounds')
inbounds_data = response.get('response', {}).get('inbounds', [])
@@ -938,7 +941,7 @@ class RemnaWaveService:
async def rename_squad(self, squad_uuid: str, new_name: str) -> bool:
try:
async with RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY) as api:
async with self.api as api:
data = {
'uuid': squad_uuid,
'name': new_name
@@ -951,7 +954,7 @@ class RemnaWaveService:
async def create_squad(self, name: str, inbound_uuids: List[str]) -> bool:
try:
async with RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY) as api:
async with self.api as api:
squad = await api.create_internal_squad(name, inbound_uuids)
return squad is not None
except Exception as e:
+6 -2
View File
@@ -23,9 +23,13 @@ logger = logging.getLogger(__name__)
class SubscriptionService:
def __init__(self):
auth_params = settings.get_remnawave_auth_params()
self.api = RemnaWaveAPI(
base_url=settings.REMNAWAVE_API_URL,
api_key=settings.REMNAWAVE_API_KEY
base_url=auth_params["base_url"],
api_key=auth_params["api_key"],
secret_key=auth_params["secret_key"],
username=auth_params["username"],
password=auth_params["password"]
)
async def create_remnawave_user(
+7 -8
View File
@@ -1,11 +1,9 @@
from aiogram.fsm.state import State, StatesGroup
class RegistrationStates(StatesGroup):
waiting_for_rules_accept = State()
waiting_for_referral_code = State()
class SubscriptionStates(StatesGroup):
selecting_period = State()
selecting_traffic = State()
@@ -19,7 +17,6 @@ class SubscriptionStates(StatesGroup):
extending_subscription = State()
confirming_traffic_reset = State()
class BalanceStates(StatesGroup):
waiting_for_amount = State()
waiting_for_stars_payment = State()
@@ -30,7 +27,6 @@ class PromoCodeStates(StatesGroup):
waiting_for_code = State()
waiting_for_referral_code = State()
class AdminStates(StatesGroup):
waiting_for_user_search = State()
@@ -72,13 +68,9 @@ class AdminStates(StatesGroup):
editing_welcome_text = State()
class SupportStates(StatesGroup):
waiting_for_message = State()
class AutoPayStates(StatesGroup):
setting_autopay_days = State()
confirming_autopay_toggle = State()
@@ -89,3 +81,10 @@ class SquadCreateStates(StatesGroup):
class SquadRenameStates(StatesGroup):
waiting_for_new_name = State()
class AdminSubmenuStates(StatesGroup):
in_users_submenu = State()
in_promo_submenu = State()
in_communications_submenu = State()
in_settings_submenu = State()
in_system_submenu = State()
+78 -25
View File
@@ -3,6 +3,7 @@ import functools
from typing import Callable, Any
from aiogram import types
from aiogram.fsm.context import FSMContext
from aiogram.exceptions import TelegramBadRequest
from app.config import settings
from app.localization.texts import get_texts
@@ -25,10 +26,16 @@ def admin_required(func: Callable) -> Callable:
if not user or not settings.is_admin(user.id):
texts = get_texts()
if isinstance(event, types.Message):
await event.answer(texts.ACCESS_DENIED)
elif isinstance(event, types.CallbackQuery):
await event.answer(texts.ACCESS_DENIED, show_alert=True)
try:
if isinstance(event, types.Message):
await event.answer(texts.ACCESS_DENIED)
elif isinstance(event, types.CallbackQuery):
await event.answer(texts.ACCESS_DENIED, show_alert=True)
except TelegramBadRequest as e:
if "query is too old" in str(e).lower():
logger.warning(f"Попытка ответить на устаревший callback query от {user.id if user else 'Unknown'}")
else:
raise
logger.warning(f"Попытка доступа к админской функции от {user.id if user else 'Unknown'}")
return
@@ -44,28 +51,71 @@ def error_handler(func: Callable) -> Callable:
async def wrapper(*args, **kwargs) -> Any:
try:
return await func(*args, **kwargs)
except TelegramBadRequest as e:
error_message = str(e).lower()
if "query is too old" in error_message or "query id is invalid" in error_message:
event = _extract_event(args)
if event and isinstance(event, types.CallbackQuery):
user_info = f"@{event.from_user.username}" if event.from_user.username else f"ID:{event.from_user.id}"
logger.warning(f"🕐 Игнорируем устаревший callback '{event.data}' от {user_info} в {func.__name__}")
else:
logger.warning(f"🕐 Игнорируем устаревший запрос в {func.__name__}: {e}")
return None
elif "message is not modified" in error_message:
logger.debug(f"📝 Сообщение не изменено в {func.__name__}")
event = _extract_event(args)
if event and isinstance(event, types.CallbackQuery):
try:
await event.answer()
except TelegramBadRequest as answer_error:
if "query is too old" not in str(answer_error).lower():
logger.error(f"Ошибка при ответе на callback: {answer_error}")
return None
else:
logger.error(f"Telegram API error в {func.__name__}: {e}")
await _send_error_message(args, kwargs, e)
except Exception as e:
logger.error(f"Ошибка в {func.__name__}: {e}", exc_info=True)
event = None
db_user = kwargs.get('db_user')
for arg in args:
if isinstance(arg, (types.Message, types.CallbackQuery)):
event = arg
break
if event:
texts = get_texts(db_user.language if db_user else 'ru')
if isinstance(event, types.Message):
await event.answer(texts.ERROR)
elif isinstance(event, types.CallbackQuery):
await event.answer(texts.ERROR, show_alert=True)
await _send_error_message(args, kwargs, e)
return wrapper
def _extract_event(args) -> types.TelegramObject:
for arg in args:
if isinstance(arg, (types.Message, types.CallbackQuery)):
return arg
return None
async def _send_error_message(args, kwargs, original_error):
try:
event = _extract_event(args)
db_user = kwargs.get('db_user')
if not event:
return
texts = get_texts(db_user.language if db_user else 'ru')
if isinstance(event, types.Message):
await event.answer(texts.ERROR)
elif isinstance(event, types.CallbackQuery):
await event.answer(texts.ERROR, show_alert=True)
except TelegramBadRequest as e:
if "query is too old" in str(e).lower():
logger.warning("Не удалось отправить сообщение об ошибке - callback query устарел")
else:
logger.error(f"Ошибка при отправке сообщения об ошибке: {e}")
except Exception as e:
logger.error(f"Критическая ошибка при отправке сообщения об ошибке: {e}")
def state_cleanup(func: Callable) -> Callable:
@functools.wraps(func)
@@ -91,10 +141,13 @@ def typing_action(func: Callable) -> Callable:
**kwargs
) -> Any:
if isinstance(event, types.Message):
await event.bot.send_chat_action(
chat_id=event.chat.id,
action="typing"
)
try:
await event.bot.send_chat_action(
chat_id=event.chat.id,
action="typing"
)
except Exception as e:
logger.warning(f"Не удалось отправить typing action: {e}")
return await func(event, *args, **kwargs)
@@ -114,4 +167,4 @@ def rate_limit(rate: float = 1.0, key: str = None):
return wrapper
return decorator
return decorator
+20 -10
View File
@@ -86,7 +86,7 @@ async def main():
admin_notification_service = AdminNotificationService(bot)
version_service.bot = bot
version_service.set_notification_service(admin_notification_service)
logger.info(f"🔄 Сервис версий настроен для репозитория: {version_service.repo}")
logger.info(f"📄 Сервис версий настроен для репозитория: {version_service.repo}")
logger.info(f"📦 Текущая версия: {version_service.current_version}")
logger.info("🔗 Бот подключен к сервисам мониторинга и техработ")
@@ -95,7 +95,6 @@ async def main():
try:
backup_service.bot = bot
# Запускаем автобекапы если они включены
settings_obj = await backup_service.get_backup_settings()
if settings_obj.auto_backup_enabled:
await backup_service.start_auto_backup()
@@ -107,12 +106,20 @@ async def main():
payment_service = PaymentService(bot)
if settings.TRIBUTE_ENABLED:
logger.info("🌐 Запуск Tribute webhook сервера...")
webhook_needed = settings.TRIBUTE_ENABLED or settings.is_cryptobot_enabled()
if webhook_needed:
enabled_services = []
if settings.TRIBUTE_ENABLED:
enabled_services.append("Tribute")
if settings.is_cryptobot_enabled():
enabled_services.append("CryptoBot")
logger.info(f"🌐 Запуск webhook сервера для: {', '.join(enabled_services)}...")
webhook_server = WebhookServer(bot)
await webhook_server.start()
else:
logger.info("ℹ️ Tribute отключен, webhook сервер не запускается")
logger.info("️ Tribute и CryptoBot отключены, webhook сервер не запускается")
if settings.is_yookassa_enabled():
logger.info("💳 Запуск YooKassa webhook сервера...")
@@ -134,7 +141,7 @@ async def main():
maintenance_task = None
if settings.is_version_check_enabled():
logger.info("🔄 Запуск сервиса проверки версий...")
logger.info("📄 Запуск сервиса проверки версий...")
version_check_task = asyncio.create_task(version_service.start_periodic_check())
else:
logger.info("ℹ️ Проверка версий отключена")
@@ -144,11 +151,14 @@ async def main():
logger.info("=" * 50)
logger.info("🎯 Активные webhook endpoints:")
if settings.TRIBUTE_ENABLED:
logger.info(f" Tribute: {settings.WEBHOOK_URL}:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
if webhook_needed:
if settings.TRIBUTE_ENABLED:
logger.info(f" Tribute: {settings.WEBHOOK_URL}:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
if settings.is_cryptobot_enabled():
logger.info(f" CryptoBot: {settings.WEBHOOK_URL}:{settings.TRIBUTE_WEBHOOK_PORT}{settings.CRYPTOBOT_WEBHOOK_PATH}")
if settings.is_yookassa_enabled():
logger.info(f" YooKassa: {settings.WEBHOOK_URL}:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
logger.info("🔄 Активные фоновые сервисы:")
logger.info("📄 Активные фоновые сервисы:")
logger.info(f" Мониторинг: {'Включен' if monitoring_task else 'Отключен'}")
logger.info(f" Техработы: {'Включен' if maintenance_task else 'Отключен'}")
logger.info(f" Проверка версий: {'Включен' if version_check_task else 'Отключен'}")
@@ -252,7 +262,7 @@ async def main():
pass
if webhook_server:
logger.info("️ Остановка Tribute webhook сервера...")
logger.info("ℹ️ Остановка webhook сервера...")
await webhook_server.stop()
if 'bot' in locals():