Compare commits

...

121 Commits

Author SHA1 Message Date
Egor ab49f3cd1e Merge pull request #20 from Fr1ngg/dev
Dev
2025-09-04 16:50:49 +03:00
Egor 732e8f7b8b Update README.md 2025-09-04 16:48:09 +03:00
Egor 9273a4e15b Update .env.example 2025-09-04 16:47:42 +03:00
Egor d89293f76a Update user_service.py 2025-09-04 16:45:57 +03:00
Egor 174462183d Update config.py 2025-09-04 16:18:34 +03:00
Egor 783927be37 Update subscription_service.py 2025-09-04 16:13:58 +03:00
Egor 98c68dd8dc Update inline.py 2025-09-04 16:01:38 +03:00
Egor 13585eab54 Update config.py 2025-09-04 15:53:22 +03:00
Egor 169336da43 Update config.py 2025-09-04 15:48:36 +03:00
Egor 9ef875f6bf Update config.py 2025-09-04 15:46:36 +03:00
Egor 3f54706a72 Update config.py 2025-09-04 15:39:09 +03:00
Egor 835e5e6f7f Update config.py 2025-09-04 15:24:29 +03:00
Egor b292442e8d Update config.py 2025-09-04 15:16:56 +03:00
Egor 9faaf5c963 Update inline.py 2025-09-04 15:13:50 +03:00
Egor 117e88fe4a Update subscription.py 2025-09-04 15:04:07 +03:00
Egor 96bf08a07c Update inline.py 2025-09-04 15:02:59 +03:00
Egor 4523403de5 Update config.py 2025-09-04 15:01:25 +03:00
Egor 02682e88c1 Update subscription.py 2025-09-04 14:48:57 +03:00
Egor 323b9a7e18 Update subscription.py 2025-09-04 14:48:10 +03:00
Egor 100dc3730f Update subscription.py 2025-09-04 14:43:09 +03:00
Egor 919047317a Update subscription.py 2025-09-04 14:38:12 +03:00
Egor 6ae5b59ed1 Update subscription.py 2025-09-04 14:37:47 +03:00
Egor b3123e2e85 Update subscription.py 2025-09-04 14:34:09 +03:00
Egor fbe0ca790e Update subscription.py 2025-09-04 14:31:40 +03:00
Egor 785648ba7a Update subscription.py 2025-09-04 14:27:22 +03:00
Egor 06f8728efa Update inline.py 2025-09-04 14:21:00 +03:00
Egor 99f102a06e Update config.py 2025-09-04 14:19:23 +03:00
Egor d759dd3a66 Update user_service.py 2025-09-04 13:58:58 +03:00
Egor 73dc22b0b8 Update texts.py 2025-09-04 04:27:33 +03:00
Egor 5a082f0eb8 Update texts.py 2025-09-04 04:20:58 +03:00
Egor 073d5a119b Update subscription.py 2025-09-04 04:20:36 +03:00
Egor 2dab106bef Update README.md 2025-09-04 04:13:00 +03:00
Egor ec8c7b7242 Update .env.example 2025-09-04 04:12:46 +03:00
Egor a4b96a089f Update user_service.py 2025-09-04 04:07:09 +03:00
Egor 269f8f6220 Update user_service.py 2025-09-04 04:04:19 +03:00
Egor f1beda2b89 Update user_service.py 2025-09-04 04:01:03 +03:00
Egor 6a00db32ae Update config.py 2025-09-04 03:28:51 +03:00
Egor 276bc5eb40 Update .env.example 2025-09-04 03:27:58 +03:00
Egor 8678afd055 Update subscription.py 2025-09-04 03:26:34 +03:00
Egor 31340ad4a5 Update subscription_service.py 2025-09-04 03:25:08 +03:00
Egor 98ce47ce71 Update inline.py 2025-09-04 03:24:07 +03:00
Egor 3462f5b3b1 Update subscription.py 2025-09-04 02:57:42 +03:00
Egor 7ec606f93c Update config.py 2025-09-03 21:11:15 +03:00
Egor 7d13bf47aa Update README.md 2025-09-03 21:08:32 +03:00
Egor 87773806fc Update .env.example 2025-09-03 21:08:09 +03:00
Egor dad6200ad8 Update bot.py 2025-09-03 21:07:40 +03:00
Egor c3991162c2 Update payment_service.py 2025-09-03 21:06:48 +03:00
Egor 08feb1f876 Update telegram_stars.py 2025-09-03 21:05:33 +03:00
Egor 6f15070052 Update balance.py 2025-09-03 21:05:13 +03:00
Egor 0c4e73ab92 Create stars_payments.py 2025-09-03 21:04:50 +03:00
Egor 48902c4968 Update telegram_stars.py 2025-09-03 20:57:01 +03:00
Egor 3c715577a6 Update telegram_stars.py 2025-09-03 20:55:25 +03:00
Egor 8e67c7a97a Update texts.py 2025-09-03 20:09:01 +03:00
Egor c938d48026 Update texts.py 2025-09-03 20:02:46 +03:00
Egor 99a4927f8a Update texts.py 2025-09-03 19:53:10 +03:00
Egor df5b8dcdcd Update inline.py 2025-09-03 19:52:51 +03:00
Egor d4a1a2e64d Update menu.py 2025-09-03 19:52:34 +03:00
Egor 5e2df269a3 Update start.py 2025-09-03 19:52:16 +03:00
Egor a2056e0441 Update docker-registry.yml 2025-09-03 18:38:28 +03:00
Egor a6bbd8f8fb Add files via upload 2025-09-03 06:39:35 +03:00
Egor 9f4e0cf5f8 Update menu.py 2025-09-03 06:34:44 +03:00
Egor c7e8365d3f Update start.py 2025-09-03 06:33:37 +03:00
Egor 49b10b490e Update inline.py 2025-09-03 06:29:20 +03:00
Egor f26bca8627 Update texts.py 2025-09-03 06:27:46 +03:00
Egor df139b3d34 Update texts.py 2025-09-03 06:22:09 +03:00
Egor 05d1edf278 Update menu.py 2025-09-03 06:21:21 +03:00
Egor a518df8ff0 Update menu.py 2025-09-03 06:16:38 +03:00
Egor 0a13fe4f40 Update menu.py 2025-09-03 06:13:15 +03:00
Egor 6410bd1085 Update menu.py 2025-09-03 06:03:26 +03:00
Egor 55c4549cff Update inline.py 2025-09-03 06:01:29 +03:00
Egor e898948073 Update texts.py 2025-09-03 05:59:21 +03:00
Egor 5f1c8d8ac0 Update README.md 2025-09-03 05:02:51 +03:00
Egor 8d1f8f2355 Update README.md 2025-09-03 04:56:05 +03:00
Egor fbbbad608b Update .env.example 2025-09-03 04:55:38 +03:00
Egor 03fc4abd2a Update subscription.py 2025-09-03 04:32:16 +03:00
Egor f280a37818 Update subscription.py 2025-09-03 04:25:31 +03:00
Egor f100ec62d4 Update subscription.py 2025-09-03 04:24:55 +03:00
Egor cd827612d9 Update subscription.py 2025-09-03 04:23:41 +03:00
Egor 50da2f82c8 Update subscription_service.py 2025-09-03 04:22:05 +03:00
Egor 4c814ec5f4 Update inline.py 2025-09-03 04:13:35 +03:00
Egor fb8c02d5ab Update subscription.py 2025-09-03 04:12:34 +03:00
Egor 465ad3d1ad Update inline.py 2025-09-03 04:10:55 +03:00
Egor 2bfaab7d16 Update subscription_service.py 2025-09-03 04:07:33 +03:00
Egor e356542df7 Update subscription.py 2025-09-03 00:46:11 +03:00
Egor a3dfbdce3b Update inline.py 2025-09-03 00:44:57 +03:00
Egor a666e8d248 Update subscription_service.py 2025-09-03 00:44:18 +03:00
Egor 9ef8df22a8 Update subscription.py 2025-09-03 00:43:54 +03:00
Egor a1471c8b9a Update subscription.py 2025-09-03 00:39:35 +03:00
Egor a99a14922c Update subscription_service.py 2025-09-03 00:17:11 +03:00
Egor ce954ad775 Update subscription.py 2025-09-03 00:15:42 +03:00
Egor efba7c10d0 Update subscription.py 2025-09-03 00:15:01 +03:00
Egor cd10c131b2 Update inline.py 2025-09-03 00:14:30 +03:00
Egor 3e4cb8dfa2 Update inline.py 2025-09-02 23:38:52 +03:00
Egor 6471ab547c Update subscription.py 2025-09-02 23:37:57 +03:00
Egor c10bf07124 Update inline.py 2025-09-02 23:32:14 +03:00
Egor 4e05882522 Update subscription.py 2025-09-02 23:30:51 +03:00
Egor 061b292d92 Update subscription.py 2025-09-02 23:28:30 +03:00
Egor 779a6c309a Update subscription.py 2025-09-02 23:24:16 +03:00
Egor c79c71f209 Update remnawave_api.py 2025-09-02 17:35:45 +03:00
Egor d4dbce5606 Update remnawave_api.py 2025-09-02 17:32:41 +03:00
Egor e53d99223d Update config.py 2025-09-02 15:32:45 +03:00
Egor ec7274e0c9 Update remnawave_api.py 2025-09-02 15:32:29 +03:00
Egor 8fe631a98e Update remnawave_api.py 2025-09-02 15:26:00 +03:00
Egor bd831bd92b Update config.py 2025-09-02 15:21:43 +03:00
Egor 9b31ba8f3d Update remnawave_api.py 2025-09-02 15:21:15 +03:00
Egor 3759be333a Update remnawave_api.py 2025-09-02 15:15:25 +03:00
Egor 555894b26c Update README.md 2025-09-02 14:55:47 +03:00
Egor bb636c003c Update .env.example 2025-09-02 14:50:48 +03:00
Egor 40c7b4aebd Update remnawave_api.py 2025-09-02 14:23:38 +03:00
Egor 028bd4888b Update remnawave_api.py 2025-09-02 14:10:01 +03:00
Egor 27d64e361a Update remnawave_api.py 2025-09-02 14:03:04 +03:00
Egor 6fc7dd49dd Update remnawave_api.py 2025-09-02 13:40:32 +03:00
Egor b835ba1d65 Update remnawave_api.py 2025-09-02 13:33:15 +03:00
Egor 811e985087 Update .env.example 2025-09-02 13:29:29 +03:00
Egor c16733b93d Update config.py 2025-09-02 13:29:13 +03:00
Egor 23ab3c5198 Update remnawave_service.py 2025-09-02 13:28:09 +03:00
Egor 9b8f121e69 Update remnawave_api.py 2025-09-02 13:25:49 +03:00
Egor 86b1b95fb9 Update subscription.py 2025-09-02 08:52:42 +03:00
Egor 9418681a00 Update subscription_service.py 2025-09-02 08:50:35 +03:00
Egor 7ebd937e2c Update README.md 2025-09-02 02:21:40 +03:00
Egor 25c404f999 Update README.md 2025-09-02 02:09:29 +03:00
19 changed files with 1159 additions and 291 deletions
+20 -12
View File
@@ -21,17 +21,30 @@ POSTGRES_USER=remnawave_user
POSTGRES_PASSWORD=secure_password_123
# ===== REMNAWAVE API =====
REMNAWAVE_API_URL=
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
# ========= ПОДПИСКИ =========
# ===== ТРИАЛ ПОДПИСКА =====
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_LIMIT_GB=10
TRIAL_DEVICE_LIMIT=1
TRIAL_SQUAD_UUID=
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
# ===== ПЛАТНАЯ ПОДПИСКА =====
# Сколько устройств доступно по дефолту при покупке платной подписки
DEFAULT_DEVICE_LIMIT=3
# Максимум устройств достопных к покупке (0 = Нет лимита)
MAX_DEVICES_LIMIT=15
# Дефолт параметры для подписок выданных через админку
DEFAULT_TRAFFIC_LIMIT_GB=100
DEFAULT_DEVICE_LIMIT=1
# ===== ГЛОБАЛЬНЫЙ ПАРАМЕТР ДЛЯ ВСЕХ ПОДПИСОК =====
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
# ===== НАСТРОЙКИ ТРАФИКА =====
# Режим выбора трафика:
@@ -60,16 +73,10 @@ PRICE_90_DAYS=36900
PRICE_180_DAYS=69900
PRICE_360_DAYS=109900
# Цены за трафик
PRICE_TRAFFIC_5GB=1000
PRICE_TRAFFIC_10GB=2000
PRICE_TRAFFIC_25GB=3000
PRICE_TRAFFIC_50GB=4000
PRICE_TRAFFIC_100GB=6000
PRICE_TRAFFIC_250GB=10000
PRICE_TRAFFIC_UNLIMITED=15000
# Выводимые пакеты трафика и их цены в копейках
TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,250:17000:false,500:19000:false,1000:19500:true,0:20000:true"
# Цена за дополнительное устройство
# Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!)
PRICE_PER_DEVICE=5000
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
@@ -86,6 +93,7 @@ MIN_BALANCE_FOR_AUTOPAY_KOPEKS=10000
# Telegram Stars (работает автоматически)
TELEGRAM_STARS_ENABLED=true
TELEGRAM_STARS_RATE_RUB=1.3
# Tribute (https://tribute.app)
TRIBUTE_ENABLED=false
+3
View File
@@ -4,12 +4,14 @@ on:
branches:
- main
- master
- dev
tags:
- 'v*'
pull_request:
branches:
- main
- master
- dev
env:
REGISTRY: ghcr.io
IMAGE_NAME: fr1ngg/remnawave-bedolaga-telegram-bot
@@ -42,6 +44,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
+45 -22
View File
@@ -38,7 +38,7 @@
### 🎛️ **Гибкость конфигурации**
- 🌍 **Выбор стран** - пользователи сами выбирают нужные локации
- 📱 **Управление устройствами** - от 1 до 10 шт
- 📱 **Управление устройствами** - от 1 до 10 шт (С настройкой кол-ва бесплатных у триальных и платных подписок)
- 📊 **Гибкие тарифы** - от 5GB до безлимита, от 14 дней до года
- 🎁 **Промо-система** - коды на деньги, дни подписки, триал-периоды
- 3 режима показа ссылки подписки: 1) С гайдом по подключению прямо в боте(тянущий данные приложений и ссылок на скачку из app-config.json) 2) Обычное открытие ссылки подписки в миниапе 3) Интеграция сабпейджа maposia - кастомно прописать ссылку можно
@@ -51,6 +51,7 @@
- 📈 **Масштабируемость**
- 🔧 **Мониторинг** - Prometheus, Grafana, health checks
- 🔧 **Режим технических работ** - Ручное включение + Мониторинг системы, который в случае падении панели Remnawave переведет бота в режим технических работ и обратно - отключит его, если панель поднимется.
- Интеграция с системой защиты панели Remnawave через куки-аутентификацию, которая используется в [remnawave-reverse-proxy](https://github.com/eGamesAPI/remnawave-reverse-proxy) для скрытия панели от несанкционированного доступа.
---
@@ -113,17 +114,30 @@ POSTGRES_USER=remnawave_user
POSTGRES_PASSWORD=secure_password_123
# ===== REMNAWAVE API =====
REMNAWAVE_API_URL=
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
# ========= ПОДПИСКИ =========
# ===== ТРИАЛ ПОДПИСКА =====
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_LIMIT_GB=10
TRIAL_DEVICE_LIMIT=1
TRIAL_SQUAD_UUID=
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
# ===== ПЛАТНАЯ ПОДПИСКА =====
# Сколько устройств доступно по дефолту при покупке платной подписки
DEFAULT_DEVICE_LIMIT=3
# Максимум устройств достопных к покупке (0 = Нет лимита)
MAX_DEVICES_LIMIT=15
# Дефолт параметры для подписок выданных через админку
DEFAULT_TRAFFIC_LIMIT_GB=100
DEFAULT_DEVICE_LIMIT=1
# ===== ГЛОБАЛЬНЫЙ ПАРАМЕТР ДЛЯ ВСЕХ ПОДПИСОК =====
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
# ===== НАСТРОЙКИ ТРАФИКА =====
# Режим выбора трафика:
@@ -152,16 +166,10 @@ PRICE_90_DAYS=36900
PRICE_180_DAYS=69900
PRICE_360_DAYS=109900
# Цены за трафик
PRICE_TRAFFIC_5GB=1000
PRICE_TRAFFIC_10GB=2000
PRICE_TRAFFIC_25GB=3000
PRICE_TRAFFIC_50GB=4000
PRICE_TRAFFIC_100GB=6000
PRICE_TRAFFIC_250GB=10000
PRICE_TRAFFIC_UNLIMITED=15000
# Выводимые пакеты трафика и их цены в копейках
TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,250:17000:false,500:19000:false,1000:19500:true,0:20000:true"
# Цена за дополнительное устройство
# Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!)
PRICE_PER_DEVICE=5000
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
@@ -178,6 +186,7 @@ MIN_BALANCE_FOR_AUTOPAY_KOPEKS=10000
# Telegram Stars (работает автоматически)
TELEGRAM_STARS_ENABLED=true
TELEGRAM_STARS_RATE_RUB=1.3
# Tribute (https://tribute.app)
TRIBUTE_ENABLED=false
@@ -431,16 +440,16 @@ your-domain.com {
```bash
# Быстрый старт
docker compose up -d --build
docker compose up -d
# Статус сервисов
docker compose ps
# Логи
docker compose logs -f bot
docker compose logs
# Перезапуск
docker compose restart bot
docker compose restart
# Остановка
docker compose down
@@ -462,26 +471,30 @@ docker compose down -v --remove-orphans
🛒 **Умная покупка подписок**
- 📅 Выбор периода (14-360 дней) (С возможностью настройки выводимых периодов 14/30/60/90/180/360 дней)
- 📊 Настройка трафика (5GB - безлимит) (Можно откоючить данный шаг, всем подпискам выдавать единый)
- 🌍 Выбор стран через сквады
- 📱 Количество устройств (1-10)
- 🌍 Выбор стран через сквады (Доступно при наличие двух стран(сквадов) в продаже)
- 📱 Количество устройств (1-5)
🧪 **Тестовая подписка**
- Получение настраиваемой разовой тестовой подписки
- Уведомления об истечении с предложением о переходе н платную версию
💰 **Удобные платежи**
- ⭐ Telegram Stars
- 💳 Tribute
- 💳 YooKassa
- 🎁 Реферальные бонусы
- Детальная история транзакций
📱 **Управление подписками**
- 📈 Просмотр статистики использования
- 🔄 Автопродление с баланса
- 🔄 Сброс/увеличение трафика
- 🌍 Смена стран на лету
- Устройства с возможностью докупки/сброса устройств)
- 🌍 Смена стран на лету (Доступно при наличие двух стран(сквадов) в продаже)
- 📱 Настройка устройств с возможностью докупки(до 10ти устройств) + сброса устройств
🎁 **Бонусная система**
- 🎫 Промокоды на деньги/дни
- 👥 Реферальная программа
- 🆓 Бесплатный триал
- 🔔 Ежедневные уведомления
</td>
@@ -503,7 +516,7 @@ docker compose down -v --remove-orphans
🎫 **Промо-система**
- 🎁 Создание промокодов (деньги/дни/длинный триал)
- 📊 Статистика использования
- ⚙️ Полное редактирование промокодов
- ⚙️ Полное редактирование промокодов (Изминение условий, активация/деактивация/удаление + статистика применения)
🖥️ **Мониторинг системы**
- 💚 Состояние Remnawave панели
@@ -517,6 +530,16 @@ docker compose down -v --remove-orphans
- 🔔 Автоуведомления о продлении
- 💬 Система поддержки
- 📝 Настройка правил сервиса
- Поддержка HTML разметки
🚧 **Режим технических работ**
- Ручное включение/отключение с указанием причины
- Включение/выключение мониторинга состояния панели (проверяет соеденение до панели раз в 30 сек(можно изменить промежуток)
- Принудительаная проверка API
📖 **Правила сервиса**
- Настройка правил сервиса
- Просмотр текущих правил
</td>
</tr>
+5
View File
@@ -25,6 +25,7 @@ from app.handlers.admin import (
statistics as admin_statistics, servers as admin_servers,
maintenance as admin_maintenance
)
from app.handlers.stars_payments import register_stars_handlers
logger = logging.getLogger(__name__)
@@ -70,6 +71,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
dp.callback_query.middleware(LoggingMiddleware())
dp.message.middleware(AuthMiddleware())
dp.callback_query.middleware(AuthMiddleware())
dp.pre_checkout_query.middleware(AuthMiddleware())
dp.message.middleware(MaintenanceMiddleware())
dp.callback_query.middleware(MaintenanceMiddleware())
dp.message.middleware(ThrottlingMiddleware())
@@ -98,6 +100,9 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
common.register_handlers(dp)
register_stars_handlers(dp)
logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей")
try:
await maintenance_service.start_monitoring()
logger.info("Мониторинг техработ запущен")
+157 -17
View File
@@ -1,5 +1,5 @@
import os
from typing import List, Optional, Union
from typing import List, Optional, Union, Dict
from pydantic_settings import BaseSettings
from pydantic import field_validator, Field
from pathlib import Path
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
REMNAWAVE_API_URL: str
REMNAWAVE_API_KEY: str
REMNAWAVE_SECRET_KEY: Optional[str] = None
TRIAL_DURATION_DAYS: int = 3
TRIAL_TRAFFIC_LIMIT_GB: int = 10
@@ -24,6 +25,7 @@ class Settings(BaseSettings):
DEFAULT_DEVICE_LIMIT: int = 1
TRIAL_SQUAD_UUID: str
DEFAULT_TRAFFIC_RESET_STRATEGY: str = "MONTH"
MAX_DEVICES_LIMIT: int = 20
TRIAL_WARNING_HOURS: int = 2
ENABLE_NOTIFICATIONS: bool = True
@@ -43,13 +45,17 @@ class Settings(BaseSettings):
PRICE_180_DAYS: int = 499000
PRICE_360_DAYS: int = 899000
PRICE_TRAFFIC_5GB: int = 10000
PRICE_TRAFFIC_10GB: int = 19000
PRICE_TRAFFIC_25GB: int = 45000
PRICE_TRAFFIC_50GB: int = 85000
PRICE_TRAFFIC_100GB: int = 159000
PRICE_TRAFFIC_250GB: int = 369000
PRICE_TRAFFIC_UNLIMITED: int = 0
PRICE_TRAFFIC_5GB: int = 2000
PRICE_TRAFFIC_10GB: int = 3500
PRICE_TRAFFIC_25GB: int = 7000
PRICE_TRAFFIC_50GB: int = 11000
PRICE_TRAFFIC_100GB: int = 15000
PRICE_TRAFFIC_250GB: int = 17000
PRICE_TRAFFIC_500GB: int = 19000
PRICE_TRAFFIC_1000GB: int = 19500
PRICE_TRAFFIC_UNLIMITED: int = 20000
TRAFFIC_PACKAGES_CONFIG: str = ""
PRICE_PER_DEVICE: int = 5000
@@ -74,6 +80,7 @@ class Settings(BaseSettings):
MAINTENANCE_MESSAGE: str = "🔧 Ведутся технические работы. Сервис временно недоступен. Попробуйте позже."
TELEGRAM_STARS_ENABLED: bool = True
TELEGRAM_STARS_RATE_RUB: float = 1.3
TRIBUTE_ENABLED: bool = False
TRIBUTE_API_KEY: Optional[str] = None
@@ -280,6 +287,137 @@ class Settings(BaseSettings):
service_name=self.PAYMENT_SERVICE_NAME,
description=description
)
def get_stars_rate(self) -> float:
return self.TELEGRAM_STARS_RATE_RUB
def stars_to_rubles(self, stars: int) -> float:
return stars * self.get_stars_rate()
def rubles_to_stars(self, rubles: float) -> int:
return max(1, int(rubles / self.get_stars_rate()))
def get_traffic_packages(self) -> List[Dict]:
import logging
logger = logging.getLogger(__name__)
try:
packages = []
config_str = self.TRAFFIC_PACKAGES_CONFIG.strip()
logger.info(f"CONFIG STRING: '{config_str}'")
if not config_str:
logger.info("CONFIG EMPTY, USING FALLBACK")
return self._get_fallback_traffic_packages()
logger.info("PARSING CONFIG...")
for package_config in config_str.split(','):
package_config = package_config.strip()
if not package_config:
continue
parts = package_config.split(':')
if len(parts) != 3:
continue
try:
gb = int(parts[0])
price = int(parts[1])
enabled = parts[2].lower() == 'true'
packages.append({
"gb": gb,
"price": price,
"enabled": enabled
})
except ValueError:
continue
logger.info(f"PARSED {len(packages)} packages from config")
return packages if packages else self._get_fallback_traffic_packages()
except Exception as e:
logger.info(f"ERROR PARSING CONFIG: {e}")
return self._get_fallback_traffic_packages()
def _get_fallback_traffic_packages(self) -> List[Dict]:
try:
if self.TRAFFIC_PACKAGES_CONFIG.strip():
packages = []
for package_config in self.TRAFFIC_PACKAGES_CONFIG.split(','):
package_config = package_config.strip()
if not package_config:
continue
parts = package_config.split(':')
if len(parts) != 3:
continue
try:
gb = int(parts[0])
price = int(parts[1])
enabled = parts[2].lower() == 'true'
packages.append({
"gb": gb,
"price": price,
"enabled": enabled
})
except ValueError:
continue
if packages:
return packages
except Exception as e:
pass
return [
{"gb": 5, "price": self.PRICE_TRAFFIC_5GB, "enabled": True},
{"gb": 10, "price": self.PRICE_TRAFFIC_10GB, "enabled": True},
{"gb": 25, "price": self.PRICE_TRAFFIC_25GB, "enabled": True},
{"gb": 50, "price": self.PRICE_TRAFFIC_50GB, "enabled": True},
{"gb": 100, "price": self.PRICE_TRAFFIC_100GB, "enabled": True},
{"gb": 250, "price": self.PRICE_TRAFFIC_250GB, "enabled": True},
{"gb": 500, "price": self.PRICE_TRAFFIC_500GB, "enabled": True},
{"gb": 1000, "price": self.PRICE_TRAFFIC_1000GB, "enabled": True},
{"gb": 0, "price": self.PRICE_TRAFFIC_UNLIMITED, "enabled": True},
]
def get_traffic_price(self, gb: int) -> int:
packages = self.get_traffic_packages()
for package in packages:
if package["gb"] == gb and package["enabled"]:
return package["price"]
enabled_packages = [pkg for pkg in packages if pkg["enabled"]]
if not enabled_packages:
return 0
unlimited_package = next((pkg for pkg in enabled_packages if pkg["gb"] == 0), None)
finite_packages = [pkg for pkg in enabled_packages if pkg["gb"] > 0]
if finite_packages:
max_package = max(finite_packages, key=lambda x: x["gb"])
if gb > max_package["gb"]:
if unlimited_package:
return unlimited_package["price"]
else:
return max_package["price"]
suitable_packages = [pkg for pkg in finite_packages if pkg["gb"] >= gb]
if suitable_packages:
nearest_package = min(suitable_packages, key=lambda x: x["gb"])
return nearest_package["price"]
if unlimited_package:
return unlimited_package["price"]
return 0
model_config = {
"env_file": ".env",
@@ -299,12 +437,14 @@ PERIOD_PRICES = {
360: settings.PRICE_360_DAYS,
}
TRAFFIC_PRICES = {
5: settings.PRICE_TRAFFIC_5GB,
10: settings.PRICE_TRAFFIC_10GB,
25: settings.PRICE_TRAFFIC_25GB,
50: settings.PRICE_TRAFFIC_50GB,
100: settings.PRICE_TRAFFIC_100GB,
250: settings.PRICE_TRAFFIC_250GB,
0: settings.PRICE_TRAFFIC_UNLIMITED,
}
def get_traffic_prices() -> Dict[int, int]:
packages = settings.get_traffic_packages()
return {package["gb"]: package["price"] for package in packages}
TRAFFIC_PRICES = get_traffic_prices()
def refresh_traffic_prices():
global TRAFFIC_PRICES
TRAFFIC_PRICES = get_traffic_prices()
refresh_traffic_prices()
+1 -1
View File
@@ -512,7 +512,7 @@ async def get_subscription_renewal_cost(
if subscription.traffic_limit_gb > 0:
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_cost = base_price + servers_cost + traffic_cost + devices_cost
+34 -11
View File
@@ -8,6 +8,7 @@ import aiohttp
import logging
from dataclasses import dataclass
from enum import Enum
from urllib.parse import urlparse, urljoin
logger = logging.getLogger(__name__)
@@ -33,7 +34,7 @@ class RemnaWaveUser:
username: str
status: UserStatus
used_traffic_bytes: int
lifetime_used_traffic_bytes: int # Новое поле
lifetime_used_traffic_bytes: int
traffic_limit_bytes: int
traffic_limit_strategy: TrafficLimitStrategy
expire_at: datetime
@@ -92,10 +93,12 @@ class RemnaWaveAPIError(Exception):
class RemnaWaveAPI:
def __init__(self, base_url: str, api_key: str):
def __init__(self, base_url: str, api_key: str, secret_key: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.secret_key = secret_key
self.session: Optional[aiohttp.ClientSession] = None
self.authenticated = False
def _detect_connection_type(self) -> str:
parsed = urlparse(self.base_url)
@@ -124,18 +127,29 @@ class RemnaWaveAPI:
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Forwarded-Proto': 'https',
'X-Forwarded-For': '127.0.0.1',
'X-Real-IP': '127.0.0.1'
}
cookies = None
if self.secret_key:
if ':' in self.secret_key:
key_name, key_value = self.secret_key.split(':', 1)
cookies = {key_name: key_value}
logger.debug(f"🍪 Используем куки: {key_name}=***")
else:
cookies = {self.secret_key: self.secret_key}
logger.debug(f"🍪 Используем куки: {self.secret_key}=***")
connector_kwargs = {}
if conn_type == "local":
logger.debug("🏠 Использую локальные заголовки proxy")
headers.update({
'X-Forwarded-For': '127.0.0.1',
'X-Forwarded-Proto': 'https',
'X-Forwarded-Host': 'localhost',
'X-Real-IP': '127.0.0.1',
'Host': 'localhost'
})
@@ -152,11 +166,18 @@ class RemnaWaveAPI:
connector = aiohttp.TCPConnector(**connector_kwargs)
self.session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
headers=headers,
connector=connector
)
session_kwargs = {
'timeout': aiohttp.ClientTimeout(total=30),
'headers': headers,
'connector': connector
}
if cookies:
session_kwargs['cookies'] = cookies
self.session = aiohttp.ClientSession(**session_kwargs)
self.authenticated = True
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
@@ -194,6 +215,8 @@ class RemnaWaveAPI:
if response.status >= 400:
error_message = response_data.get('message', f'HTTP {response.status}')
logger.error(f"API Error {response.status}: {error_message}")
logger.error(f"Response: {response_text[:500]}")
raise RemnaWaveAPIError(
error_message,
response.status,
+22 -10
View File
@@ -13,6 +13,14 @@ class TelegramStarsService:
def __init__(self, bot: Bot):
self.bot = bot
@staticmethod
def calculate_stars_from_rubles(rubles: float) -> int:
return settings.rubles_to_stars(rubles)
@staticmethod
def calculate_rubles_from_stars(stars: int) -> float:
return settings.stars_to_rubles(stars)
async def create_invoice(
self,
chat_id: int,
@@ -23,7 +31,8 @@ class TelegramStarsService:
start_parameter: Optional[str] = None
) -> Optional[str]:
try:
stars_amount = max(1, amount_kopeks // 100)
amount_rubles = amount_kopeks / 100
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
invoice_link = await self.bot.create_invoice_link(
title=title,
@@ -35,7 +44,10 @@ class TelegramStarsService:
start_parameter=start_parameter
)
logger.info(f"Создан Stars invoice на {stars_amount} звезд для {chat_id}")
logger.info(
f"Создан Stars invoice на {stars_amount} звезд (~{amount_rubles:.2f}₽) "
f"для {chat_id}, курс: {settings.get_stars_rate():.2f}₽/⭐"
)
return invoice_link
except Exception as e:
@@ -52,7 +64,8 @@ class TelegramStarsService:
keyboard: Optional[InlineKeyboardMarkup] = None
) -> Optional[Dict[str, Any]]:
try:
stars_amount = max(1, amount_kopeks // 100)
amount_rubles = amount_kopeks / 100
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
message = await self.bot.send_invoice(
chat_id=chat_id,
@@ -65,10 +78,14 @@ class TelegramStarsService:
reply_markup=keyboard
)
logger.info(f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд")
logger.info(
f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд "
f"(~{amount_rubles:.2f}₽), курс: {settings.get_stars_rate():.2f}₽/⭐"
)
return {
"message_id": message.message_id,
"stars_amount": stars_amount,
"rubles_amount": amount_rubles,
"payload": payload
}
@@ -88,13 +105,8 @@ class TelegramStarsService:
ok=ok,
error_message=error_message
)
logger.info(f"Ответ на pre_checkout_query: ok={ok}")
return True
except Exception as e:
logger.error(f"Ошибка ответа на pre_checkout_query: {e}")
return False
def calculate_stars_amount(self, rubles: float) -> int:
return max(1, int(rubles))
def calculate_rubles_from_stars(self, stars: int) -> float:
return float(stars)
+10 -3
View File
@@ -356,10 +356,15 @@ async def process_stars_payment_amount(
texts = get_texts(db_user.language)
if not settings.TELEGRAM_STARS_ENABLED:
await message.answer(" Оплата Stars временно недоступна")
await message.answer(" Оплата Stars временно недоступна")
return
try:
from app.external.telegram_stars import TelegramStarsService
amount_rubles = amount_kopeks / 100
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
payment_service = PaymentService(message.bot)
invoice_link = await payment_service.create_stars_invoice(
amount_kopeks=amount_kopeks,
@@ -374,7 +379,9 @@ async def process_stars_payment_amount(
await message.answer(
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
f"Сумма: {texts.format_price(amount_kopeks)}\n\n"
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
f"К оплате: {stars_amount} звезд\n"
f"📊 Курс: {settings.get_stars_rate():.2f}₽ за звезду\n\n"
f"Нажмите кнопку ниже для оплаты:",
reply_markup=keyboard,
parse_mode="HTML"
@@ -384,7 +391,7 @@ async def process_stars_payment_amount(
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
await message.answer(" Ошибка создания платежа")
await message.answer(" Ошибка создания платежа")
@error_handler
+3 -4
View File
@@ -19,7 +19,6 @@ async def show_main_menu(
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
from datetime import datetime
@@ -34,7 +33,6 @@ async def show_main_menu(
menu_text = texts.MAIN_MENU.format(
user_name=db_user.full_name,
balance=texts.format_price(db_user.balance_kopeks),
subscription_status=_get_subscription_status(db_user, texts)
)
@@ -45,7 +43,8 @@ async def show_main_menu(
is_admin=settings.is_admin(db_user.telegram_id),
has_had_paid_subscription=db_user.has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=db_user.balance_kopeks
)
)
await callback.answer()
@@ -109,7 +108,7 @@ async def handle_back_to_menu(
def _get_subscription_status(user: User, texts) -> str:
if not user.subscription:
return " Отсутствует"
return " Отсутствует"
subscription = user.subscription
current_time = datetime.utcnow()
+132
View File
@@ -0,0 +1,132 @@
import logging
from aiogram import Dispatcher, types, F
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import User
from app.services.payment_service import PaymentService
from app.external.telegram_stars import TelegramStarsService
from app.database.crud.user import get_user_by_telegram_id
logger = logging.getLogger(__name__)
async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
try:
logger.info(f"📋 Pre-checkout query от {query.from_user.id}: {query.total_amount} XTR, payload: {query.invoice_payload}")
if not query.invoice_payload or not query.invoice_payload.startswith("balance_"):
logger.warning(f"Невалидный payload: {query.invoice_payload}")
await query.answer(
ok=False,
error_message="Ошибка валидации платежа. Попробуйте еще раз."
)
return
try:
from app.database.database import get_db
async for db in get_db():
user = await get_user_by_telegram_id(db, query.from_user.id)
if not user:
logger.warning(f"Пользователь {query.from_user.id} не найден в БД")
await query.answer(
ok=False,
error_message="Пользователь не найден. Обратитесь в поддержку."
)
return
break
except Exception as db_error:
logger.error(f"Ошибка подключения к БД в pre_checkout_query: {db_error}")
await query.answer(
ok=False,
error_message="Техническая ошибка. Попробуйте позже."
)
return
await query.answer(ok=True)
logger.info(f"✅ Pre-checkout одобрен для пользователя {query.from_user.id}")
except Exception as e:
logger.error(f"Ошибка в pre_checkout_query: {e}", exc_info=True)
await query.answer(
ok=False,
error_message="Техническая ошибка. Попробуйте позже."
)
async def handle_successful_payment(
message: types.Message,
db: AsyncSession,
**kwargs
):
try:
payment = message.successful_payment
user_id = message.from_user.id
logger.info(
f"💳 Успешный Stars платеж от {user_id}: "
f"{payment.total_amount} XTR, "
f"payload: {payment.invoice_payload}, "
f"charge_id: {payment.telegram_payment_charge_id}"
)
user = await get_user_by_telegram_id(db, user_id)
if not user:
logger.error(f"Пользователь {user_id} не найден при обработке Stars платежа")
await message.answer(
"❌ Ошибка: пользователь не найден. Обратитесь в поддержку."
)
return
payment_service = PaymentService(message.bot)
success = await payment_service.process_stars_payment(
db=db,
user_id=user.id,
stars_amount=payment.total_amount,
payload=payment.invoice_payload,
telegram_payment_charge_id=payment.telegram_payment_charge_id
)
if success:
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount)
await message.answer(
f"🎉 <b>Платеж успешно обработан!</b>\n\n"
f"⭐ Потрачено звезд: {payment.total_amount}\n"
f"💰 Зачислено на баланс: {rubles_amount:.2f}\n"
f"🆔 ID транзакции: {payment.telegram_payment_charge_id[:8]}...\n\n"
f"Спасибо за пополнение! 🚀",
parse_mode="HTML"
)
logger.info(
f"✅ Stars платеж успешно обработан: "
f"пользователь {user.id}, {payment.total_amount} звезд → {rubles_amount:.2f}"
)
else:
logger.error(f"Ошибка обработки Stars платежа для пользователя {user.id}")
await message.answer(
"❌ Произошла ошибка при зачислении средств. "
"Обратитесь в поддержку, платеж будет проверен вручную."
)
except Exception as e:
logger.error(f"Ошибка в successful_payment: {e}", exc_info=True)
await message.answer(
"❌ Техническая ошибка при обработке платежа. "
"Обратитесь в поддержку для решения проблемы."
)
def register_stars_handlers(dp: Dispatcher):
dp.pre_checkout_query.register(
handle_pre_checkout_query,
F.currency == "XTR"
)
dp.message.register(
handle_successful_payment,
F.successful_payment
)
logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей")
+75 -30
View File
@@ -138,7 +138,6 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
await message.answer(
texts.MAIN_MENU.format(
user_name=user.full_name,
balance=texts.format_price(user.balance_kopeks),
subscription_status=_get_subscription_status(user, texts)
),
reply_markup=get_main_menu_keyboard(
@@ -146,7 +145,8 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
is_admin=settings.is_admin(user.telegram_id),
has_had_paid_subscription=user.has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=user.balance_kopeks
)
)
await state.clear()
@@ -380,6 +380,7 @@ async def process_referral_code_skip(
await complete_registration_from_callback(callback, state, db)
async def complete_registration_from_callback(
callback: types.CallbackQuery,
state: FSMContext,
@@ -405,21 +406,24 @@ async def complete_registration_from_callback(
subscription_is_active = existing_user.subscription.is_active
user_name = existing_user.full_name
user_telegram_id = existing_user.telegram_id
user_language = existing_user.language
has_had_paid_subscription = existing_user.has_had_paid_subscription
balance_kopeks = existing_user.balance_kopeks
try:
await callback.message.answer(
texts.MAIN_MENU.format(
user_name=user_name,
balance=texts.format_price(balance_kopeks),
subscription_status=_get_subscription_status(existing_user, texts)
),
reply_markup=get_main_menu_keyboard(
language=existing_user.language,
is_admin=settings.is_admin(existing_user.telegram_id),
has_had_paid_subscription=existing_user.has_had_paid_subscription,
language=user_language,
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
except Exception as e:
@@ -503,7 +507,6 @@ async def complete_registration_from_callback(
has_active_subscription = False
subscription_is_active = False
user_name = user.full_name
balance_kopeks = user.balance_kopeks
user_telegram_id = user.telegram_id
@@ -514,7 +517,6 @@ async def complete_registration_from_callback(
await callback.message.answer(
texts.MAIN_MENU.format(
user_name=user_name,
balance=texts.format_price(balance_kopeks),
subscription_status=_get_subscription_status_simple(texts)
),
reply_markup=get_main_menu_keyboard(
@@ -522,7 +524,8 @@ async def complete_registration_from_callback(
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
logger.info(f"✅ Главное меню отправлено для пользователя {user_telegram_id}")
@@ -532,24 +535,25 @@ async def complete_registration_from_callback(
balance_rubles = balance_kopeks / 100
await callback.message.answer(
f"Добро пожаловать, {user_name}!\n"
f"Баланс: {balance_rubles}\n"
f"Баланс: {balance_rubles:.2f}\n"
f"Подписка: Нет активной подписки",
reply_markup=get_main_menu_keyboard(
language=user_language,
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
logger.info(f"✅ Fallback главное меню отправлено для пользователя {user_telegram_id}")
except Exception as fallback_error:
logger.error(f" Критическая ошибка при отправке fallback меню: {fallback_error}")
logger.error(f" Критическая ошибка при отправке fallback меню: {fallback_error}")
try:
await callback.message.answer(f"Добро пожаловать, {user_name}! Регистрация завершена.")
logger.info(f"✅ Простое приветствие отправлено для пользователя {user_telegram_id}")
except Exception as final_error:
logger.error(f" Критическая ошибка при отправке простого сообщения: {final_error}")
logger.error(f" Критическая ошибка при отправке простого сообщения: {final_error}")
logger.info(f"✅ Регистрация завершена для пользователя: {user_telegram_id}")
@@ -578,24 +582,30 @@ async def complete_registration(
if existing_user.subscription:
subscription_is_active = existing_user.subscription.is_active
user_name = existing_user.full_name
user_telegram_id = existing_user.telegram_id
user_language = existing_user.language
has_had_paid_subscription = existing_user.has_had_paid_subscription
balance_kopeks = existing_user.balance_kopeks
try:
await message.answer(
texts.MAIN_MENU.format(
user_name=existing_user.full_name,
balance=texts.format_price(existing_user.balance_kopeks),
user_name=user_name,
subscription_status=_get_subscription_status(existing_user, texts)
),
reply_markup=get_main_menu_keyboard(
language=existing_user.language,
is_admin=settings.is_admin(existing_user.telegram_id),
has_had_paid_subscription=existing_user.has_had_paid_subscription,
language=user_language,
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
except Exception as e:
logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}")
await message.answer(f"Добро пожаловать, {existing_user.full_name}!")
await message.answer(f"Добро пожаловать, {user_name}!")
await state.clear()
return
@@ -674,7 +684,6 @@ async def complete_registration(
has_active_subscription = False
subscription_is_active = False
user_name = user.full_name
balance_kopeks = user.balance_kopeks
user_telegram_id = user.telegram_id
@@ -685,7 +694,6 @@ async def complete_registration(
await message.answer(
texts.MAIN_MENU.format(
user_name=user_name,
balance=texts.format_price(balance_kopeks),
subscription_status=_get_subscription_status_simple(texts)
),
reply_markup=get_main_menu_keyboard(
@@ -693,7 +701,8 @@ async def complete_registration(
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
logger.info(f"✅ Главное меню отправлено для пользователя {user_telegram_id}")
@@ -703,19 +712,20 @@ async def complete_registration(
balance_rubles = balance_kopeks / 100
await message.answer(
f"Добро пожаловать, {user_name}!\n"
f"Баланс: {balance_rubles}\n"
f"Баланс: {balance_rubles:.2f}\n"
f"Подписка: Нет активной подписки",
reply_markup=get_main_menu_keyboard(
language=user_language,
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
logger.info(f"✅ Fallback главное меню отправлено для пользователя {user_telegram_id}")
except Exception as fallback_error:
logger.error(f" Критическая ошибка при отправке fallback меню: {fallback_error}")
logger.error(f" Критическая ошибка при отправке fallback меню: {fallback_error}")
try:
await message.answer(f"Добро пожаловать, {user_name}! Регистрация завершена.")
logger.info(f"✅ Простое приветствие отправлено для пользователя {user_telegram_id}")
@@ -726,9 +736,44 @@ async def complete_registration(
def _get_subscription_status(user, texts):
if user.subscription and user.subscription.is_active:
return getattr(texts, 'SUBSCRIPTION_ACTIVE', 'Активна')
return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки')
if not user or not hasattr(user, 'subscription'):
return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки')
if not user.subscription:
return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки')
subscription = user.subscription
from datetime import datetime
current_time = datetime.utcnow()
if hasattr(subscription, 'end_date') and subscription.end_date <= current_time:
return f"🔴 Истекла\n📅 {subscription.end_date.strftime('%d.%m.%Y')}"
if hasattr(subscription, 'end_date'):
days_left = (subscription.end_date - current_time).days
else:
days_left = 0
is_trial = getattr(subscription, 'is_trial', False)
if is_trial:
if days_left > 1:
return f"🎁 Тестовая подписка\n📅 до {subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)"
elif days_left == 1:
return "🎁 Тестовая подписка\n⚠️ истекает завтра!"
else:
return "🎁 Тестовая подписка\n⚠️ истекает сегодня!"
else:
if days_left > 7:
return f"💎 Активна\n📅 до {subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)"
elif days_left > 1:
return f"💎 Активна\n⚠️ истекает через {days_left} дн."
elif days_left == 1:
return "💎 Активна\n⚠️ истекает завтра!"
else:
return "💎 Активна\n⚠️ истекает сегодня!"
def _get_subscription_status_simple(texts):
+328 -56
View File
@@ -8,7 +8,7 @@ import json
import os
from typing import Dict, List, Any, Tuple, Optional
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
from app.config import settings, PERIOD_PRICES, get_traffic_prices
from app.states import SubscriptionStates
from app.database.crud.subscription import (
get_subscription_by_user_id, create_trial_subscription,
@@ -43,6 +43,7 @@ from app.services.referral_service import process_referral_purchase
logger = logging.getLogger(__name__)
TRAFFIC_PRICES = get_traffic_prices()
async def show_subscription_info(
callback: types.CallbackQuery,
@@ -80,7 +81,7 @@ async def show_subscription_info(
if subscription.is_trial:
actual_status = "trial_active"
status_display = "Тестовая"
status_emoji = "🎁"
status_emoji = "🎯"
else:
actual_status = "paid_active"
status_display = "Активна"
@@ -116,26 +117,61 @@ async def show_subscription_info(
subscription_type = "Триал" if subscription.is_trial else "Платная"
if subscription.traffic_limit_gb == 0:
traffic_used_display = f"∞ (безлимит) / {subscription.traffic_used_gb:.1f} ГБ"
traffic_used_display = f"∞ (безлимит) | Использовано: {subscription.traffic_used_gb:.1f} ГБ"
else:
traffic_used_display = f"{subscription.traffic_used_gb:.1f} / {subscription.traffic_limit_gb} ГБ"
devices_used = await get_current_devices_count(db_user)
devices_used_str = ""
devices_list = []
devices_count = 0
try:
if db_user.remnawave_uuid:
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
async with service.api as api:
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
if response and 'response' in response:
devices_info = response['response']
devices_count = devices_info.get('total', 0)
devices_list = devices_info.get('devices', [])
devices_used_str = str(devices_count)
logger.info(f"Найдено {devices_count} устройств для пользователя {db_user.telegram_id}")
else:
logger.warning(f"Не удалось получить информацию об устройствах для {db_user.telegram_id}")
except Exception as e:
logger.error(f"Ошибка получения устройств для отображения: {e}")
devices_used_str = await get_current_devices_count(db_user)
servers_names = await get_servers_display_names(subscription.connected_squads)
servers_display = servers_names if servers_names else "Нет серверов"
message = f"""👤 {db_user.full_name}
💰 Баланс: {settings.format_price(db_user.balance_kopeks)}
📱 Подписка: {status_emoji} {status_display}{warning_text}
📱 Информация о подписке
🎭 Тип: {subscription_type}
📅 Действует до: {subscription.end_date.strftime("%d.%m.%Y %H:%M")}
Осталось: {time_left_text}
📈 Трафик: {traffic_used_display}
🌍 Серверы: {len(subscription.connected_squads)} стран
📱 Устройства: {devices_used} / {subscription.device_limit}"""
🌍 Серверы: {servers_display}
📱 Устройства: {devices_used_str} / {subscription.device_limit}"""
if devices_list and len(devices_list) > 0:
message += f"\n\n<blockquote>📱 <b>Подключенные устройства:</b>\n"
for device in devices_list[:5]:
platform = device.get('platform', 'Unknown')
device_model = device.get('deviceModel', 'Unknown')
device_info = f"{platform} - {device_model}"
if len(device_info) > 35:
device_info = device_info[:32] + "..."
message += f"{device_info}\n"
message += "</blockquote>"
if hasattr(subscription, 'subscription_url') and subscription.subscription_url:
if actual_status in ['trial_active', 'paid_active']:
@@ -154,6 +190,64 @@ async def show_subscription_info(
)
await callback.answer()
async def get_current_devices_detailed(db_user: User) -> dict:
try:
if not db_user.remnawave_uuid:
return {"count": 0, "devices": []}
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
async with service.api as api:
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
if response and 'response' in response:
devices_info = response['response']
total_devices = devices_info.get('total', 0)
devices_list = devices_info.get('devices', [])
return {
"count": total_devices,
"devices": devices_list[:5]
}
else:
return {"count": 0, "devices": []}
except Exception as e:
logger.error(f"Ошибка получения детальной информации об устройствах: {e}")
return {"count": 0, "devices": []}
async def get_servers_display_names(squad_uuids: List[str]) -> str:
"""
Получает отображаемые названия серверов по их UUID
"""
if not squad_uuids:
return "Нет серверов"
try:
countries = await _get_available_countries()
server_names = []
for uuid in squad_uuids:
for country in countries:
if country['uuid'] == uuid:
server_names.append(country['name'])
break
if not server_names:
return f"{len(squad_uuids)} стран"
if len(server_names) > 6:
displayed = ", ".join(server_names[:6])
remaining = len(server_names) - 6
return f"{displayed} и ещё {remaining}"
else:
return ", ".join(server_names)
except Exception as e:
logger.error(f"Ошибка получения названий серверов: {e}")
return f"{len(squad_uuids)} стран"
async def get_current_devices_count(db_user: User) -> str:
try:
if not db_user.remnawave_uuid:
@@ -181,7 +275,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
if subscription.is_trial:
return 0
from app.config import TRAFFIC_PRICES, PERIOD_PRICES, settings
from app.config import settings
from app.services.subscription_service import SubscriptionService
subscription_service = SubscriptionService()
@@ -197,8 +291,8 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
subscription.connected_squads, db
)
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
traffic_cost = settings.get_traffic_price(subscription.traffic_limit_gb)
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
@@ -212,7 +306,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
return total_cost
except Exception as e:
logger.error(f"⚠ Ошибка расчета стоимости подписки: {e}")
logger.error(f" Ошибка расчета стоимости подписки: {e}")
return 0
@@ -328,7 +422,7 @@ async def start_subscription_purchase(
initial_data = {
'period_days': None,
'countries': [],
'devices': 1,
'devices': settings.DEFAULT_DEVICE_LIMIT,
'total_price': 0
}
@@ -412,11 +506,11 @@ async def get_countries_price_by_uuids_fallback(country_uuids: List[str], db: As
total_price += price
prices_list.append(price)
else:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
except Exception:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
@@ -424,7 +518,7 @@ async def get_countries_price_by_uuids_fallback(country_uuids: List[str], db: As
except Exception as e:
logger.error(f"Ошибка fallback функции: {e}")
default_prices = [1000] * len(country_uuids)
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
async def handle_manage_country(
@@ -785,10 +879,14 @@ async def confirm_add_traffic(
texts = get_texts(db_user.language)
subscription = db_user.subscription
price = TRAFFIC_PRICES[traffic_gb]
price = settings.get_traffic_price(traffic_gb)
if price == 0 and traffic_gb != 0:
await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True)
return
if db_user.balance_kopeks < price:
await callback.answer(" Недостаточно средств на балансе", show_alert=True)
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
return
try:
@@ -798,7 +896,7 @@ async def confirm_add_traffic(
)
if not success:
await callback.answer(" Ошибка списания средств", show_alert=True)
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
if traffic_gb == 0:
@@ -853,6 +951,11 @@ async def confirm_add_traffic(
await callback.answer()
def update_traffic_prices():
from app.config import refresh_traffic_prices
refresh_traffic_prices()
logger.info("🔄 TRAFFIC_PRICES обновлены из конфигурации")
async def confirm_add_devices(
callback: types.CallbackQuery,
@@ -864,10 +967,20 @@ async def confirm_add_devices(
texts = get_texts(db_user.language)
subscription = db_user.subscription
new_total_devices = subscription.device_limit + devices_count
if settings.MAX_DEVICES_LIMIT > 0 and new_total_devices > settings.MAX_DEVICES_LIMIT:
await callback.answer(
f"⚠️ Превышен максимальный лимит устройств ({settings.MAX_DEVICES_LIMIT}). "
f"У вас: {subscription.device_limit}, добавляете: {devices_count}",
show_alert=True
)
return
price = devices_count * settings.PRICE_PER_DEVICE
if db_user.balance_kopeks < price:
await callback.answer(" Недостаточно средств на балансе", show_alert=True)
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
return
try:
@@ -877,7 +990,7 @@ async def confirm_add_devices(
)
if not success:
await callback.answer(" Ошибка списания средств", show_alert=True)
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
await add_subscription_devices(db, subscription, devices_count)
@@ -1146,12 +1259,19 @@ async def select_period(
data['total_price'] = PERIOD_PRICES[period_days]
if settings.is_traffic_fixed():
fixed_traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
fixed_traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
data['total_price'] += fixed_traffic_price
data['traffic_gb'] = settings.get_fixed_traffic_limit()
await state.set_data(data)
if settings.is_traffic_selectable():
available_packages = [pkg for pkg in settings.get_traffic_packages() if pkg['enabled']]
if not available_packages:
await callback.answer("⚠️ Пакеты трафика не настроены", show_alert=True)
return
await callback.message.edit_text(
texts.SELECT_TRAFFIC,
reply_markup=get_traffic_packages_keyboard(db_user.language)
@@ -1179,6 +1299,56 @@ async def select_period(
await callback.answer()
async def refresh_traffic_config():
try:
from app.config import refresh_traffic_prices
refresh_traffic_prices()
packages = settings.get_traffic_packages()
enabled_count = sum(1 for pkg in packages if pkg['enabled'])
logger.info(f"🔄 Конфигурация трафика обновлена: {enabled_count} активных пакетов")
for pkg in packages:
if pkg['enabled']:
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
logger.info(f" 📦 {gb_text}: {pkg['price']/100}")
return True
except Exception as e:
logger.error(f"⚠️ Ошибка обновления конфигурации трафика: {e}")
return False
async def get_traffic_packages_info() -> str:
try:
packages = settings.get_traffic_packages()
info_lines = ["📦 Настроенные пакеты трафика:"]
enabled_packages = [pkg for pkg in packages if pkg['enabled']]
disabled_packages = [pkg for pkg in packages if not pkg['enabled']]
if enabled_packages:
info_lines.append("\n✅ Активные:")
for pkg in enabled_packages:
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
info_lines.append(f"{gb_text}: {pkg['price']/100}")
if disabled_packages:
info_lines.append("\n❌ Отключенные:")
for pkg in disabled_packages:
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
info_lines.append(f"{gb_text}: {pkg['price']/100}")
info_lines.append(f"\n📊 Всего пакетов: {len(packages)}")
info_lines.append(f"🟢 Активных: {len(enabled_packages)}")
info_lines.append(f"🔴 Отключенных: {len(disabled_packages)}")
return "\n".join(info_lines)
except Exception as e:
return f"⚠️ Ошибка получения информации: {e}"
async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSession):
devices_used = await get_current_devices_count(db_user)
@@ -1257,7 +1427,10 @@ async def select_traffic(
data = await state.get_data()
data['traffic_gb'] = traffic_gb
data['total_price'] += TRAFFIC_PRICES[traffic_gb]
traffic_price = settings.get_traffic_price(traffic_gb)
data['total_price'] += traffic_price
await state.set_data(data)
if await _should_show_countries_management():
@@ -1288,7 +1461,6 @@ async def select_country(
db_user: User,
db: AsyncSession
):
country_uuid = callback.data.split('_')[1]
data = await state.get_data()
@@ -1300,7 +1472,7 @@ async def select_country(
countries = await _get_available_countries()
base_price = PERIOD_PRICES[data['period_days']] + TRAFFIC_PRICES[data['traffic_gb']]
base_price = PERIOD_PRICES[data['period_days']] + settings.get_traffic_price(data['traffic_gb'])
try:
subscription_service = SubscriptionService()
@@ -1360,7 +1532,7 @@ async def select_devices(
base_price = (
PERIOD_PRICES[data['period_days']] +
TRAFFIC_PRICES[data['traffic_gb']]
settings.get_traffic_price(data['traffic_gb'])
)
countries = await _get_available_countries()
@@ -1369,7 +1541,7 @@ async def select_devices(
if c['uuid'] in data['countries']
)
devices_price = (devices - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
data['devices'] = devices
data['total_price'] = base_price + countries_price + devices_price
@@ -1387,7 +1559,7 @@ async def devices_continue(
db: AsyncSession
):
if not callback.data == "devices_continue":
await callback.answer(" Некорректный запрос", show_alert=True)
await callback.answer("⚠️ Некорректный запрос", show_alert=True)
return
data = await state.get_data()
@@ -1410,21 +1582,23 @@ async def devices_continue(
base_price = PERIOD_PRICES[data['period_days']]
if settings.is_traffic_fixed():
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
final_traffic_gb = settings.get_fixed_traffic_limit()
else:
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
traffic_price = settings.get_traffic_price(data['traffic_gb'])
final_traffic_gb = data['traffic_gb']
devices_price = (data['devices'] - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_price = base_price + traffic_price + countries_price + devices_price
data['total_price'] = total_price
await state.set_data(data)
if settings.is_traffic_fixed():
if data['traffic_gb'] == 0:
if final_traffic_gb == 0:
traffic_display = "Безлимитный"
else:
traffic_display = f"{data['traffic_gb']} ГБ"
traffic_display = f"{final_traffic_gb} ГБ"
else:
if data['traffic_gb'] == 0:
traffic_display = "Безлимитный"
@@ -1474,13 +1648,13 @@ async def confirm_purchase(
countries_price += country['price_kopeks']
server_prices.append(country['price_kopeks'])
devices_price = (data['devices'] - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
if settings.is_traffic_fixed():
traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
final_traffic_gb = settings.get_fixed_traffic_limit()
else:
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
traffic_price = settings.get_traffic_price(data['traffic_gb'])
final_traffic_gb = data['traffic_gb']
final_price = base_price + traffic_price + countries_price + devices_price
@@ -1568,7 +1742,7 @@ async def confirm_purchase(
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
if not remnawave_user:
logger.error(f" Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
logger.error(f"⚠️ Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
logger.info(f"🔄 Fallback: принудительное создание нового RemnaWave пользователя")
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
@@ -1634,6 +1808,91 @@ async def confirm_purchase(
await state.clear()
await callback.answer()
async def add_traffic(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
if settings.is_traffic_fixed():
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
return
traffic_gb = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
subscription = db_user.subscription
price = settings.get_traffic_price(traffic_gb)
if price == 0 and traffic_gb != 0:
await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True)
return
if db_user.balance_kopeks < price:
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
return
try:
success = await subtract_user_balance(
db, db_user, price,
f"Добавление {traffic_gb} ГБ трафика"
)
if not success:
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
if traffic_gb == 0:
subscription.traffic_limit_gb = 0
else:
await add_subscription_traffic(db, subscription, traffic_gb)
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
await create_transaction(
db=db,
user_id=db_user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price,
description=f"Добавление {traffic_gb} ГБ трафика"
)
try:
await process_referral_purchase(
db=db,
user_id=db_user.id,
purchase_amount_kopeks=price,
transaction_id=None
)
except Exception as e:
logger.error(f"Ошибка обработки реферальной покупки: {e}")
await db.refresh(db_user)
await db.refresh(subscription)
success_text = f"✅ Трафик успешно добавлен!\n\n"
if traffic_gb == 0:
success_text += "🎉 Теперь у вас безлимитный трафик!"
else:
success_text += f"📈 Добавлено: {traffic_gb} ГБ\n"
success_text += f"Новый лимит: {texts.format_traffic(subscription.traffic_limit_gb)}"
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language)
)
logger.info(f"✅ Пользователь {db_user.telegram_id} добавил {traffic_gb} ГБ трафика")
except Exception as e:
logger.error(f"Ошибка добавления трафика: {e}")
await callback.message.edit_text(
texts.ERROR,
reply_markup=get_back_keyboard(db_user.language)
)
await callback.answer()
async def create_paid_subscription_with_traffic_mode(
db: AsyncSession,
user_id: int,
@@ -1666,6 +1925,15 @@ async def create_paid_subscription_with_traffic_mode(
return subscription
def validate_traffic_price(gb: int) -> bool:
from app.config import settings
price = settings.get_traffic_price(gb)
if gb == 0:
return True
return price > 0
async def handle_subscription_settings(
callback: types.CallbackQuery,
@@ -1885,7 +2153,7 @@ async def _get_available_countries():
countries.append({
"uuid": squad["uuid"],
"name": squad_name,
"price_kopeks": 1000,
"price_kopeks": 0,
"is_available": True
})
@@ -2243,7 +2511,7 @@ async def handle_connect_subscription(
subscription = db_user.subscription
if not subscription or not subscription.subscription_url:
await callback.answer(" У вас нет активной подписки или ссылка еще генерируется", show_alert=True)
await callback.answer(" У вас нет активной подписки или ссылка еще генерируется", show_alert=True)
return
connect_mode = settings.CONNECT_BUTTON_MODE
@@ -2252,13 +2520,10 @@ async def handle_connect_subscription(
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="🔗 Открыть подписку",
text="🔗 Открыть подписку в мини-приложении",
web_app=types.WebAppInfo(url=subscription.subscription_url)
)
],
[
InlineKeyboardButton(text="📋 Показать ссылку", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
@@ -2266,9 +2531,9 @@ async def handle_connect_subscription(
await callback.message.edit_text(
f"""
🔗 <b>Подключить подписку</b>
📱 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:
🚀 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:
""",
reply_markup=keyboard,
parse_mode="HTML"
@@ -2276,7 +2541,7 @@ async def handle_connect_subscription(
elif connect_mode == "miniapp_custom":
if not settings.MINIAPP_CUSTOM_URL:
await callback.answer(" Кастомная ссылка для мини-приложения не настроена", show_alert=True)
await callback.answer(" Кастомная ссылка для мини-приложения не настроена", show_alert=True)
return
keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -2286,9 +2551,6 @@ async def handle_connect_subscription(
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL)
)
],
[
InlineKeyboardButton(text="📋 Показать ссылку подписки", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
@@ -2299,9 +2561,6 @@ async def handle_connect_subscription(
🚀 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть приложение:
📋 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
""",
reply_markup=keyboard,
parse_mode="HTML"
@@ -2464,6 +2723,16 @@ async def handle_specific_app_guide(
)
await callback.answer()
async def handle_no_traffic_packages(
callback: types.CallbackQuery,
db_user: User
):
await callback.answer(
"⚠️ В данный момент нет доступных пакетов трафика. "
"Обратитесь в техподдержку для получения информации.",
show_alert=True
)
async def handle_open_subscription_link(
callback: types.CallbackQuery,
@@ -2575,6 +2844,7 @@ def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMa
def register_handlers(dp: Dispatcher):
update_traffic_prices()
dp.callback_query.register(
show_subscription_info,
@@ -2596,7 +2866,6 @@ def register_handlers(dp: Dispatcher):
F.data.in_(["menu_buy", "subscription_upgrade"])
)
dp.callback_query.register(
handle_add_countries,
F.data == "subscription_add_countries"
@@ -2652,7 +2921,6 @@ def register_handlers(dp: Dispatcher):
F.data == "confirm_reset_devices"
)
dp.callback_query.register(
select_period,
F.data.startswith("period_"),
@@ -2683,7 +2951,6 @@ def register_handlers(dp: Dispatcher):
SubscriptionStates.confirming_purchase
)
dp.callback_query.register(
handle_autopay_menu,
F.data == "subscription_autopay"
@@ -2765,3 +3032,8 @@ def register_handlers(dp: Dispatcher):
handle_subscription_settings,
F.data == "subscription_settings"
)
dp.callback_query.register(
handle_no_traffic_packages,
F.data == "no_traffic_packages"
)
+126 -41
View File
@@ -1,4 +1,5 @@
from typing import List, Optional
from aiogram import types
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
@@ -20,20 +21,29 @@ def get_main_menu_keyboard(
is_admin: bool = False,
has_had_paid_subscription: bool = False,
has_active_subscription: bool = False,
subscription_is_active: bool = False
subscription_is_active: bool = False,
balance_kopeks: int = 0
) -> InlineKeyboardMarkup:
texts = get_texts(language)
if settings.DEBUG:
print(f"DEBUG KEYBOARD: language={language}, is_admin={is_admin}, has_had_paid={has_had_paid_subscription}, has_active={has_active_subscription}, sub_active={subscription_is_active}")
print(f"DEBUG KEYBOARD: language={language}, is_admin={is_admin}, has_had_paid={has_had_paid_subscription}, has_active={has_active_subscription}, sub_active={subscription_is_active}, balance={balance_kopeks}")
if hasattr(texts, 'BALANCE_BUTTON') and balance_kopeks > 0:
balance_button_text = texts.BALANCE_BUTTON.format(balance=texts.format_price(balance_kopeks))
else:
balance_button_text = f"💰 Баланс: {texts.format_price(balance_kopeks)}"
keyboard = [
[
InlineKeyboardButton(text=texts.MENU_BALANCE, callback_data="menu_balance"),
InlineKeyboardButton(text=texts.MENU_SUBSCRIPTION, callback_data="menu_subscription")
InlineKeyboardButton(text=balance_button_text, callback_data="menu_balance")
]
]
if has_active_subscription and subscription_is_active:
keyboard.append([
InlineKeyboardButton(text=texts.MENU_SUBSCRIPTION, callback_data="menu_subscription")
])
show_trial = not has_had_paid_subscription and not has_active_subscription
@@ -107,15 +117,28 @@ def get_subscription_keyboard(
connect_mode = settings.CONNECT_BUTTON_MODE
if connect_mode == "miniapp_subscription":
button_text = "🚀 Открыть в мини-приложении"
keyboard.append([
InlineKeyboardButton(
text="🚀 Открыть в мини-приложении",
web_app=types.WebAppInfo(url=subscription.subscription_url)
)
])
elif connect_mode == "miniapp_custom":
button_text = "🚀 Открыть приложение"
if settings.MINIAPP_CUSTOM_URL:
keyboard.append([
InlineKeyboardButton(
text="🚀 Открыть приложение",
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL)
)
])
else:
keyboard.append([
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
])
else:
button_text = "🔗 Подключиться"
keyboard.append([
InlineKeyboardButton(text=button_text, callback_data="subscription_connect")
])
keyboard.append([
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
])
if not is_trial and subscription and subscription.days_left <= 3:
keyboard.append([
@@ -219,28 +242,57 @@ def get_subscription_period_keyboard(language: str = "ru") -> InlineKeyboardMark
def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
import logging
logger = logging.getLogger(__name__)
from app.config import settings
if settings.is_traffic_fixed():
return get_back_keyboard(language)
logger.info(f"🔍 RAW CONFIG: '{settings.TRAFFIC_PACKAGES_CONFIG}'")
all_packages = settings.get_traffic_packages()
logger.info(f"🔍 ALL PACKAGES: {all_packages}")
enabled_packages = [pkg for pkg in all_packages if pkg['enabled']]
disabled_packages = [pkg for pkg in all_packages if not pkg['enabled']]
logger.info(f"🔍 ENABLED: {len(enabled_packages)} packages")
logger.info(f"🔍 DISABLED: {len(disabled_packages)} packages")
for pkg in disabled_packages:
logger.info(f"🔍 DISABLED PACKAGE: {pkg['gb']}GB = {pkg['price']} kopeks, enabled={pkg['enabled']}")
texts = get_texts(language)
keyboard = []
packages = [
(5, texts.TRAFFIC_5GB),
(10, texts.TRAFFIC_10GB),
(25, texts.TRAFFIC_25GB),
(50, texts.TRAFFIC_50GB),
(100, texts.TRAFFIC_100GB),
(250, texts.TRAFFIC_250GB),
(0, texts.TRAFFIC_UNLIMITED)
]
traffic_packages = settings.get_traffic_packages()
for gb, text in packages:
for package in traffic_packages:
gb = package["gb"]
price = package["price"]
enabled = package["enabled"]
if not enabled:
continue
if gb == 0:
text = f"♾️ Безлимит - {settings.format_price(package['price'])}"
else:
text = f"📊 {gb} ГБ - {settings.format_price(package['price'])}"
keyboard.append([
InlineKeyboardButton(text=text, callback_data=f"traffic_{gb}")
])
if not keyboard:
keyboard.append([
InlineKeyboardButton(
text="⚠️ Пакеты трафика не настроены",
callback_data="no_traffic_packages"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="subscription_config_back")
@@ -248,7 +300,6 @@ def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_countries_keyboard(countries: List[dict], selected: List[str], language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -291,19 +342,31 @@ def get_devices_keyboard(current: int, language: str = "ru") -> InlineKeyboardMa
texts = get_texts(language)
keyboard = []
for devices in range(1, 6):
price = (devices - 1) * settings.PRICE_PER_DEVICE
price_text = f" (+{texts.format_price(price)})" if price > 0 else ""
start_devices = settings.DEFAULT_DEVICE_LIMIT
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 50
end_devices = min(max_devices + 1, start_devices + 10)
buttons = []
for devices in range(start_devices, end_devices):
price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
price_text = f" (+{texts.format_price(price)})" if price > 0 else " (вкл.)"
emoji = "" if devices == current else ""
device_word = _get_device_declension(devices)
button_text = f"{emoji} {devices}{price_text}"
keyboard.append([
buttons.append(
InlineKeyboardButton(
text=f"{emoji} {devices} {device_word}{price_text}",
text=button_text,
callback_data=f"devices_{devices}"
)
])
)
for i in range(0, len(buttons), 2):
if i + 1 < len(buttons):
keyboard.append([buttons[i], buttons[i + 1]])
else:
keyboard.append([buttons[i]])
keyboard.extend([
[InlineKeyboardButton(text="✅ Продолжить", callback_data="devices_continue")],
@@ -602,34 +665,48 @@ def get_add_traffic_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
packages = [
(5, f"📊 +5 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_5GB)}"),
(10, f"📊 +10 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_10GB)}"),
(25, f"📊 +25 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_25GB)}"),
(50, f"📊 +50 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_50GB)}"),
(100, f"📊 +100 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_100GB)}"),
(0, f"📊 Безлимит - {settings.format_price(settings.PRICE_TRAFFIC_UNLIMITED)}")
]
traffic_packages = settings.get_traffic_packages()
for gb, text in packages:
for package in traffic_packages:
gb = package["gb"]
price = package["price"]
enabled = package["enabled"]
if not enabled:
continue
if gb == 0:
text = f"📊 Безлимит - {settings.format_price(package['price'])}"
else:
text = f"📊 +{gb} ГБ - {settings.format_price(package['price'])}"
keyboard.append([
InlineKeyboardButton(text=text, callback_data=f"add_traffic_{gb}")
])
if not keyboard:
keyboard.append([
InlineKeyboardButton(
text="⚠️ Пакеты трафика не настроены",
callback_data="no_traffic_packages"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_add_devices_keyboard(current_devices: int, language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
max_devices = 10
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 100
for add_count in range(1, min(6, max_devices - current_devices + 1)):
max_add = min(5, max_devices - current_devices)
for add_count in range(1, max_add + 1):
price = add_count * settings.PRICE_PER_DEVICE
total_devices = current_devices + add_count
@@ -642,6 +719,14 @@ def get_add_devices_keyboard(current_devices: int, language: str = "ru") -> Inli
)
])
if max_add == 0:
keyboard.append([
InlineKeyboardButton(
text="⚠️ Достигнут максимум устройств",
callback_data="max_devices_reached"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
])
+6 -6
View File
@@ -105,19 +105,17 @@ class RussianTexts(Texts):
REFERRAL_CODE_APPLIED = "🎁 Реферальный код применен! Вы получите бонус после первой покупки."
REFERRAL_CODE_INVALID = "❌ Неверный реферальный код"
REFERRAL_CODE_SKIP = "⏭️ Пропустить"
MAIN_MENU = """👤 <b>{user_name}</b>
💰 <b>Баланс:</b> {balance}
📱 <b>Подписка:</b> {subscription_status}
Выберите действие:
"""
MENU_BALANCE = "💰 Баланс"
MENU_SUBSCRIPTION = "📱 Подписка"
MENU_TRIAL = "🎁 Тестовая подписка"
MENU_TRIAL = "🧪 Тестовая подписка"
MENU_BUY_SUBSCRIPTION = "💎 Купить подписку"
MENU_EXTEND_SUBSCRIPTION = "⏰ Продлить подписку"
MENU_PROMOCODE = "🎫 Промокод"
@@ -126,9 +124,11 @@ class RussianTexts(Texts):
MENU_RULES = "📋 Правила сервиса"
MENU_LANGUAGE = "🌐 Язык"
MENU_ADMIN = "⚙️ Админ-панель"
BALANCE_BUTTON = "💰 Баланс: {balance}"
BALANCE_BUTTON_ZERO = "💰 Баланс: 0.00 ₽"
SUBSCRIPTION_NONE = "❌ Нет активной подписки"
SUBSCRIPTION_TRIAL = "🎁 Тестовая подписка"
SUBSCRIPTION_TRIAL = "🧪 Тестовая подписка"
SUBSCRIPTION_ACTIVE = "✅ Активна"
SUBSCRIPTION_EXPIRED = "⏰ Истекла"
+69 -5
View File
@@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.services.yookassa_service import YooKassaService
from app.external.telegram_stars import TelegramStarsService
from app.database.crud.yookassa import create_yookassa_payment, link_yookassa_payment_to_transaction
from app.database.crud.transaction import create_transaction
from app.database.crud.user import add_user_balance, get_user_by_id
@@ -22,6 +23,7 @@ class PaymentService:
def __init__(self, bot: Optional[Bot] = None):
self.bot = bot
self.yookassa_service = YooKassaService() if settings.is_yookassa_enabled() else None
self.stars_service = TelegramStarsService(bot) if bot else None
async def create_stars_invoice(
self,
@@ -30,28 +32,90 @@ class PaymentService:
payload: Optional[str] = None
) -> str:
if not self.bot:
if not self.bot or not self.stars_service:
raise ValueError("Bot instance required for Stars payments")
try:
stars_amount = max(1, amount_kopeks // 100)
amount_rubles = amount_kopeks / 100
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
invoice_link = await self.bot.create_invoice_link(
title="Пополнение баланса VPN",
description=description,
description=f"{description} (≈{stars_amount} ⭐)",
payload=payload or f"balance_topup_{amount_kopeks}",
provider_token="",
currency="XTR",
prices=[LabeledPrice(label="Пополнение", amount=stars_amount)]
)
logger.info(f"Создан Stars invoice на {stars_amount} звезд")
logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{amount_rubles:.2f}₽)")
return invoice_link
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
raise
async def process_stars_payment(
self,
db: AsyncSession,
user_id: int,
stars_amount: int,
payload: str,
telegram_payment_charge_id: str
) -> bool:
try:
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(stars_amount)
amount_kopeks = int(rubles_amount * 100)
transaction = await create_transaction(
db=db,
user_id=user_id,
type=TransactionType.DEPOSIT,
amount_kopeks=amount_kopeks,
description=f"Пополнение через Telegram Stars ({stars_amount} ⭐)",
payment_method=PaymentMethod.TELEGRAM_STARS,
external_id=telegram_payment_charge_id,
is_completed=True
)
user = await get_user_by_id(db, user_id)
if user:
await add_user_balance(
db,
user,
amount_kopeks,
f"Пополнение Stars: {rubles_amount:.2f}₽ ({stars_amount} ⭐)"
)
if self.bot:
try:
await self.bot.send_message(
user.telegram_id,
f"✅ <b>Пополнение успешно!</b>\n\n"
f"⭐ Звезд: {stars_amount}\n"
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
f"🏦 Способ: Telegram Stars\n"
f"🆔 Транзакция: {telegram_payment_charge_id[:8]}...\n\n"
f"Баланс пополнен автоматически!",
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {rubles_amount:.2f}")
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
logger.info(
f"✅ Обработан Stars платеж: пользователь {user_id}, "
f"{stars_amount} звезд → {rubles_amount:.2f}"
)
return True
else:
logger.error(f"Пользователь с ID {user_id} не найден при обработке Stars платежа")
return False
except Exception as e:
logger.error(f"Ошибка обработки Stars платежа: {e}", exc_info=True)
return False
async def create_yookassa_payment(
self,
db: AsyncSession,
@@ -196,7 +260,7 @@ class PaymentService:
user.telegram_id,
f"✅ <b>Пополнение успешно!</b>\n\n"
f"💰 Сумма: {settings.format_price(updated_payment.amount_kopeks)}\n"
f"🏦 Способ: Банковская карта\n"
f"🦐 Способ: Банковская карта\n"
f"🆔 Транзакция: {yookassa_payment_id[:8]}...\n\n"
f"Баланс пополнен автоматически!",
parse_mode="HTML"
+2 -1
View File
@@ -25,7 +25,8 @@ class RemnaWaveService:
def __init__(self):
self.api = RemnaWaveAPI(
base_url=settings.REMNAWAVE_API_URL,
api_key=settings.REMNAWAVE_API_KEY
api_key=settings.REMNAWAVE_API_KEY,
secret_key=settings.REMNAWAVE_SECRET_KEY
)
def _parse_remnawave_date(self, date_str: str) -> datetime:
+25 -21
View File
@@ -229,36 +229,40 @@ class SubscriptionService:
devices: int,
db: AsyncSession
) -> Tuple[int, List[int]]:
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
from app.config import PERIOD_PRICES
from app.database.crud.server_squad import get_server_squad_by_id
if settings.MAX_DEVICES_LIMIT > 0 and devices > settings.MAX_DEVICES_LIMIT:
raise ValueError(f"Превышен максимальный лимит устройств: {settings.MAX_DEVICES_LIMIT}")
base_price = PERIOD_PRICES.get(period_days, 0)
traffic_price = TRAFFIC_PRICES.get(traffic_gb, 0)
traffic_price = settings.get_traffic_price(traffic_gb)
server_prices = []
total_servers_price = 0
for server_id in server_squad_ids:
server = await get_server_squad_by_id(db, server_id)
if server and server.is_available and not server.is_full:
server_prices.append(server.price_kopeks)
total_servers_price += server.price_kopeks
logger.debug(f"🏷️ Сервер {server.display_name}: {server.price_kopeks/100}")
logger.debug(f"Сервер {server.display_name}: {server.price_kopeks/100}")
else:
server_prices.append(0)
logger.warning(f"⚠️ Сервер ID {server_id} недоступен")
devices_price = max(0, devices - 1) * settings.PRICE_PER_DEVICE
logger.warning(f"Сервер ID {server_id} недоступен")
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_price = base_price + traffic_price + total_servers_price + devices_price
logger.info(f"💰 Расчет стоимости новой подписки:")
logger.info(f" 📅 Период {period_days} дней: {base_price/100}")
logger.info(f" 📊 Трафик {traffic_gb} ГБ: {traffic_price/100}")
logger.info(f" 🌍 Серверы ({len(server_squad_ids)}): {total_servers_price/100}")
logger.info(f" 📱 Устройства ({devices}): {devices_price/100}")
logger.info(f" 💎 ИТОГО: {total_price/100}")
logger.info(f"Расчет стоимости новой подписки:")
logger.info(f" Период {period_days} дней: {base_price/100}")
logger.info(f" Трафик {traffic_gb} ГБ: {traffic_price/100}")
logger.info(f" Серверы ({len(server_squad_ids)}): {total_servers_price/100}")
logger.info(f" Устройства ({devices}): {devices_price/100}")
logger.info(f" ИТОГО: {total_price/100}")
return total_price, server_prices
@@ -269,7 +273,7 @@ class SubscriptionService:
db: AsyncSession
) -> int:
try:
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
from app.config import PERIOD_PRICES
base_price = PERIOD_PRICES.get(period_days, 0)
@@ -277,9 +281,9 @@ class SubscriptionService:
subscription.connected_squads, db
)
devices_price = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
traffic_price = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
traffic_price = settings.get_traffic_price(subscription.traffic_limit_gb)
total_price = base_price + servers_price + devices_price + traffic_price
@@ -364,7 +368,7 @@ class SubscriptionService:
prices_list.append(price)
logger.debug(f"🏷️ Страна {server.display_name}: {price/100}")
else:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
logger.warning(f"⚠️ Сервер {country_uuid} недоступен, используем базовую цену: {default_price/100}")
@@ -374,7 +378,7 @@ class SubscriptionService:
except Exception as e:
logger.error(f"Ошибка получения цен стран: {e}")
default_prices = [1000] * len(country_uuids)
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
async def _get_countries_price(self, country_uuids: List[str], db: AsyncSession) -> int:
+96 -51
View File
@@ -2,7 +2,7 @@ import logging
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete
from sqlalchemy import delete, select, update
from app.database.crud.user import (
get_user_by_id, get_user_by_telegram_id, get_users_list,
@@ -13,7 +13,7 @@ from app.database.crud.transaction import get_user_transactions_count
from app.database.crud.subscription import get_subscription_by_user_id
from app.database.models import (
User, UserStatus, Subscription, Transaction, PromoCodeUse,
ReferralEarning, SubscriptionServer
ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory
)
from app.config import settings
@@ -242,9 +242,81 @@ class UserService:
from app.services.subscription_service import SubscriptionService
subscription_service = SubscriptionService()
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
logger.info(f" ✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
except Exception as e:
logger.error(f" Ошибка деактивации RemnaWave пользователя: {e}")
logger.warning(f" ⚠️ Ошибка деактивации RemnaWave: {e}")
try:
from app.database.models import YooKassaPayment
from sqlalchemy import select
yookassa_result = await db.execute(
select(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
)
yookassa_payments = yookassa_result.scalars().all()
if yookassa_payments:
logger.info(f" 🔄 Удаляем {len(yookassa_payments)} YooKassa платежей")
await db.execute(
delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
)
await db.flush()
logger.info(f" ✅ YooKassa платежи удалены")
except Exception as e:
logger.error(f" ❌ Ошибка удаления YooKassa платежей: {e}")
try:
transactions_result = await db.execute(
select(Transaction).where(Transaction.user_id == user_id)
)
transactions = transactions_result.scalars().all()
if transactions:
logger.info(f" 🔄 Удаляем {len(transactions)} транзакций")
await db.execute(
delete(Transaction).where(Transaction.user_id == user_id)
)
await db.flush()
logger.info(f" ✅ Транзакции удалены")
except Exception as e:
logger.error(f" ❌ Ошибка удаления транзакций: {e}")
try:
await db.execute(
delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалены использования промокодов пользователя {user_id}")
except Exception as e:
logger.error(f" ❌ Ошибка удаления использований промокодов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.user_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалены реферальные доходы пользователя {user_id}")
except Exception as e:
logger.error(f" ❌ Ошибка удаления реферальных доходов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.referral_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалены реферальные записи о пользователе {user_id}")
except Exception as e:
logger.error(f" ❌ Ошибка удаления реферальных записей: {e}")
try:
from app.database.models import BroadcastHistory
await db.execute(
delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалена история рассылок админа {user_id}")
except Exception as e:
logger.error(f" ❌ Ошибка удаления истории рассылок: {e}")
if user.subscription:
try:
@@ -253,74 +325,47 @@ class UserService:
SubscriptionServer.subscription_id == user.subscription.id
)
)
logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
await db.flush()
logger.info(f" 🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
logger.error(f" ❌ Ошибка удаления SubscriptionServer: {e}")
if user.subscription:
try:
await db.delete(user.subscription)
logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
from app.database.models import Subscription
await db.execute(
delete(Subscription).where(Subscription.user_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалена подписка пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления подписки: {e}")
try:
await db.execute(
delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id)
)
logger.info(f"🗑️ Удалены использования промокодов пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.user_id == user.id)
)
logger.info(f"🗑️ Удалены реферальные доходы пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.referral_id == user.id)
)
logger.info(f"🗑️ Удалены реферальные записи о пользователе {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления реферальных записей: {e}")
logger.error(f" ❌ Ошибка удаления подписки: {e}")
try:
from sqlalchemy import update
await db.execute(
referrals_result = await db.execute(
update(User)
.where(User.referred_by_id == user.id)
.where(User.referred_by_id == user_id)
.values(referred_by_id=None)
)
logger.info(f"🗑️ Очищены реферальные ссылки у рефералов пользователя {user_id}")
if referrals_result.rowcount > 0:
logger.info(f" 🔗 Очищены реферальные ссылки у {referrals_result.rowcount} рефералов")
await db.flush()
except Exception as e:
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
logger.error(f" ❌ Ошибка очистки реферальных ссылок: {e}")
try:
await db.execute(
delete(Transaction).where(Transaction.user_id == user.id)
delete(User).where(User.id == user_id)
)
logger.info(f"🗑️ Удалены транзакции пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления транзакций: {e}")
try:
user.status = UserStatus.DELETED.value
user.balance_kopeks = 0
user.remnawave_uuid = None
user.updated_at = datetime.utcnow()
await db.commit()
logger.info(f"✅ Пользователь {user_id} помечен как удаленный и обнулен")
logger.info(f" ✅ Пользователь {user_id} окончательно удален из базы")
except Exception as e:
logger.error(f"❌ Ошибка обновления статуса пользователя: {e}")
logger.error(f" ❌ Ошибка финального удаления пользователя: {e}")
await db.rollback()
return False
logger.info(f"✅ Пользователь {user_id} (Telegram ID: {user.telegram_id}) полностью удален админом {admin_id}")
logger.info(f"✅ Пользователь {user.telegram_id} (ID: {user_id}) полностью удален администратором {admin_id}")
return True
except Exception as e: