Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e3a183e2b | |||
| 0879855d3f | |||
| 2358552961 | |||
| 35b461b60c | |||
| 1b5cfc3920 | |||
| 4a179ccc1e | |||
| 4921578af3 | |||
| 55fb1f2044 | |||
| ad38aae7b0 | |||
| 7e0840ef09 | |||
| 03fcb5f2f7 | |||
| 0c4ea86e03 | |||
| 209bb9763e | |||
| 187de6807e | |||
| b8c9302018 | |||
| a86fccffc0 | |||
| a448adec93 | |||
| d45ff62a57 | |||
| 3c50fc6a71 | |||
| 292a120f74 | |||
| a7868787a4 | |||
| 5275f4fbeb | |||
| ba25ec7ad0 | |||
| 301b4124ef | |||
| a936a97e5e | |||
| c390d18399 | |||
| 43df9bff48 | |||
| c15a9ea7cd | |||
| 271742b306 | |||
| ebdadd81c0 | |||
| 1364855303 | |||
| b5d862b231 | |||
| 048a8adaa1 | |||
| 4b7b999d43 | |||
| d02fa83af5 | |||
| 89045095b1 | |||
| 0de721e67d | |||
| c7c171a1d3 | |||
| a2500148fd | |||
| 99b8a3726b | |||
| 8b426e0f6c | |||
| caca561997 | |||
| 385bd78021 | |||
| 8ca127e93f | |||
| ac9b8aaf68 | |||
| 4939f56f1b | |||
| 659e950722 | |||
| 5186ae4334 | |||
| 64cd38e2c3 | |||
| cc29c887c5 | |||
| 7fd7dd7cc8 | |||
| 9ac3088b72 | |||
| 9745885f1b | |||
| 2ccaaebfc9 |
+15
-4
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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" \
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+30
-10
@@ -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
@@ -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
@@ -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}"
|
||||
)
|
||||
])
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user