Compare commits

...

184 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
Egor 6df92bde2a Update README.md 2025-09-02 00:34:13 +03:00
Egor 60ff006fec Update README.md 2025-09-02 00:33:03 +03:00
Egor 0181f5e10c Update docker-compose.yml 2025-09-02 00:30:33 +03:00
Egor 4bbcc60bbb Update subscriptions.py 2025-09-02 00:16:07 +03:00
Egor 8e2138e2aa Update subscriptions.py 2025-09-02 00:14:45 +03:00
Egor 70b9e8c10d Update inline.py 2025-09-01 23:44:56 +03:00
Egor dc1eddb0df Update subscription.py 2025-09-01 23:44:02 +03:00
Egor 630551cc4b Update README.md 2025-09-01 23:17:46 +03:00
Egor 4f94b25182 Update docker-compose.yml 2025-09-01 22:50:17 +03:00
Egor d3b240eae3 Update Dockerfile 2025-09-01 22:49:41 +03:00
Egor e2883d6727 Update main.py 2025-09-01 22:29:47 +03:00
Egor 21757b37ab Update bot.py 2025-09-01 22:27:37 +03:00
Egor 9190857ffa Update maintenance_service.py 2025-09-01 22:27:08 +03:00
Egor a607443aa7 Update README.md 2025-09-01 19:59:39 +03:00
Egor 41969bc0c2 Update .env.example 2025-09-01 19:59:20 +03:00
Egor 0d443afb87 Update balance.py 2025-09-01 19:58:21 +03:00
Egor 3d85294d41 Update config.py 2025-09-01 19:55:23 +03:00
Egor 86896771a6 Update inline.py 2025-09-01 05:02:10 +03:00
Egor 42d52990fa Update README.md 2025-09-01 04:34:16 +03:00
Egor 6296c8257f Update README.md 2025-09-01 04:13:29 +03:00
Egor 14a40cb1b6 Create docker-compose.yml 2025-09-01 04:10:55 +03:00
Egor d0e3650de5 Update config.py 2025-09-01 03:52:56 +03:00
Egor df8ed666ae Update menu.py 2025-09-01 03:51:29 +03:00
Egor 2217b7f800 Update config.py 2025-09-01 03:46:13 +03:00
Egor 924febc2f3 Update menu.py 2025-09-01 03:43:27 +03:00
Egor 729ffb7952 Update messages.py 2025-09-01 01:06:54 +03:00
Egor 1568ca7633 Update README.md 2025-09-01 00:11:27 +03:00
Egor 7d5e476580 Update referral.py 2025-08-31 23:18:00 +03:00
Egor 9bcd187358 Update referral.py 2025-08-31 23:14:44 +03:00
Egor a9cbb6272f Update referral.py 2025-08-31 23:07:27 +03:00
Egor b524decfa7 Update referral.py 2025-08-31 23:04:00 +03:00
Egor 5ee01f7735 Update referral.py 2025-08-31 23:00:20 +03:00
Egor f46f2d6523 Update start.py 2025-08-31 22:45:54 +03:00
Egor 435dbf6cee Update auth.py 2025-08-31 22:42:02 +03:00
Egor 94d38eeb71 Update auth.py 2025-08-31 22:41:31 +03:00
Egor 0e7f6f90ed Update auth.py 2025-08-31 22:40:41 +03:00
Egor f3789287dc Update start.py 2025-08-31 22:40:04 +03:00
Egor a8312d7c10 Update promocode.py 2025-08-31 22:10:21 +03:00
Egor 612f4a7978 Update promocodes.py 2025-08-31 22:07:04 +03:00
Egor 2462347b5f Update promocode.py 2025-08-31 22:06:15 +03:00
Egor 3ec3c0d76b Update promocodes.py 2025-08-31 22:05:16 +03:00
Egor 8224aa98f4 Update promocodes.py 2025-08-31 22:02:00 +03:00
Egor bb9d9422fb Update promocode.py 2025-08-31 22:01:16 +03:00
Egor f3a184ff00 Update promocodes.py 2025-08-31 21:53:25 +03:00
Egor 866e4586df Update promocode.py 2025-08-31 21:52:18 +03:00
Egor a30647893d Update promocodes.py 2025-08-31 21:39:14 +03:00
Egor 73826312e5 Update promocodes.py 2025-08-31 19:08:16 +03:00
Egor dae34dc0ef Update promocodes.py 2025-08-31 18:59:39 +03:00
Egor 17d0c8c26f Update promocodes.py 2025-08-31 18:50:55 +03:00
Egor 67831dc162 Update promocodes.py 2025-08-31 18:45:16 +03:00
Egor ae84fbe141 Update promocodes.py 2025-08-31 18:36:54 +03:00
Egor 8c9bd890e8 Update promocodes.py 2025-08-31 18:35:57 +03:00
Egor e8c1956de5 Update promocodes.py 2025-08-31 18:28:06 +03:00
Egor 1540a41d3a Update .env.example 2025-08-31 17:40:08 +03:00
Egor 9cb61cb696 Update README.md 2025-08-31 17:39:52 +03:00
Egor 3511325285 Update auth.py 2025-08-31 16:18:42 +03:00
Egor 62507c5d28 Update auth.py 2025-08-31 16:10:28 +03:00
Egor 387df29a05 Update start.py 2025-08-31 15:40:31 +03:00
Egor 75dca7e1ad Update start.py 2025-08-31 15:32:25 +03:00
Egor 2d5e54d5ad Update README.md 2025-08-31 06:52:42 +03:00
Egor d62b2b0d6f Update README.md 2025-08-31 06:50:22 +03:00
Egor e273719ad1 Update README.md 2025-08-31 06:49:41 +03:00
Egor f6726f1c94 Update README.md 2025-08-31 06:15:32 +03:00
29 changed files with 2746 additions and 1417 deletions
+132 -83
View File
@@ -1,113 +1,120 @@
# TELEGRAM BOT CONFIGURATION
# ===============================================
# 🤖 REMNAWAVE BEDOLAGA BOT CONFIGURATION
# ===============================================
# ===== TELEGRAM BOT =====
BOT_TOKEN=
ADMIN_IDS=
SUPPORT_USERNAME=
SUPPORT_USERNAME=@support
# DATABASE CONFIGURATION
DATABASE_URL=sqlite+aiosqlite:///./bot.db
REDIS_URL=redis://localhost:6379/0
# ===== DATABASE =====
# Для Docker используйте PostgreSQL:
DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
# Для локального запуска без Docker используйте SQLite: sqlite+aiosqlite:///./bot.db
# DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
# REMNAWAVE API CONFIGURATION
REMNAWAVE_API_URL=
REDIS_URL=redis://redis:6379/0
# Пароли для Docker (PostgreSQL/Redis)
POSTGRES_DB=remnawave_bot
POSTGRES_USER=remnawave_user
POSTGRES_PASSWORD=secure_password_123
# ===== 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
# === NEW: Traffic Selection Mode Settings ===
# ========= ПОДПИСКИ =========
# ===== ТРИАЛ ПОДПИСКА =====
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_LIMIT_GB=10
TRIAL_DEVICE_LIMIT=1
TRIAL_SQUAD_UUID=
# ===== ПЛАТНАЯ ПОДПИСКА =====
# Сколько устройств доступно по дефолту при покупке платной подписки
DEFAULT_DEVICE_LIMIT=3
# Максимум устройств достопных к покупке (0 = Нет лимита)
MAX_DEVICES_LIMIT=15
# Дефолт параметры для подписок выданных через админку
DEFAULT_TRAFFIC_LIMIT_GB=100
# ===== ГЛОБАЛЬНЫЙ ПАРАМЕТР ДЛЯ ВСЕХ ПОДПИСОК =====
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
# ===== НАСТРОЙКИ ТРАФИКА =====
# Режим выбора трафика:
# "selectable" - пользователи выбирают пакеты трафика (по умолчанию)
# "fixed" - фиксированный лимит трафика для всех подписок, доступно 5/10/25/50/100/250/0 (0 безлимит) гб
# "fixed" - фиксированный лимит трафика для всех подписок
TRAFFIC_SELECTION_MODE=selectable
# Фиксированный лимит трафика в ГБ (используется только в режиме "fixed")
# 0 = безлимит
# для "fixed" обязательно должы быть проставлены цены на пакеты 5/10/25/50/100/250/0 можно постать 0 руб - будет беслпатно
FIXED_TRAFFIC_LIMIT_GB=0
FIXED_TRAFFIC_LIMIT_GB=100
# TRIAL SUBSCRIPTION SETTINGS
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_LIMIT_GB=10
TRIAL_DEVICE_LIMIT=2
TRIAL_SQUAD_UUID=
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
# === УПРАВЛЕНИЕ ПЕРИОДАМИ ПОДПИСКИ ===
# ===== ПЕРИОДЫ ПОДПИСКИ =====
# Доступные периоды подписки (через запятую)
# Возможные значения: 14,30,60,90,180,360
AVAILABLE_SUBSCRIPTION_PERIODS=14,30,60,90,180,360
AVAILABLE_SUBSCRIPTION_PERIODS=30,90,180
AVAILABLE_RENEWAL_PERIODS=30,90,180
# SUBSCRIPTION PRICING (в копейках для точности)
BASE_SUBSCRIPTION_PRICE=50000
# ===== ЦЕНЫ (в копейках) =====
BASE_SUBSCRIPTION_PRICE=0
PRICE_14_DAYS=5000
PRICE_30_DAYS=9900
PRICE_60_DAYS=18900
PRICE_90_DAYS=26900
PRICE_180_DAYS=49900
PRICE_360_DAYS=89900
# Цены за периоды
PRICE_14_DAYS=7000
PRICE_30_DAYS=9900
PRICE_60_DAYS=25900
PRICE_90_DAYS=36900
PRICE_180_DAYS=69900
PRICE_360_DAYS=109900
PRICE_TRAFFIC_5GB=2000
PRICE_TRAFFIC_10GB=4000
PRICE_TRAFFIC_25GB=6000
PRICE_TRAFFIC_50GB=10000
PRICE_TRAFFIC_100GB=15000
PRICE_TRAFFIC_250GB=20000
PRICE_TRAFFIC_UNLIMITED=25000
# Выводимые пакеты трафика и их цены в копейках
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
# REFERRAL SYSTEM SETTINGS
REFERRAL_REGISTRATION_REWARD=5000
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
REFERRAL_REGISTRATION_REWARD=10000
REFERRED_USER_REWARD=10000
REFERRAL_COMMISSION_PERCENT=25
# Режим работы кнопки "Подключиться"
# guide - открывает гайд подключения (режим 1)
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
CONNECT_BUTTON_MODE=miniapp_subscription
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
# MINIAPP_CUSTOM_URL=
# AUTO-PAYMENT SETTINGS
# ===== АВТОПРОДЛЕНИЕ =====
AUTOPAY_WARNING_DAYS=3,1
DEFAULT_AUTOPAY_DAYS_BEFORE=3
MIN_BALANCE_FOR_AUTOPAY_KOPEKS=10000
# MONITORING SETTINGS
MONITORING_INTERVAL=60
INACTIVE_USER_DELETE_MONTHS=3
# ===== ПЛАТЕЖНЫЕ СИСТЕМЫ =====
TRIAL_WARNING_HOURS=2
ENABLE_NOTIFICATIONS=true
NOTIFICATION_RETRY_ATTEMPTS=3
MONITORING_LOGS_RETENTION_DAYS=30
# PAYMENT SYSTEMS
# Telegram Stars (работает автоматически)
TELEGRAM_STARS_ENABLED=true
TELEGRAM_STARS_RATE_RUB=1.3
# Tribute (https://tribute.app)
TRIBUTE_ENABLED=false
TRIBUTE_API_KEY=
TRIBUTE_WEBHOOK_SECRET=your_webhook_secret
TRIBUTE_DONATE_LINK=https://t.me/tribute/app?startapp=XXXX
TRIBUTE_DONATE_LINK=
TRIBUTE_WEBHOOK_PATH=/tribute-webhook
TRIBUTE_WEBHOOK_PORT=8081
# === НОВЫЕ НАСТРОЙКИ YOOKASSA ===
# Включение/выключение YooKassa
# YooKassa (https://yookassa.ru)
YOOKASSA_ENABLED=false
# Основные настройки YooKassa (получить в личном кабинете)
YOOKASSA_SHOP_ID=your_shop_id_here
YOOKASSA_SECRET_KEY=your_secret_key_here
# URL для возврата после оплаты (необязательно, по умолчанию t.me/your_bot)
YOOKASSA_RETURN_URL=https://yourdomain.com/payment-success
# Email по умолчанию для чеков (если пользователь не указал свой)
YOOKASSA_SHOP_ID=
YOOKASSA_SECRET_KEY=
YOOKASSA_RETURN_URL=
YOOKASSA_DEFAULT_RECEIPT_EMAIL=receipts@yourdomain.com
# Настройки чеков для налоговой
YOOKASSA_VAT_CODE=1
# Коды НДС:
# 1 - НДС не облагается
# 2 - НДС 0%
# 2 - НДС 0%
# 3 - НДС 10%
# 4 - НДС 20%
# 5 - НДС 10/110
@@ -116,7 +123,7 @@ YOOKASSA_VAT_CODE=1
YOOKASSA_PAYMENT_MODE=full_payment
# Способы расчета:
# full_payment - полная оплата
# partial_payment - частичная оплата
# partial_payment - частичная оплата
# advance - аванс
# full_prepayment - полная предоплата
# partial_prepayment - частичная предоплата
@@ -126,7 +133,7 @@ YOOKASSA_PAYMENT_MODE=full_payment
YOOKASSA_PAYMENT_SUBJECT=service
# Предметы расчета:
# commodity - товар
# excise - подакцизный товар
# excise - подакцизный товар
# job - работа
# service - услуга
# gambling_bet - ставка в азартной игре
@@ -139,21 +146,63 @@ YOOKASSA_PAYMENT_SUBJECT=service
# composite - составной предмет расчета
# another - другое
# Webhook для получения уведомлений от YooKassa
# Webhook настройки
YOOKASSA_WEBHOOK_PATH=/yookassa-webhook
YOOKASSA_WEBHOOK_PORT=8082
YOOKASSA_WEBHOOK_SECRET=ваш_секретный_ключ_для_webhook
YOOKASSA_WEBHOOK_SECRET=your_webhook_secret
WEBHOOK_URL=https://example.com
# ===== НАСТРОЙКИ ОПИСАНИЙ ПЛАТЕЖЕЙ =====
# Эти настройки позволяют изменить описания платежей,
# чтобы избежать блокировок платежных систем
PAYMENT_SERVICE_NAME=Интернет-сервис
PAYMENT_BALANCE_DESCRIPTION=Пополнение баланса
PAYMENT_SUBSCRIPTION_DESCRIPTION=Оплата подписки
PAYMENT_BALANCE_TEMPLATE={service_name} - {description}
PAYMENT_SUBSCRIPTION_TEMPLATE={service_name} - {description}
# ===== ИНТЕРФЕЙС И UX =====
# Режим работы кнопки "Подключиться"
# guide - открывает гайд подключения (режим 1)
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
CONNECT_BUTTON_MODE=guide
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
MINIAPP_CUSTOM_URL=
# ===== МОНИТОРИНГ И УВЕДОМЛЕНИЯ =====
MONITORING_INTERVAL=60
INACTIVE_USER_DELETE_MONTHS=3
# Уведомления
TRIAL_WARNING_HOURS=2
ENABLE_NOTIFICATIONS=true
NOTIFICATION_RETRY_ATTEMPTS=3
MONITORING_LOGS_RETENTION_DAYS=30
NOTIFICATION_CACHE_HOURS=24
# ===== РЕЖИМ ТЕХНИЧЕСКИХ РАБОТ =====
MAINTENANCE_MODE=false
MAINTENANCE_CHECK_INTERVAL=30
MAINTENANCE_AUTO_ENABLE=true
MAINTENANCE_MESSAGE=Ведутся технические работы. Сервис временно недоступен. Попробуйте позже.
# ===== ЛОКАЛИЗАЦИЯ =====
DEFAULT_LANGUAGE=ru
AVAILABLE_LANGUAGES=ru,en
# ===== ЛОГИРОВАНИЕ =====
LOG_LEVEL=INFO
LOG_FILE=logs/bot.log
# ===== РАЗРАБОТКА =====
DEBUG=false
WEBHOOK_URL=
WEBHOOK_PATH=/webhook
# LOCALIZATION
DEFAULT_LANGUAGE=ru
AVAILABLE_LANGUAGES=ru
# LOGGING
LOG_LEVEL=INFO
LOG_FILE=/tmp/bot.log
# DEVELOPMENT
DEBUG=false
# ===== ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ =====
# Конфигурация приложений для гайда подключения
APP_CONFIG_PATH=app-config.json
ENABLE_DEEP_LINKS=true
APP_CONFIG_CACHE_TTL=3600
+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:
+18 -10
View File
@@ -1,35 +1,43 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
wget \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Create non-root user with specific UID/GID for consistency
RUN groupadd -g 1000 app && \
useradd -u 1000 -g 1000 -m -s /bin/bash app
# Copy application code
COPY . .
# Create logs directory
RUN mkdir -p logs
# Create necessary directories and set permissions
RUN mkdir -p logs data && \
chown -R app:app /app && \
chmod -R 755 /app && \
chmod -R 777 logs
# Switch to non-root user
USER app
# Set environment variables
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
# Create non-root user
RUN useradd --create-home --shell /bin/bash app
RUN chown -R app:app /app
USER app
# Expose webhook ports для платежных систем
# Expose webhook ports
EXPOSE 8081
EXPOSE 8082
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
# Run the application
CMD ["python", "main.py"]
+373 -859
View File
File diff suppressed because it is too large Load Diff
+8 -1
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__)
@@ -52,6 +53,9 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
)
maintenance_service.set_bot(bot)
logger.info("Бот установлен в maintenance_service")
try:
redis_client = redis.from_url(settings.REDIS_URL)
await redis_client.ping()
@@ -67,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())
@@ -95,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("Мониторинг техработ запущен")
@@ -107,7 +115,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
async def shutdown_bot():
"""Корректное завершение работы бота"""
try:
await maintenance_service.stop_monitoring()
logger.info("Мониторинг техработ остановлен")
+181 -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
@@ -93,6 +100,12 @@ class Settings(BaseSettings):
YOOKASSA_WEBHOOK_PATH: str = "/yookassa-webhook"
YOOKASSA_WEBHOOK_PORT: int = 8082
YOOKASSA_WEBHOOK_SECRET: Optional[str] = None
PAYMENT_BALANCE_DESCRIPTION: str = "Пополнение баланса"
PAYMENT_SUBSCRIPTION_DESCRIPTION: str = "Оплата подписки"
PAYMENT_SERVICE_NAME: str = "Интернет-сервис"
PAYMENT_BALANCE_TEMPLATE: str = "{service_name} - {description}"
PAYMENT_SUBSCRIPTION_TEMPLATE: str = "{service_name} - {description}"
CONNECT_BUTTON_MODE: str = "guide"
MINIAPP_CUSTOM_URL: str = ""
@@ -256,6 +269,155 @@ class Settings(BaseSettings):
except (ValueError, AttributeError):
return [30, 90, 180]
def get_balance_payment_description(self, amount_kopeks: int) -> str:
return self.PAYMENT_BALANCE_TEMPLATE.format(
service_name=self.PAYMENT_SERVICE_NAME,
description=f"{self.PAYMENT_BALANCE_DESCRIPTION} на {self.format_price(amount_kopeks)}"
)
def get_subscription_payment_description(self, period_days: int, amount_kopeks: int) -> str:
return self.PAYMENT_SUBSCRIPTION_TEMPLATE.format(
service_name=self.PAYMENT_SERVICE_NAME,
description=f"{self.PAYMENT_SUBSCRIPTION_DESCRIPTION} на {period_days} дней"
)
def get_custom_payment_description(self, description: str) -> str:
return self.PAYMENT_BALANCE_TEMPLATE.format(
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",
@@ -275,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()
+19 -5
View File
@@ -5,7 +5,7 @@ from sqlalchemy import select, and_, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database.models import PromoCode, PromoCodeUse, PromoCodeType
from app.database.models import PromoCode, PromoCodeUse, PromoCodeType, User
logger = logging.getLogger(__name__)
@@ -187,8 +187,13 @@ async def update_promocode(
async def delete_promocode(db: AsyncSession, promocode: PromoCode) -> bool:
try:
from sqlalchemy import delete as sql_delete
await db.execute(
sql_delete(PromoCodeUse).where(PromoCodeUse.promocode_id == promocode.id)
)
await db.delete(promocode)
await db.commit()
@@ -222,15 +227,24 @@ async def get_promocode_statistics(db: AsyncSession, promocode_id: int) -> dict:
today_uses = today_uses_result.scalar()
recent_uses_result = await db.execute(
select(PromoCodeUse)
select(PromoCodeUse, User)
.join(User, PromoCodeUse.user_id == User.id)
.where(PromoCodeUse.promocode_id == promocode_id)
.order_by(PromoCodeUse.used_at.desc())
.limit(10)
)
recent_uses = recent_uses_result.scalars().all()
recent_uses_data = recent_uses_result.all()
recent_uses = []
for use, user in recent_uses_data:
use.user_username = user.username
use.user_full_name = user.full_name
use.user_telegram_id = user.telegram_id
recent_uses.append(use)
return {
"total_uses": total_uses,
"today_uses": today_uses,
"recent_uses": recent_uses
}
}
+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)
+20 -11
View File
@@ -45,7 +45,8 @@ async def show_messages_menu(
await callback.message.edit_text(
text,
reply_markup=get_admin_messages_keyboard(db_user.language)
reply_markup=get_admin_messages_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
@@ -60,7 +61,8 @@ async def show_broadcast_targets(
await callback.message.edit_text(
"🎯 <b>Выбор целевой аудитории</b>\n\n"
"Выберите категорию пользователей для рассылки:",
reply_markup=get_broadcast_target_keyboard(db_user.language)
reply_markup=get_broadcast_target_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
@@ -118,7 +120,8 @@ async def show_messages_history(
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
parse_mode="HTML"
)
await callback.answer()
@@ -135,7 +138,7 @@ async def show_custom_broadcast(
stats = await get_users_statistics(db)
text = f"""
🔍 <b>Рассылка по критериям</b>
📝 <b>Рассылка по критериям</b>
📊 <b>Доступные фильтры:</b>
@@ -158,7 +161,8 @@ async def show_custom_broadcast(
await callback.message.edit_text(
text,
reply_markup=get_custom_criteria_keyboard(db_user.language)
reply_markup=get_custom_criteria_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
@@ -196,7 +200,8 @@ async def select_custom_criteria(
f"<i>Поддерживается HTML разметка</i>",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="admin_messages")]
])
]),
parse_mode="HTML"
)
await state.set_state(AdminStates.waiting_for_broadcast_message)
@@ -233,7 +238,8 @@ async def select_broadcast_target(
f"<i>Поддерживается HTML разметка</i>",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="admin_messages")]
])
]),
parse_mode="HTML"
)
await state.set_state(AdminStates.waiting_for_broadcast_message)
@@ -284,7 +290,8 @@ async def process_broadcast_message(
await message.answer(
preview_text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
parse_mode="HTML"
)
await state.set_state(AdminStates.confirming_broadcast)
@@ -304,7 +311,8 @@ async def confirm_broadcast(
await callback.message.edit_text(
"📨 Начинаю рассылку...\n\n"
"⏳ Это может занять несколько минут.",
reply_markup=None
reply_markup=None,
parse_mode="HTML"
)
if target.startswith('custom_'):
@@ -367,7 +375,8 @@ async def confirm_broadcast(
result_text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="📨 К рассылкам", callback_data="admin_messages")]
])
]),
parse_mode="HTML"
)
await state.clear()
@@ -548,4 +557,4 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_custom_broadcast, F.data == "admin_msg_custom")
dp.callback_query.register(select_custom_criteria, F.data.startswith("criteria_"))
dp.message.register(process_broadcast_message, AdminStates.waiting_for_broadcast_message)
dp.message.register(process_broadcast_message, AdminStates.waiting_for_broadcast_message)
+547 -110
View File
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.states import AdminStates
from app.database.models import User, PromoCodeType
from app.database.models import PromoCode, PromoCodeUse, PromoCodeType, User
from app.keyboards.admin import (
get_admin_promocodes_keyboard, get_promocode_type_keyboard,
get_admin_pagination_keyboard, get_confirmation_keyboard
@@ -77,6 +77,7 @@ async def show_promocodes_list(
return
text = f"🎫 <b>Список промокодов</b> (стр. {page}/{total_pages})\n\n"
keyboard = []
for promo in promocodes:
status_emoji = "" if promo.is_active else ""
@@ -93,9 +94,14 @@ async def show_promocodes_list(
if promo.valid_until:
text += f"⏰ До: {format_datetime(promo.valid_until)}\n"
text += f"🔧 Управление: /promo_{promo.id}\n\n"
keyboard = []
keyboard.append([
types.InlineKeyboardButton(
text=f"🎫 {promo.code}",
callback_data=f"promo_manage_{promo.id}"
)
])
text += "\n"
if total_pages > 1:
pagination_row = get_admin_pagination_keyboard(
@@ -182,128 +188,75 @@ async def show_promocode_management(
)
await callback.answer()
@admin_required
@error_handler
async def toggle_promocode_status(
async def show_promocode_edit_menu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
new_status = not promo.is_active
await update_promocode(db, promo, is_active=new_status)
status_text = "активирован" if new_status else "деактивирован"
await callback.answer(f"✅ Промокод {status_text}", show_alert=True)
await show_promocode_management(callback, db_user, db)
@admin_required
@error_handler
async def confirm_delete_promocode(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
text = f"""
<b>Подтверждение удаления</b>
<b>Редактирование промокода</b> <code>{promo.code}</code>
Вы действительно хотите удалить промокод <code>{promo.code}</code>?
<b>Внимание:</b> Это действие нельзя отменить!
💰 <b>Текущие параметры:</b>
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
if promo.type == PromoCodeType.BALANCE.value:
text += f"• Бонус: {settings.format_price(promo.balance_bonus_kopeks)}\n"
elif promo.type in [PromoCodeType.SUBSCRIPTION_DAYS.value, PromoCodeType.TRIAL_SUBSCRIPTION.value]:
text += f"• Дней: {promo.subscription_days}\n"
text += f"• Использований: {promo.current_uses}/{promo.max_uses}\n"
if promo.valid_until:
text += f"• До: {format_datetime(promo.valid_until)}\n"
else:
text += f"• Срок: бессрочно\n"
text += f"\nВыберите параметр для изменения:"
keyboard = [
[
types.InlineKeyboardButton(
text=" Да, удалить",
callback_data=f"promo_delete_confirm_{promo.id}"
),
text="📅 Дата окончания",
callback_data=f"promo_edit_date_{promo.id}"
)
],
[
types.InlineKeyboardButton(
text="❌ Отмена",
callback_data=f"promo_manage_{promo.id}"
text="📊 Количество использований",
callback_data=f"promo_edit_uses_{promo.id}"
)
]
])
]
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
@admin_required
@error_handler
async def delete_promocode_confirmed(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
if promo.type == PromoCodeType.BALANCE.value:
keyboard.insert(1, [
types.InlineKeyboardButton(
text="💰 Сумма бонуса",
callback_data=f"promo_edit_amount_{promo.id}"
)
])
elif promo.type in [PromoCodeType.SUBSCRIPTION_DAYS.value, PromoCodeType.TRIAL_SUBSCRIPTION.value]:
keyboard.insert(1, [
types.InlineKeyboardButton(
text="📅 Количество дней",
callback_data=f"promo_edit_days_{promo.id}"
)
])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
code = promo.code
success = await delete_promocode(db, promo)
if success:
await callback.answer(f"✅ Промокод {code} удален", show_alert=True)
await show_promocodes_list(callback, db_user, db)
else:
await callback.answer("❌ Ошибка удаления промокода", show_alert=True)
@admin_required
@error_handler
async def show_promocode_stats(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
stats = await get_promocode_statistics(db, promo_id)
text = f"""
📊 <b>Статистика промокода</b> <code>{promo.code}</code>
📈 <b>Общая статистика:</b>
- Всего использований: {stats['total_uses']}
- Использований сегодня: {stats['today_uses']}
- Осталось использований: {promo.max_uses - promo.current_uses}
📅 <b>Последние использования:</b>
"""
for use in stats['recent_uses'][:5]:
use_date = format_datetime(use.used_at)
text += f"- {use_date} (ID: {use.user_id})\n"
if not stats['recent_uses']:
text += "- Пока не было использований\n"
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
keyboard.extend([
[
types.InlineKeyboardButton(
text="⬅️ Назад",
@@ -312,7 +265,160 @@ async def show_promocode_stats(
]
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def start_edit_promocode_date(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
await state.update_data(
editing_promo_id=promo_id,
edit_action="date"
)
text = f"""
📅 <b>Изменение даты окончания промокода</b>
Введите количество дней до окончания (от текущего момента):
• Введите <b>0</b> для бессрочного промокода
• Введите положительное число для установки срока
<i>Например: 30 (промокод будет действовать 30 дней)</i>
ID промокода: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await state.set_state(AdminStates.setting_promocode_expiry)
await callback.answer()
@admin_required
@error_handler
async def start_edit_promocode_amount(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
await state.update_data(
editing_promo_id=promo_id,
edit_action="amount"
)
text = f"""
💰 <b>Изменение суммы бонуса промокода</b>
Введите новую сумму в рублях:
<i>Например: 500</i>
ID промокода: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await state.set_state(AdminStates.setting_promocode_value)
await callback.answer()
@admin_required
@error_handler
async def start_edit_promocode_days(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
# ИСПРАВЛЕНИЕ: берем последний элемент как ID
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
await state.update_data(
editing_promo_id=promo_id,
edit_action="days"
)
text = f"""
📅 <b>Изменение количества дней подписки</b>
Введите новое количество дней:
<i>Например: 30</i>
ID промокода: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await state.set_state(AdminStates.setting_promocode_value)
await callback.answer()
@admin_required
@error_handler
async def start_edit_promocode_uses(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
await state.update_data(
editing_promo_id=promo_id,
edit_action="uses"
)
text = f"""
📊 <b>Изменение максимального количества использований</b>
Введите новое количество использований:
• Введите <b>0</b> для безлимитных использований
• Введите положительное число для ограничения
<i>Например: 100</i>
ID промокода: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await state.set_state(AdminStates.setting_promocode_uses)
await callback.answer()
@@ -375,7 +481,6 @@ async def process_promocode_code(
await message.answer("❌ Код должен содержать только латинские буквы и цифры (3-20 символов)")
return
from app.database.crud.promocode import get_promocode_by_code
existing = await get_promocode_by_code(db, code)
if existing:
await message.answer("❌ Промокод с таким кодом уже существует")
@@ -411,12 +516,18 @@ async def process_promocode_code(
async def process_promocode_value(
message: types.Message,
db_user: User,
state: FSMContext
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
if data.get('editing_promo_id'):
await handle_edit_value(message, db_user, state, db)
return
try:
value = int(message.text.strip())
data = await state.get_data()
promo_type = data.get('promocode_type')
if promo_type == "balance" and (value < 1 or value > 10000):
@@ -437,13 +548,72 @@ async def process_promocode_value(
await message.answer("❌ Введите корректное число")
async def handle_edit_value(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
promo_id = data.get('editing_promo_id')
edit_action = data.get('edit_action')
promo = await db.get(PromoCode, promo_id)
if not promo:
await message.answer("❌ Промокод не найден")
await state.clear()
return
try:
value = int(message.text.strip())
if edit_action == "amount":
if value < 1 or value > 10000:
await message.answer("❌ Сумма должна быть от 1 до 10,000 рублей")
return
await update_promocode(db, promo, balance_bonus_kopeks=value * 100)
await message.answer(
f"✅ Сумма бонуса изменена на {value}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
])
)
elif edit_action == "days":
if value < 1 or value > 3650:
await message.answer("❌ Количество дней должно быть от 1 до 3650")
return
await update_promocode(db, promo, subscription_days=value)
await message.answer(
f"✅ Количество дней изменено на {value}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
])
)
await state.clear()
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: {edit_action} = {value}")
except ValueError:
await message.answer("❌ Введите корректное число")
@admin_required
@error_handler
async def process_promocode_uses(
message: types.Message,
db_user: User,
state: FSMContext
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
if data.get('editing_promo_id'):
await handle_edit_uses(message, db_user, state, db)
return
try:
max_uses = int(message.text.strip())
@@ -465,6 +635,54 @@ async def process_promocode_uses(
await message.answer("❌ Введите корректное число")
async def handle_edit_uses(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
promo_id = data.get('editing_promo_id')
promo = await db.get(PromoCode, promo_id)
if not promo:
await message.answer("❌ Промокод не найден")
await state.clear()
return
try:
max_uses = int(message.text.strip())
if max_uses < 0 or max_uses > 100000:
await message.answer("❌ Количество использований должно быть от 0 до 100,000")
return
if max_uses == 0:
max_uses = 999999
if max_uses < promo.current_uses:
await message.answer(
f"❌ Новый лимит ({max_uses}) не может быть меньше текущих использований ({promo.current_uses})"
)
return
await update_promocode(db, promo, max_uses=max_uses)
uses_text = "безлимитное" if max_uses == 999999 else str(max_uses)
await message.answer(
f"✅ Максимальное количество использований изменено на {uses_text}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
])
)
await state.clear()
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: max_uses = {max_uses}")
except ValueError:
await message.answer("❌ Введите корректное число")
@admin_required
@error_handler
async def process_promocode_expiry(
@@ -473,6 +691,12 @@ async def process_promocode_expiry(
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
if data.get('editing_promo_id'):
await handle_edit_expiry(message, db_user, state, db)
return
try:
expiry_days = int(message.text.strip())
@@ -480,7 +704,6 @@ async def process_promocode_expiry(
await message.answer("❌ Срок действия должен быть от 0 до 3650 дней")
return
data = await state.get_data()
code = data.get('promocode_code')
promo_type = data.get('promocode_type')
value = data.get('promocode_value', 0)
@@ -544,6 +767,209 @@ async def process_promocode_expiry(
await message.answer("❌ Введите корректное число дней")
async def handle_edit_expiry(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
promo_id = data.get('editing_promo_id')
promo = await db.get(PromoCode, promo_id)
if not promo:
await message.answer("❌ Промокод не найден")
await state.clear()
return
try:
expiry_days = int(message.text.strip())
if expiry_days < 0 or expiry_days > 3650:
await message.answer("❌ Срок действия должен быть от 0 до 3650 дней")
return
valid_until = None
if expiry_days > 0:
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
await update_promocode(db, promo, valid_until=valid_until)
if valid_until:
expiry_text = f"до {format_datetime(valid_until)}"
else:
expiry_text = "бессрочно"
await message.answer(
f"✅ Срок действия промокода изменен: {expiry_text}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
])
)
await state.clear()
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: expiry = {expiry_days} дней")
except ValueError:
await message.answer("❌ Введите корректное число дней")
@admin_required
@error_handler
async def toggle_promocode_status(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
new_status = not promo.is_active
await update_promocode(db, promo, is_active=new_status)
status_text = "активирован" if new_status else "деактивирован"
await callback.answer(f"✅ Промокод {status_text}", show_alert=True)
await show_promocode_management(callback, db_user, db)
@admin_required
@error_handler
async def confirm_delete_promocode(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
text = f"""
⚠️ <b>Подтверждение удаления</b>
Вы действительно хотите удалить промокод <code>{promo.code}</code>?
📊 <b>Информация о промокоде:</b>
• Использований: {promo.current_uses}/{promo.max_uses}
• Статус: {'Активен' if promo.is_active else 'Неактивен'}
<b>⚠️ Внимание:</b> Это действие нельзя отменить!
ID: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"promo_delete_confirm_{promo.id}"
),
types.InlineKeyboardButton(
text="❌ Отмена",
callback_data=f"promo_manage_{promo.id}"
)
]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
@admin_required
@error_handler
async def delete_promocode_confirmed(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
code = promo.code
success = await delete_promocode(db, promo)
if success:
await callback.answer(f"✅ Промокод {code} удален", show_alert=True)
await show_promocodes_list(callback, db_user, db)
else:
await callback.answer("❌ Ошибка удаления промокода", show_alert=True)
@admin_required
@error_handler
async def show_promocode_stats(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
stats = await get_promocode_statistics(db, promo_id)
text = f"""
📊 <b>Статистика промокода</b> <code>{promo.code}</code>
📈 <b>Общая статистика:</b>
- Всего использований: {stats['total_uses']}
- Использований сегодня: {stats['today_uses']}
- Осталось использований: {promo.max_uses - promo.current_uses}
📅 <b>Последние использования:</b>
"""
if stats['recent_uses']:
for use in stats['recent_uses'][:5]:
use_date = format_datetime(use.used_at)
if hasattr(use, 'user_username') and use.user_username:
user_display = f"@{use.user_username}"
elif hasattr(use, 'user_full_name') and use.user_full_name:
user_display = use.user_full_name
elif hasattr(use, 'user_telegram_id'):
user_display = f"ID{use.user_telegram_id}"
else:
user_display = f"ID{use.user_id}"
text += f"- {use_date} | {user_display}\n"
else:
text += "- Пока не было использований\n"
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[
types.InlineKeyboardButton(
text="⬅️ Назад",
callback_data=f"promo_manage_{promo.id}"
)
]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_promocodes_menu, F.data == "admin_promocodes")
dp.callback_query.register(show_promocodes_list, F.data == "admin_promo_list")
@@ -552,11 +978,22 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_promocode_management, F.data.startswith("promo_manage_"))
dp.callback_query.register(toggle_promocode_status, F.data.startswith("promo_toggle_"))
dp.callback_query.register(confirm_delete_promocode, F.data.startswith("promo_delete_"))
dp.callback_query.register(delete_promocode_confirmed, F.data.startswith("promo_delete_confirm_"))
dp.callback_query.register(show_promocode_stats, F.data.startswith("promo_stats_"))
dp.callback_query.register(start_edit_promocode_date, F.data.startswith("promo_edit_date_"))
dp.callback_query.register(start_edit_promocode_amount, F.data.startswith("promo_edit_amount_"))
dp.callback_query.register(start_edit_promocode_days, F.data.startswith("promo_edit_days_"))
dp.callback_query.register(start_edit_promocode_uses, F.data.startswith("promo_edit_uses_"))
dp.callback_query.register(
show_promocode_edit_menu,
F.data.regexp(r"^promo_edit_\d+$")
)
dp.callback_query.register(delete_promocode_confirmed, F.data.startswith("promo_delete_confirm_"))
dp.callback_query.register(confirm_delete_promocode, F.data.startswith("promo_delete_"))
dp.message.register(process_promocode_code, AdminStates.creating_promocode)
dp.message.register(process_promocode_value, AdminStates.setting_promocode_value)
dp.message.register(process_promocode_uses, AdminStates.setting_promocode_uses)
dp.message.register(process_promocode_expiry, AdminStates.setting_promocode_expiry)
dp.message.register(process_promocode_expiry, AdminStates.setting_promocode_expiry)
+9 -10
View File
@@ -306,13 +306,13 @@ async def show_pricing_settings(
"""
keyboard = [
[
types.InlineKeyboardButton(text="📅 Периоды", callback_data="admin_edit_period_prices"),
types.InlineKeyboardButton(text="📈 Трафик", callback_data="admin_edit_traffic_prices")
],
[
types.InlineKeyboardButton(text="📱 Устройства", callback_data="admin_edit_device_price")
],
# [
# types.InlineKeyboardButton(text="📅 Периоды", callback_data="admin_edit_period_prices"),
# types.InlineKeyboardButton(text="📈 Трафик", callback_data="admin_edit_traffic_prices")
# ],
# [
# types.InlineKeyboardButton(text="📱 Устройства", callback_data="admin_edit_device_price")
# ],
[
types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")
]
@@ -405,8 +405,7 @@ async def show_countries_management(
keyboard = [
[
types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_countries"),
types.InlineKeyboardButton(text="⚙️ API настройки", callback_data="admin_rw_api")
types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_countries")
],
[
types.InlineKeyboardButton(text="📊 Статистика нод", callback_data="admin_rw_nodes"),
@@ -495,4 +494,4 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
handle_subscriptions_pagination,
F.data.startswith("admin_subs_list_page_")
)
)
+11 -4
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
@@ -412,7 +419,7 @@ async def process_yookassa_payment_amount(
db=db,
user_id=db_user.id,
amount_kopeks=amount_kopeks,
description=f"Пополнение баланса VPN на {settings.format_price(amount_kopeks)}",
description=settings.get_balance_payment_description(amount_kopeks),
receipt_email=None,
receipt_phone=None,
metadata={
+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()
+2 -27
View File
@@ -85,13 +85,7 @@ async def create_invite_message(
[
types.InlineKeyboardButton(
text="📤 Поделиться",
switch_inline_query=invite_text
)
],
[
types.InlineKeyboardButton(
text="📋 Скопировать ссылку",
callback_data="copy_referral_link"
switch_inline_query=invite_text
)
],
[
@@ -104,7 +98,7 @@ async def create_invite_message(
await callback.message.edit_text(
f"📝 <b>Приглашение создано!</b>\n\n"
f"Используйте кнопку ниже для отправки приглашения или скопируйте текст:\n\n"
f"Нажмите кнопку «📤 Поделиться» чтобы отправить приглашение в любой чат, или скопируйте текст ниже:\n\n"
f"<code>{invite_text}</code>",
reply_markup=keyboard,
parse_mode="HTML"
@@ -112,20 +106,6 @@ async def create_invite_message(
await callback.answer()
async def copy_referral_link(
callback: types.CallbackQuery,
db_user: User
):
bot_username = (await callback.bot.get_me()).username
referral_link = f"https://t.me/{bot_username}?start={db_user.referral_code}"
await callback.answer(
f"Ссылка скопирована: {referral_link}",
show_alert=True
)
def register_handlers(dp: Dispatcher):
dp.callback_query.register(
@@ -137,8 +117,3 @@ def register_handlers(dp: Dispatcher):
create_invite_message,
F.data == "referral_create_invite"
)
dp.callback_query.register(
copy_referral_link,
F.data == "copy_referral_link"
)
+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 платежей")
+242 -25
View File
@@ -1,4 +1,5 @@
import logging
from datetime import datetime
from aiogram import Dispatcher, types, F
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
@@ -20,6 +21,62 @@ from app.utils.user_utils import generate_unique_referral_code
logger = logging.getLogger(__name__)
async def handle_potential_referral_code(
message: types.Message,
state: FSMContext,
db: AsyncSession
):
current_state = await state.get_state()
logger.info(f"🔍 REFERRAL CHECK: Проверка сообщения '{message.text}' в состоянии {current_state}")
if current_state not in [
RegistrationStates.waiting_for_rules_accept.state,
RegistrationStates.waiting_for_referral_code.state,
None
]:
return False
user = await get_user_by_telegram_id(db, message.from_user.id)
if user and user.status == UserStatus.ACTIVE.value:
return False
potential_code = message.text.strip()
if len(potential_code) < 4 or len(potential_code) > 20:
return False
referrer = await get_user_by_referral_code(db, potential_code)
if not referrer:
await message.answer(
"❌ Неверный реферальный код.\n\n"
"💡 Если у вас есть реферальный код, убедитесь что он введен правильно.\n"
"⏭️ Для продолжения регистрации без реферального кода используйте команду /start"
)
return True
data = await state.get_data() or {}
data['referral_code'] = potential_code
data['referrer_id'] = referrer.id
await state.set_data(data)
await message.answer("✅ Реферальный код принят!")
logger.info(f"✅ Реферальный код {potential_code} применен для пользователя {message.from_user.id}")
if current_state != RegistrationStates.waiting_for_referral_code.state:
language = data.get('language', 'ru')
texts = get_texts(language)
await message.answer(
texts.RULES_TEXT,
reply_markup=get_rules_keyboard(language)
)
await state.set_state(RegistrationStates.waiting_for_rules_accept)
logger.info("📋 Правила отправлены после ввода реферального кода")
else:
await complete_registration(message, state, db)
return True
async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession, db_user=None):
logger.info(f"🚀 START: Обработка /start от {message.from_user.id}")
@@ -36,6 +93,37 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
if user and user.status != UserStatus.DELETED.value:
logger.info(f"✅ Активный пользователь найден: {user.telegram_id}")
profile_updated = False
if user.username != message.from_user.username:
old_username = user.username
user.username = message.from_user.username
logger.info(f"📝 Username обновлен: '{old_username}''{user.username}'")
profile_updated = True
if user.first_name != message.from_user.first_name:
old_first_name = user.first_name
user.first_name = message.from_user.first_name
logger.info(f"📝 Имя обновлено: '{old_first_name}''{user.first_name}'")
profile_updated = True
if user.last_name != message.from_user.last_name:
old_last_name = user.last_name
user.last_name = message.from_user.last_name
logger.info(f"📝 Фамилия обновлена: '{old_last_name}''{user.last_name}'")
profile_updated = True
user.last_activity = datetime.utcnow()
if profile_updated:
user.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(user)
logger.info(f"💾 Профиль пользователя {user.telegram_id} обновлен")
else:
await db.commit()
texts = get_texts(user.language)
if referral_code and not user.referred_by_id:
@@ -50,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(
@@ -58,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()
@@ -107,6 +195,13 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
user.remnawave_uuid = None
user.has_had_paid_subscription = False
user.referred_by_id = None
user.username = message.from_user.username
user.first_name = message.from_user.first_name
user.last_name = message.from_user.last_name
user.updated_at = datetime.utcnow()
user.last_activity = datetime.utcnow()
from app.utils.user_utils import generate_unique_referral_code
user.referral_code = await generate_unique_referral_code(db, user.telegram_id)
@@ -285,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,
@@ -299,6 +395,10 @@ async def complete_registration_from_callback(
logger.warning(f"⚠️ Пользователь {callback.from_user.id} уже активен! Показываем главное меню.")
texts = get_texts(existing_user.language)
data = await state.get_data()
if data.get('referral_code') and not existing_user.referred_by_id:
await callback.message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.")
has_active_subscription = existing_user.subscription is not None
subscription_is_active = False
@@ -306,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:
@@ -362,7 +465,7 @@ async def complete_registration_from_callback(
user = existing_user
logger.info(f"✅ Пользователь {callback.from_user.id} восстановлен")
else:
elif not existing_user:
logger.info(f"🆕 Создаем нового пользователя {callback.from_user.id}")
referral_code = await generate_unique_referral_code(db, callback.from_user.id)
@@ -377,6 +480,20 @@ async def complete_registration_from_callback(
referred_by_id=referrer_id,
referral_code=referral_code
)
else:
logger.info(f"🔄 Обновляем существующего пользователя {callback.from_user.id}")
existing_user.status = UserStatus.ACTIVE.value
existing_user.language = language
if referrer_id and not existing_user.referred_by_id:
existing_user.referred_by_id = referrer_id
from datetime import datetime
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
await db.commit()
await db.refresh(existing_user)
user = existing_user
if referrer_id:
try:
@@ -390,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
@@ -401,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(
@@ -409,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}")
@@ -419,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}")
@@ -451,6 +568,48 @@ async def complete_registration(
existing_user = await get_user_by_telegram_id(db, message.from_user.id)
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
logger.warning(f"⚠️ Пользователь {message.from_user.id} уже активен! Показываем главное меню.")
texts = get_texts(existing_user.language)
data = await state.get_data()
if data.get('referral_code') and not existing_user.referred_by_id:
await message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.")
has_active_subscription = existing_user.subscription is not None
subscription_is_active = False
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=user_name,
subscription_status=_get_subscription_status(existing_user, texts)
),
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,
balance_kopeks=balance_kopeks
)
)
except Exception as e:
logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}")
await message.answer(f"Добро пожаловать, {user_name}!")
await state.clear()
return
data = await state.get_data()
language = data.get('language', 'ru')
texts = get_texts(language)
@@ -483,7 +642,7 @@ async def complete_registration(
user = existing_user
logger.info(f"✅ Пользователь {message.from_user.id} восстановлен")
else:
elif not existing_user:
logger.info(f"🆕 Создаем нового пользователя {message.from_user.id}")
referral_code = await generate_unique_referral_code(db, message.from_user.id)
@@ -498,6 +657,20 @@ async def complete_registration(
referred_by_id=referrer_id,
referral_code=referral_code
)
else:
logger.info(f"🔄 Обновляем существующего пользователя {message.from_user.id}")
existing_user.status = UserStatus.ACTIVE.value
existing_user.language = language
if referrer_id and not existing_user.referred_by_id:
existing_user.referred_by_id = referrer_id
from datetime import datetime
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
await db.commit()
await db.refresh(existing_user)
user = existing_user
if referrer_id:
try:
@@ -511,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
@@ -522,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(
@@ -530,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}")
@@ -540,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}")
@@ -563,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):
@@ -614,4 +822,13 @@ def register_handlers(dp: Dispatcher):
)
logger.info("✅ Зарегистрирован process_referral_code_input")
dp.message.register(
handle_potential_referral_code,
StateFilter(
RegistrationStates.waiting_for_rules_accept,
RegistrationStates.waiting_for_referral_code
)
)
logger.info("✅ Зарегистрирован handle_potential_referral_code")
logger.info("🔧 === КОНЕЦ регистрации обработчиков start.py ===")
+380 -72
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
}
@@ -349,6 +443,9 @@ async def handle_add_countries(
db: AsyncSession,
state: FSMContext
):
if not await _should_show_countries_management():
await callback.answer("ℹ️ Управление серверами недоступно - доступен только один сервер", show_alert=True)
return
texts = get_texts(db_user.language)
subscription = db_user.subscription
@@ -366,7 +463,7 @@ async def handle_add_countries(
current_countries_names.append(country['name'])
text = "🌍 <b>Управление странами подписки</b>\n\n"
text += f"📍 <b>Текущие страны ({len(current_countries)}):</b>\n"
text += f"📋 <b>Текущие страны ({len(current_countries)}):</b>\n"
if current_countries_names:
text += "\n".join(f"{name}" for name in current_countries_names)
else:
@@ -409,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)
@@ -421,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(
@@ -782,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:
@@ -795,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:
@@ -850,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,
@@ -861,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:
@@ -874,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)
@@ -1143,27 +1259,96 @@ 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)
)
await state.set_state(SubscriptionStates.selecting_traffic)
else:
countries = await _get_available_countries()
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
if await _should_show_countries_management():
countries = await _get_available_countries()
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
else:
countries = await _get_available_countries()
available_countries = [c for c in countries if c.get('is_available', True)]
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
await state.set_data(data)
await callback.message.edit_text(
texts.SELECT_DEVICES,
reply_markup=get_devices_keyboard(1, db_user.language)
)
await state.set_state(SubscriptionStates.selecting_devices)
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)
@@ -1237,23 +1422,36 @@ async def select_traffic(
state: FSMContext,
db_user: User
):
traffic_gb = int(callback.data.split('_')[1])
texts = get_texts(db_user.language)
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)
countries = await _get_available_countries()
if await _should_show_countries_management():
countries = await _get_available_countries()
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
else:
countries = await _get_available_countries()
available_countries = [c for c in countries if c.get('is_available', True)]
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
await state.set_data(data)
await callback.message.edit_text(
texts.SELECT_DEVICES,
reply_markup=get_devices_keyboard(1, db_user.language)
)
await state.set_state(SubscriptionStates.selecting_devices)
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
await callback.answer()
@@ -1263,7 +1461,6 @@ async def select_country(
db_user: User,
db: AsyncSession
):
country_uuid = callback.data.split('_')[1]
data = await state.get_data()
@@ -1275,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()
@@ -1335,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()
@@ -1344,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
@@ -1362,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()
@@ -1385,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 = "Безлимитный"
@@ -1449,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
@@ -1543,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)
@@ -1609,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,
@@ -1641,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,
@@ -1667,9 +1960,11 @@ async def handle_subscription_settings(
Выберите что хотите изменить:
"""
show_countries = await _should_show_countries_management()
await callback.message.edit_text(
settings_text,
reply_markup=get_subscription_settings_keyboard(db_user.language),
reply_markup=get_subscription_settings_keyboard(db_user.language, show_countries),
parse_mode="HTML"
)
await callback.answer()
@@ -1858,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
})
@@ -1989,6 +2284,15 @@ async def handle_add_country_to_subscription(
await callback.answer()
async def _should_show_countries_management() -> bool:
try:
countries = await _get_available_countries()
available_countries = [c for c in countries if c.get('is_available', True)]
return len(available_countries) > 1
except Exception as e:
logger.error(f"Ошибка проверки доступных серверов: {e}")
return True
async def confirm_add_countries_to_subscription(
callback: types.CallbackQuery,
@@ -2207,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
@@ -2216,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")
]
@@ -2230,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"
@@ -2240,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=[
@@ -2250,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")
]
@@ -2263,9 +2561,6 @@ async def handle_connect_subscription(
🚀 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть приложение:
📋 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
""",
reply_markup=keyboard,
parse_mode="HTML"
@@ -2428,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,
@@ -2539,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,
@@ -2560,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"
@@ -2616,7 +2921,6 @@ def register_handlers(dp: Dispatcher):
F.data == "confirm_reset_devices"
)
dp.callback_query.register(
select_period,
F.data.startswith("period_"),
@@ -2647,7 +2951,6 @@ def register_handlers(dp: Dispatcher):
SubscriptionStates.confirming_purchase
)
dp.callback_query.register(
handle_autopay_menu,
F.data == "subscription_autopay"
@@ -2729,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"
)
+148 -58
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([
@@ -142,27 +165,31 @@ def get_subscription_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_subscription_settings_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_subscription_settings_keyboard(language: str = "ru", show_countries_management: bool = True) -> InlineKeyboardMarkup:
from app.config import settings
texts = get_texts(language)
keyboard = [
[
keyboard = []
if show_countries_management:
keyboard.append([
InlineKeyboardButton(text="🌍 Добавить страны", callback_data="subscription_add_countries")
],
])
keyboard.extend([
[
InlineKeyboardButton(text="📱 Добавить устройства", callback_data="subscription_add_devices")
],
[
InlineKeyboardButton(text="🔄 Сбросить устройства", callback_data="subscription_reset_devices")
]
]
])
if settings.is_traffic_selectable():
keyboard.insert(1, [
keyboard.insert(-2, [
InlineKeyboardButton(text="📈 Добавить трафик", callback_data="subscription_add_traffic")
])
keyboard.insert(2, [
keyboard.insert(-2, [
InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="subscription_reset_traffic")
])
@@ -215,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")
@@ -244,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 = []
@@ -287,17 +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 ""
keyboard.append([
button_text = f"{emoji} {devices}{price_text}"
buttons.append(
InlineKeyboardButton(
text=f"{emoji} {devices} устройство{_get_device_suffix(devices)}{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")],
@@ -306,6 +375,13 @@ def get_devices_keyboard(current: int, language: str = "ru") -> InlineKeyboardMa
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def _get_device_declension(count: int) -> str:
if count % 10 == 1 and count % 100 != 11:
return "устройство"
elif count % 10 in [2, 3, 4] and count % 100 not in [12, 13, 14]:
return "устройства"
else:
return "устройств"
def get_subscription_confirm_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
@@ -500,16 +576,6 @@ def get_pagination_keyboard(
return keyboard
def _get_device_suffix(count: int) -> str:
if count == 1:
return ""
elif 2 <= count <= 4:
return "а"
else:
return ""
def get_confirmation_keyboard(
confirm_data: str,
cancel_data: str = "cancel",
@@ -599,44 +665,68 @@ 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
add_device_word = _get_device_declension(add_count)
keyboard.append([
InlineKeyboardButton(
text=f"📱 +{add_count} устройство{_get_device_suffix(add_count)} (итого: {total_devices}) - {settings.format_price(price)}",
text=f"📱 +{add_count} {add_device_word} (итого: {total_devices}) - {settings.format_price(price)}",
callback_data=f"add_devices_{add_count}"
)
])
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 = "⏰ Истекла"
+28 -2
View File
@@ -1,4 +1,5 @@
import logging
from datetime import datetime
from typing import Callable, Dict, Any, Awaitable
from aiogram import BaseMiddleware
from aiogram.types import Message, CallbackQuery, TelegramObject, User as TgUser
@@ -56,7 +57,7 @@ class AuthMiddleware(BaseMiddleware):
)
if is_registration_process:
logger.info(f"📝 Пропускаем пользователя {user.id} в процессе регистрации")
logger.info(f"🔍 Пропускаем пользователя {user.id} в процессе регистрации")
data['db'] = db
data['db_user'] = None
data['is_admin'] = False
@@ -124,8 +125,33 @@ class AuthMiddleware(BaseMiddleware):
logger.info(f"❌ Удаленный пользователь {user.id} попытался использовать бота без /start")
return
from datetime import datetime
profile_updated = False
if db_user.username != user.username:
old_username = db_user.username
db_user.username = user.username
logger.info(f"📝 [Middleware] Username обновлен для {user.id}: '{old_username}''{db_user.username}'")
profile_updated = True
if db_user.first_name != user.first_name:
old_first_name = db_user.first_name
db_user.first_name = user.first_name
logger.info(f"📝 [Middleware] Имя обновлено для {user.id}: '{old_first_name}''{db_user.first_name}'")
profile_updated = True
if db_user.last_name != user.last_name:
old_last_name = db_user.last_name
db_user.last_name = user.last_name
logger.info(f"📝 [Middleware] Фамилия обновлена для {user.id}: '{old_last_name}''{db_user.last_name}'")
profile_updated = True
db_user.last_activity = datetime.utcnow()
if profile_updated:
db_user.updated_at = datetime.utcnow()
logger.info(f"💾 [Middleware] Профиль пользователя {user.id} обновлен в middleware")
await db.commit()
data['db'] = db
+142 -4
View File
@@ -28,8 +28,15 @@ class MaintenanceService:
self._status = MaintenanceStatus(is_active=False)
self._check_task: Optional[asyncio.Task] = None
self._is_checking = False
self._max_consecutive_failures = 3
self._max_consecutive_failures = 3
self._bot = None
self._last_notification_sent = None
def set_bot(self, bot):
"""Устанавливает ссылку на бота для отправки уведомлений"""
self._bot = bot
logger.info("Бот установлен для maintenance_service")
@property
def status(self) -> MaintenanceStatus:
return self._status
@@ -51,6 +58,51 @@ class MaintenanceService:
else:
return settings.get_maintenance_message()
async def _notify_admins(self, message: str, alert_type: str = "info"):
"""Отправка уведомлений администраторам"""
if not self._bot:
logger.warning("Бот не установлен, уведомления не могут быть отправлены")
return
cache_key = f"maintenance_notification_{alert_type}"
if await cache.get(cache_key):
return
admin_ids = settings.get_admin_ids()
if not admin_ids:
logger.warning("Список администраторов пуст")
return
emoji_map = {
"error": "🚨",
"warning": "⚠️",
"success": "",
"info": ""
}
emoji = emoji_map.get(alert_type, "")
formatted_message = f"{emoji} <b>Maintenance Service</b>\n\n{message}"
success_count = 0
for admin_id in admin_ids:
try:
await self._bot.send_message(
chat_id=admin_id,
text=formatted_message,
parse_mode="HTML"
)
success_count += 1
await asyncio.sleep(0.1)
except Exception as e:
logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
if success_count > 0:
logger.info(f"Уведомление отправлено {success_count} администраторам")
await cache.set(cache_key, True, expire=300)
else:
logger.error("Не удалось отправить уведомления ни одному администратору")
async def enable_maintenance(self, reason: Optional[str] = None, auto: bool = False) -> bool:
try:
if self._status.is_active:
@@ -64,6 +116,18 @@ class MaintenanceService:
await self._save_status_to_cache()
notification_msg = f"""
Режим технических работ ВКЛЮЧЕН
📋 <b>Причина:</b> {self._status.reason}
🤖 <b>Автоматически:</b> {'Да' if auto else 'Нет'}
🕐 <b>Время:</b> {self._status.enabled_at.strftime('%d.%m.%Y %H:%M:%S')}
Обычные пользователи временно не смогут использовать бота.
"""
await self._notify_admins(notification_msg, "warning" if auto else "info")
logger.warning(f"🔧 Режим техработ ВКЛЮЧЕН. Причина: {self._status.reason}")
return True
@@ -77,6 +141,11 @@ class MaintenanceService:
logger.info("Режим техработ уже выключен")
return True
was_auto = self._status.auto_enabled
duration = None
if self._status.enabled_at:
duration = datetime.utcnow() - self._status.enabled_at
self._status.is_active = False
self._status.enabled_at = None
self._status.reason = None
@@ -85,6 +154,27 @@ class MaintenanceService:
await self._save_status_to_cache()
duration_str = ""
if duration:
hours = int(duration.total_seconds() // 3600)
minutes = int((duration.total_seconds() % 3600) // 60)
if hours > 0:
duration_str = f"\n⏱️ <b>Длительность:</b> {hours}ч {minutes}мин"
else:
duration_str = f"\n⏱️ <b>Длительность:</b> {minutes}мин"
notification_msg = f"""
Режим технических работ ВЫКЛЮЧЕН
🤖 <b>Автоматически:</b> {'Да' if was_auto else 'Нет'}
🕐 <b>Время:</b> {datetime.utcnow().strftime('%d.%m.%Y %H:%M:%S')}
{duration_str}
Сервис снова доступен для пользователей.
"""
await self._notify_admins(notification_msg, "success")
logger.info("✅ Режим техработ ВЫКЛЮЧЕН")
return True
@@ -102,6 +192,17 @@ class MaintenanceService:
self._check_task = asyncio.create_task(self._monitoring_loop())
logger.info(f"🔄 Запущен мониторинг API RemnaWave (интервал: {settings.get_maintenance_check_interval()}с)")
await self._notify_admins(f"""
Мониторинг технических работ запущен
🔄 <b>Интервал проверки:</b> {settings.get_maintenance_check_interval()} секунд
🤖 <b>Автовключение:</b> {'Включено' if settings.is_maintenance_auto_enable() else 'Отключено'}
🎯 <b>Порог ошибок:</b> {self._max_consecutive_failures}
Система будет следить за доступностью API.
""", "info")
return True
except Exception as e:
@@ -117,7 +218,8 @@ class MaintenanceService:
except asyncio.CancelledError:
pass
logger.info("⏹️ Мониторинг API остановлен")
await self._notify_admins("Мониторинг технических работ остановлен", "info")
logger.info("ℹ️ Мониторинг API остановлен")
return True
except Exception as e:
@@ -138,6 +240,18 @@ class MaintenanceService:
is_connected = await test_api_connection(api)
if is_connected:
# API восстановилось
if not self._status.api_status:
await self._notify_admins(f"""
API RemnaWave восстановлено!
<b>Статус:</b> Доступно
🕐 <b>Время восстановления:</b> {self._status.last_check.strftime('%H:%M:%S')}
🔄 <b>Неудачных попыток было:</b> {self._status.consecutive_failures}
API снова отвечает на запросы.
""", "success")
self._status.api_status = True
self._status.consecutive_failures = 0
@@ -147,9 +261,21 @@ class MaintenanceService:
return True
else:
was_available = self._status.api_status
self._status.api_status = False
self._status.consecutive_failures += 1
if was_available:
await self._notify_admins(f"""
API RemnaWave недоступно!
<b>Статус:</b> Недоступно
🕐 <b>Время обнаружения:</b> {self._status.last_check.strftime('%H:%M:%S')}
🔄 <b>Попытка:</b> {self._status.consecutive_failures}
Началась серия неудачных проверок API.
""", "error")
if (self._status.consecutive_failures >= self._max_consecutive_failures and
not self._status.is_active and
settings.is_maintenance_auto_enable()):
@@ -163,6 +289,17 @@ class MaintenanceService:
except Exception as e:
logger.error(f"Ошибка проверки API: {e}")
if self._status.api_status:
await self._notify_admins(f"""
Ошибка при проверке API RemnaWave
<b>Ошибка:</b> {str(e)}
🕐 <b>Время:</b> {datetime.utcnow().strftime('%H:%M:%S')}
Не удалось выполнить проверку доступности API.
""", "error")
self._status.api_status = False
self._status.consecutive_failures += 1
return False
@@ -216,7 +353,7 @@ class MaintenanceService:
if status_data.get("last_check"):
self._status.last_check = datetime.fromisoformat(status_data["last_check"])
logger.info(f"📥 Состояние техработ загружено из кеша: активен={self._status.is_active}")
logger.info(f"🔥 Состояние техработ загружено из кеша: активен={self._status.is_active}")
except Exception as e:
logger.error(f"Ошибка загрузки состояния из кеша: {e}")
@@ -232,7 +369,8 @@ class MaintenanceService:
"consecutive_failures": self._status.consecutive_failures,
"monitoring_active": self._check_task is not None and not self._check_task.done(),
"auto_enable_configured": settings.is_maintenance_auto_enable(),
"check_interval": settings.get_maintenance_check_interval()
"check_interval": settings.get_maintenance_check_interval(),
"bot_connected": self._bot is not None
}
async def force_api_check(self) -> Dict[str, Any]:
+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:
+77
View File
@@ -0,0 +1,77 @@
services:
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-remnawave_bot}
POSTGRES_USER: ${POSTGRES_USER:-remnawave_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password_123}
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:7-alpine
container_name: remnawave_bot_redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- bot_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
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
volumes:
- ./logs:/app/logs:rw
- ./data:/app/data:rw
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "${TRIBUTE_WEBHOOK_PORT:-8081}:8081"
- "${YOOKASSA_WEBHOOK_PORT:-8082}:8082"
networks:
- bot_network
user: "1000:1000"
command: >
bash -c "
mkdir -p /app/logs /app/data &&
python main.py
"
volumes:
postgres_data:
redis_data:
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
+16 -9
View File
@@ -77,6 +77,8 @@ async def main():
bot, dp = await setup_bot()
monitoring_service.bot = bot
maintenance_service.set_bot(bot)
logger.info("🔗 Бот подключен к сервисам мониторинга и техработ")
payment_service = PaymentService(bot)
@@ -95,11 +97,16 @@ async def main():
else:
logger.info("ℹ️ YooKassa отключена, webhook сервер не запускается")
logger.info("🔍 Запуск службы мониторинга...")
logger.info("📊 Запуск службы мониторинга...")
monitoring_task = asyncio.create_task(monitoring_service.start_monitoring())
logger.info("🔧 Запуск службы техработ...")
maintenance_task = asyncio.create_task(maintenance_service.start_monitoring())
logger.info("🔧 Проверка службы техработ...")
if not maintenance_service._check_task or maintenance_service._check_task.done():
logger.info("🔧 Запуск службы техработ...")
maintenance_task = asyncio.create_task(maintenance_service.start_monitoring())
else:
logger.info("🔧 Служба техработ уже запущена")
maintenance_task = None
logger.info("🔄 Запуск polling...")
polling_task = asyncio.create_task(dp.start_polling(bot, skip_updates=True))
@@ -131,7 +138,7 @@ async def main():
logger.error(f"Служба мониторинга завершилась с ошибкой: {exception}")
monitoring_task = asyncio.create_task(monitoring_service.start_monitoring())
if maintenance_task.done():
if maintenance_task and maintenance_task.done():
exception = maintenance_task.exception()
if exception:
logger.error(f"Служба техработ завершилась с ошибкой: {exception}")
@@ -154,7 +161,7 @@ async def main():
logger.info("🛑 Начинается корректное завершение работы...")
if yookassa_server_task and not yookassa_server_task.done():
logger.info("️ Остановка YooKassa webhook сервера...")
logger.info("️ Остановка YooKassa webhook сервера...")
yookassa_server_task.cancel()
try:
await yookassa_server_task
@@ -162,7 +169,7 @@ async def main():
pass
if monitoring_task and not monitoring_task.done():
logger.info("️ Остановка службы мониторинга...")
logger.info("️ Остановка службы мониторинга...")
monitoring_service.stop_monitoring()
monitoring_task.cancel()
try:
@@ -171,7 +178,7 @@ async def main():
pass
if maintenance_task and not maintenance_task.done():
logger.info("️ Остановка службы техработ...")
logger.info("️ Остановка службы техработ...")
await maintenance_service.stop_monitoring()
maintenance_task.cancel()
try:
@@ -180,7 +187,7 @@ async def main():
pass
if polling_task and not polling_task.done():
logger.info("️ Остановка polling...")
logger.info("️ Остановка polling...")
polling_task.cancel()
try:
await polling_task
@@ -188,7 +195,7 @@ async def main():
pass
if webhook_server:
logger.info("️ Остановка Tribute webhook сервера...")
logger.info("️ Остановка Tribute webhook сервера...")
await webhook_server.stop()
if 'bot' in locals():