Compare commits

...

243 Commits

Author SHA1 Message Date
Egor 77d17f97d3 Update admin_handlers.py 2025-08-15 04:56:43 +03:00
Egor d5c1ec3aea Update database.py 2025-08-15 04:54:53 +03:00
Egor 97a7384381 Update database.py 2025-08-15 04:49:48 +03:00
Egor 27b7bfccba Update admin_handlers.py 2025-08-15 04:49:18 +03:00
Egor ab15624a93 Update keyboards.py 2025-08-15 04:19:58 +03:00
Egor a9c5d8ed69 Update handlers.py 2025-08-15 04:19:40 +03:00
Egor 234b90796e Update handlers.py 2025-08-15 04:02:38 +03:00
Egor 5f018ba22f Update keyboards.py 2025-08-15 04:00:47 +03:00
Egor e11cbbcc18 Update handlers.py 2025-08-15 03:58:51 +03:00
Egor ba4d30d8db Update handlers.py 2025-08-15 03:46:22 +03:00
Egor d11431769c Update keyboards.py 2025-08-15 03:36:12 +03:00
Egor d1d1bbb3e4 Update handlers.py 2025-08-15 03:35:42 +03:00
Egor 823f30882a Update handlers.py 2025-08-15 03:33:09 +03:00
Egor d36f389469 Merge pull request #1 from maximovnikolay/feature/disable_stars_payment
disable stars payment button by config

Спасибо за помощь и вклад! Очень ценю!
2025-08-15 03:25:01 +03:00
Maximov Nikolay add686a4d5 disable stars payment button by config 2025-08-14 23:49:23 +03:00
Egor 59be2d8f02 Update README.md 2025-08-14 05:47:01 +03:00
Egor aa3db49f65 Update README.md 2025-08-14 05:39:19 +03:00
Egor c101cb3d3f Update README.md 2025-08-14 05:29:41 +03:00
Egor 63912af43b Update README.md 2025-08-14 05:26:12 +03:00
Egor d19263dd79 Update .env.example 2025-08-14 05:08:03 +03:00
Egor f8c9c59a03 Update database.py 2025-08-14 04:45:02 +03:00
Egor 5595f7a954 Update admin_handlers.py 2025-08-14 04:44:33 +03:00
Egor bac4527945 Update autopay_service.py 2025-08-14 04:44:09 +03:00
Egor 51da8bcb72 Update remnawave_api.py 2025-08-14 04:41:38 +03:00
Egor 8aa6e7e245 Update subscription_monitor.py 2025-08-14 04:40:29 +03:00
Egor 5029b35d14 Update subscription_monitor.py 2025-08-14 04:39:37 +03:00
Egor 2412027d9b Update handlers.py 2025-08-14 04:39:17 +03:00
Egor ddff5219e5 Create webhook_server.py 2025-08-14 04:38:58 +03:00
Egor 346bc18b58 Create tribute_service.py 2025-08-14 04:38:12 +03:00
Egor c256ae3664 Update keyboards.py 2025-08-14 04:37:33 +03:00
Egor 2b47358a9e Update config.py 2025-08-14 04:36:58 +03:00
Egor e0b041ce44 Update main.py 2025-08-14 04:36:41 +03:00
Egor 3f64dee927 Update database.py 2025-08-13 08:02:35 +03:00
Egor 67c5ebeabd Update admin_handlers.py 2025-08-13 08:02:18 +03:00
Egor 84bc8e96cc Update database.py 2025-08-13 07:58:18 +03:00
Egor 2d30cec7c6 Update lucky_game.py 2025-08-13 07:55:50 +03:00
Egor 5f5f66178b Update lucky_game.py 2025-08-13 07:50:17 +03:00
Egor e9fadec0ac Update database.py 2025-08-13 07:49:40 +03:00
Egor 10008e51ff Update admin_handlers.py 2025-08-13 07:39:20 +03:00
Egor 3098610d17 Update database.py 2025-08-13 07:31:29 +03:00
Egor 7a2566d320 Update admin_handlers.py 2025-08-13 07:31:09 +03:00
Egor 01ba8ca53e Update subscription_monitor.py 2025-08-13 07:09:12 +03:00
Egor 2a0a2dccff Update README.md 2025-08-13 06:55:22 +03:00
Egor 66a6f32d4c Update .env.example 2025-08-13 06:51:35 +03:00
Egor 02b3f61953 Update translations.py 2025-08-13 06:37:10 +03:00
Egor dc08526255 Update config.py 2025-08-13 06:36:52 +03:00
Egor 2ca339610b Update subscription_monitor.py 2025-08-13 06:36:32 +03:00
Egor ffe65937c1 Update README.md 2025-08-12 19:16:27 +03:00
Egor 4b6f03eef5 Update README.md 2025-08-12 19:15:02 +03:00
Egor d2f530a7fe Update bedolaga.sh 2025-08-12 19:10:37 +03:00
Egor 44ea4b6b13 Update .env.example 2025-08-12 18:55:22 +03:00
Egor ebc8421a70 Update README.md 2025-08-12 18:52:20 +03:00
Egor 526f142dea Update remnawave_api.py 2025-08-12 18:12:34 +03:00
Egor ca431d36e6 Add files via upload 2025-08-12 03:49:13 +03:00
Egor 68a81bdd89 Delete screens/уведомления/Снимок экрана 2025-08-12 в 01.45.14.png 2025-08-12 03:48:59 +03:00
Fr1ngg 68a3d713ef Update README.md 2025-08-12 03:19:42 +03:00
Fr1ngg 50d3d08e38 Update README.md 2025-08-12 03:14:42 +03:00
Fr1ngg 9a2fec4d8f Update README.md 2025-08-12 03:10:34 +03:00
Fr1ngg e68b1d2092 Add files via upload 2025-08-12 02:48:15 +03:00
Fr1ngg 6dfedef81e Update README.md 2025-08-12 02:36:41 +03:00
Fr1ngg 317e39c823 Update keyboards.py 2025-08-12 02:34:19 +03:00
Fr1ngg 0072a3cb71 Update translations.py 2025-08-12 02:34:03 +03:00
Fr1ngg 8d021c1530 Update database.py 2025-08-12 02:33:44 +03:00
Fr1ngg 23606dc2d4 Update admin_handlers.py 2025-08-12 02:33:23 +03:00
Fr1ngg 0838c4196d Add files via upload 2025-08-12 01:47:52 +03:00
Fr1ngg 65605a0de2 Create 1 2025-08-12 01:47:38 +03:00
Fr1ngg a20ac80281 Delete screens/уведомления 2025-08-12 01:47:25 +03:00
Fr1ngg 02e83fb0d0 Create уведомления 2025-08-12 01:47:13 +03:00
Fr1ngg eddad0db6e Add files via upload 2025-08-12 01:46:00 +03:00
Fr1ngg ffad0afb61 Delete screens/Balance/Снимок экрана 2025-08-09 в 07.54.52.png 2025-08-12 01:44:40 +03:00
Fr1ngg 83eed8e1cd Add files via upload 2025-08-12 01:44:17 +03:00
Fr1ngg 0c7c8abade Add files via upload 2025-08-12 01:42:18 +03:00
Fr1ngg a064890ed8 Delete screens/adminpanel/Снимок экрана 2025-08-09 в 08.09.05.png 2025-08-12 01:41:33 +03:00
Fr1ngg f310f4a35e Delete screens/sub/Снимок экрана 2025-08-09 в 08.05.47.png 2025-08-12 01:41:14 +03:00
Fr1ngg 0268e09882 Delete screens/Главное меню/Mainmenu.png 2025-08-12 01:40:23 +03:00
Fr1ngg 449814d0df Add files via upload 2025-08-12 01:40:09 +03:00
Fr1ngg a3d3577103 Update README.md 2025-08-12 01:37:43 +03:00
Fr1ngg a3086fdd2c Update README.md 2025-08-12 01:34:57 +03:00
Fr1ngg daafc6a41e Update README.md 2025-08-12 01:31:42 +03:00
Fr1ngg c27ee116f7 Update README.md 2025-08-12 01:28:56 +03:00
Fr1ngg 5167cde443 Update bedolaga.sh 2025-08-12 01:25:30 +03:00
Fr1ngg 34542d3a9c Update bedolaga.sh 2025-08-12 01:09:56 +03:00
Fr1ngg e73d1df4cc Update logo2.svg 2025-08-12 01:05:39 +03:00
Fr1ngg f9a95c3d48 Update README.md 2025-08-12 01:04:53 +03:00
Fr1ngg 2dd2b5fdbf Update README.md 2025-08-12 01:04:37 +03:00
Fr1ngg 896f1db9d4 Update README.md 2025-08-11 23:09:23 +03:00
Fr1ngg a1bd2d775f Create emergency_fix.py 2025-08-11 22:54:05 +03:00
Fr1ngg 8cbe23f9ed Update database.py 2025-08-11 22:31:17 +03:00
Fr1ngg edc891c93d Update README.md 2025-08-11 22:06:49 +03:00
Fr1ngg 2d706a4be0 Update keyboards.py 2025-08-11 22:01:38 +03:00
Fr1ngg 32cb81ef34 Update handlers.py 2025-08-11 22:01:09 +03:00
Fr1ngg 88aa3c4a3d Update database.py
autopay mod
2025-08-11 22:00:43 +03:00
Fr1ngg e813d3ccb7 Create autopay_service.py 2025-08-11 22:00:19 +03:00
Fr1ngg a24d7a4e24 Update main.py 2025-08-11 21:59:51 +03:00
Fr1ngg 39987b9717 Update admin_handlers.py
autopay mod
2025-08-11 21:59:31 +03:00
Fr1ngg c77a1ea4f8 Update README.md 2025-08-11 17:57:35 +03:00
Fr1ngg 64b5d90d66 Update README.md 2025-08-11 03:00:54 +03:00
Fr1ngg 5a1fb32e1d Update admin_handlers.py 2025-08-11 02:58:19 +03:00
Fr1ngg c4d8108e8c Update config.py 2025-08-11 02:53:37 +03:00
Fr1ngg 1350d80083 Update database.py 2025-08-11 02:51:28 +03:00
Fr1ngg e0cc218007 Update handlers.py 2025-08-11 02:49:06 +03:00
Fr1ngg bcaf8f2247 Update keyboards.py 2025-08-11 02:48:36 +03:00
Fr1ngg d1190ed9dc Update stars_handlers.py 2025-08-11 02:47:05 +03:00
Fr1ngg 513d6037d4 Update subscription_monitor.py 2025-08-11 02:46:40 +03:00
Fr1ngg e86012b2ab Update translations.py 2025-08-11 02:46:11 +03:00
Fr1ngg 214e381dc5 Update README.md 2025-08-10 10:41:22 +03:00
Fr1ngg aa79cb670b Update .env.example 2025-08-10 10:30:33 +03:00
Fr1ngg f0106e15fa Create stars_handlers.py 2025-08-10 10:26:28 +03:00
Fr1ngg 1d194c6be2 Update .env.example 2025-08-10 10:18:03 +03:00
Fr1ngg 6917890007 Update admin_handlers.py 2025-08-10 10:15:34 +03:00
Fr1ngg 1c69de7ecd Update config.py 2025-08-10 10:13:14 +03:00
Fr1ngg 62188793d9 Update database.py 2025-08-10 10:12:21 +03:00
Fr1ngg 585ee2d42b Update handlers.py 2025-08-10 10:11:12 +03:00
Fr1ngg 2a58cfd45c Update keyboards.py 2025-08-10 10:10:54 +03:00
Fr1ngg 93003c5fe0 Update main.py 2025-08-10 10:10:27 +03:00
Fr1ngg 91b0787d72 Update README.md 2025-08-10 07:04:17 +03:00
Fr1ngg 315277d4b0 Create bedolaga.sh 2025-08-10 06:56:04 +03:00
Fr1ngg 1821c0d89d Delete run.sh 2025-08-10 06:54:40 +03:00
Fr1ngg 5650da0c5f Update README.md 2025-08-10 03:56:42 +03:00
Fr1ngg 0ca8b73cef Update .env.example 2025-08-10 03:51:10 +03:00
Fr1ngg 6bab26ab27 Update lucky_game.py 2025-08-10 03:44:57 +03:00
Fr1ngg a72243e7fa Update keyboards.py 2025-08-10 03:42:56 +03:00
Fr1ngg 5bd86d9cf7 Update config.py 2025-08-10 03:40:36 +03:00
Fr1ngg 00fd3fbadb Update database.py 2025-08-10 03:39:51 +03:00
Fr1ngg b806f40797 Update middlewares.py 2025-08-10 03:38:20 +03:00
Fr1ngg c82aa1d253 Update main.py 2025-08-10 03:37:51 +03:00
Fr1ngg 071919f0db Update remnawave_api.py 2025-08-10 03:36:51 +03:00
Fr1ngg ab41e96ba5 Update subscription_monitor.py 2025-08-10 03:34:52 +03:00
Fr1ngg 95a470ba7b Update admin_handlers.py 2025-08-10 03:32:41 +03:00
Fr1ngg 1e0653ed33 Update referral_utils.py 2025-08-10 03:27:15 +03:00
Fr1ngg 3f0df08031 Update handlers.py 2025-08-10 03:25:53 +03:00
Fr1ngg 8c3bc8a5ba Update utils.py 2025-08-10 03:22:42 +03:00
Fr1ngg 3a5a8f48e4 Update README.md 2025-08-09 11:31:02 +03:00
Fr1ngg f4b99ffb09 Rename logo.svg to logo2.svg 2025-08-09 11:30:57 +03:00
Fr1ngg 0c6afdebc3 Update README.md 2025-08-09 11:30:17 +03:00
Fr1ngg 98b382ba2f Update logo.svg 2025-08-09 11:29:57 +03:00
Fr1ngg 54a24385b1 Update README.md 2025-08-09 08:54:57 +03:00
Fr1ngg 3eeb84cdae Update README.md 2025-08-09 08:29:02 +03:00
Fr1ngg 204a10ae14 Update README.md 2025-08-09 08:26:02 +03:00
Fr1ngg ba1dd7547e Create logo.svg 2025-08-09 08:24:25 +03:00
Fr1ngg 238585c257 Update .gitignore 2025-08-09 08:24:02 +03:00
Fr1ngg d8ebe1ad13 Add files via upload 2025-08-09 08:18:01 +03:00
Fr1ngg 819df3e93a Create 1 2025-08-09 08:13:36 +03:00
Fr1ngg 99e46feafa Delete screens/sub/1 2025-08-09 08:07:05 +03:00
Fr1ngg a432c8ab54 Add files via upload 2025-08-09 08:06:52 +03:00
Fr1ngg d740b7560c Create 1 2025-08-09 08:06:17 +03:00
Fr1ngg 91c4c73d7e Delete screens/Balance/1 2025-08-09 08:05:33 +03:00
Fr1ngg 4ce2384f27 Update handlers.py 2025-08-09 08:04:43 +03:00
Fr1ngg 02a6289d56 Add files via upload 2025-08-09 07:57:18 +03:00
Fr1ngg 52ae6eba81 Create 1 2025-08-09 07:55:29 +03:00
Fr1ngg 080b6e74a2 Add files via upload 2025-08-09 07:54:08 +03:00
Fr1ngg ad7d32023c Delete screens/Luckgame/1 2025-08-09 07:53:17 +03:00
Fr1ngg 1baf8fbe37 Add files via upload 2025-08-09 07:53:03 +03:00
Fr1ngg 758ca6a373 Create 1 2025-08-09 07:52:38 +03:00
Fr1ngg 65504d0a23 Delete screens/1 2025-08-09 07:50:15 +03:00
Fr1ngg 0409f4e2e2 Delete screens/Рефералы/1 2025-08-09 07:49:57 +03:00
Fr1ngg 8ca63e939a Add files via upload 2025-08-09 07:49:38 +03:00
Fr1ngg 25a621fd51 Delete screens/Главное меню/1 2025-08-09 07:43:58 +03:00
Fr1ngg 3f71a8aff7 Rename Снимок экрана 2025-08-09 в 07.42.12.png to Mainmenu.png 2025-08-09 07:43:45 +03:00
Fr1ngg 6ec81c37b9 Add files via upload 2025-08-09 07:42:41 +03:00
Fr1ngg 8a9ff98da8 Create 1 2025-08-09 07:40:31 +03:00
Fr1ngg bb7e29eeac Create 1 2025-08-09 07:40:05 +03:00
Fr1ngg 27d8783104 Create 1 2025-08-09 07:39:34 +03:00
Fr1ngg 9a01e6db43 Create .gitignore 2025-08-09 07:37:50 +03:00
Fr1ngg b341ddf9d9 Update README.md 2025-08-09 07:33:26 +03:00
Fr1ngg 3c8d0ac859 Update README.md 2025-08-09 07:33:03 +03:00
Fr1ngg 3f529c0dee Update README.md 2025-08-09 07:19:13 +03:00
Fr1ngg a6734e542d Create lucky_game.py 2025-08-09 07:04:20 +03:00
Fr1ngg 2287a67dfd Create referral_utils.py 2025-08-09 07:03:30 +03:00
Fr1ngg 31dbaa80d7 Update admin_handlers.py 2025-08-09 07:02:43 +03:00
Fr1ngg b1846a3ea9 Update api_error_handlers.py 2025-08-09 07:02:24 +03:00
Fr1ngg e1a7e9e622 Update config.py 2025-08-09 07:02:00 +03:00
Fr1ngg 0ae48faedb Update database.py 2025-08-09 07:01:35 +03:00
Fr1ngg b8122222f8 Update handlers.py 2025-08-09 07:00:51 +03:00
Fr1ngg 5e5a39c22e Update keyboards.py 2025-08-09 07:00:27 +03:00
Fr1ngg 1e7cf214e9 Update main.py 2025-08-09 07:00:10 +03:00
Fr1ngg d7e7a2df9a Update middlewares.py 2025-08-09 06:59:52 +03:00
Fr1ngg 584aa35894 Update remnawave_api.py 2025-08-09 06:59:36 +03:00
Fr1ngg 652ae8057a Update subscription_monitor.py 2025-08-09 06:59:10 +03:00
Fr1ngg 0bc0a1354a Update translations.py 2025-08-09 06:58:45 +03:00
Fr1ngg 7d30838813 Update utils.py 2025-08-09 06:58:22 +03:00
Fr1ngg 3da178ed02 Update handlers.py 2025-08-09 06:58:00 +03:00
Fr1ngg e6ac3e3812 Update README.md 2025-08-09 06:53:32 +03:00
Fr1ngg 62087e20b5 Update README.md 2025-08-09 06:50:45 +03:00
Fr1ngg b7a47c6eb6 Update .env.example 2025-08-09 06:32:47 +03:00
Fr1ngg 9b14baca51 Update README.md 2025-08-08 05:43:08 +03:00
Fr1ngg eb1a75c9f5 Update .env.example 2025-08-08 05:24:58 +03:00
Fr1ngg 5e79106134 Update admin_handlers.py 2025-08-08 05:22:43 +03:00
Fr1ngg ee45ec356e Update api_error_handlers.py 2025-08-08 05:22:20 +03:00
Fr1ngg 9275bff828 Update config.py 2025-08-08 05:21:51 +03:00
Fr1ngg 9150c70618 Update database.py 2025-08-08 05:21:25 +03:00
Fr1ngg b792e749d6 Update handlers.py 2025-08-08 05:21:00 +03:00
Fr1ngg 9871572740 Update keyboards.py 2025-08-08 05:20:37 +03:00
Fr1ngg 52c1cb3cbc Update main.py 2025-08-08 05:20:16 +03:00
Fr1ngg d5b2b371c8 Update middlewares.py 2025-08-08 05:19:23 +03:00
Fr1ngg d5cb022b02 Update remnawave_api.py 2025-08-08 05:19:06 +03:00
Fr1ngg 021f62fd30 Update subscription_monitor.py 2025-08-08 05:18:41 +03:00
Fr1ngg 684e398798 Update translations.py 2025-08-08 05:18:12 +03:00
Fr1ngg 12b64e9978 Update utils.py 2025-08-08 05:17:45 +03:00
Fr1ngg 273df23f20 Update README.md 2025-08-07 09:32:39 +03:00
Fr1ngg a9b0dbf3eb Update handlers.py 2025-08-07 09:31:28 +03:00
Fr1ngg 5f56e068d3 Update remnawave_api.py 2025-08-07 09:27:59 +03:00
Fr1ngg 92b82132d8 Update remnawave_api.py 2025-08-07 09:27:42 +03:00
Fr1ngg 79ae9d3a1c Update config.py 2025-08-07 09:25:55 +03:00
Fr1ngg 86714a93f9 Update handlers.py 2025-08-07 09:25:40 +03:00
Fr1ngg 553f6f8cf6 Update handlers.py 2025-08-07 09:24:22 +03:00
Fr1ngg b561f67c12 Update utils.py 2025-08-07 09:23:52 +03:00
Fr1ngg d03f9b80c7 Update translations.py 2025-08-07 09:23:26 +03:00
Fr1ngg 6d9a5fb578 Update README.md 2025-08-07 08:23:07 +03:00
Fr1ngg 44d19245c4 Update admin_handlers.py 2025-08-07 08:19:54 +03:00
Fr1ngg ce36c268cc Update main.py 2025-08-07 07:53:09 +03:00
Fr1ngg 1357b07921 Update middlewares.py 2025-08-07 07:52:49 +03:00
Fr1ngg d4bfdcb749 Update config.py 2025-08-07 07:52:33 +03:00
Fr1ngg 39621706c8 Update subscription_monitor.py 2025-08-07 07:52:01 +03:00
Fr1ngg 4684e7a1e5 Update utils.py 2025-08-07 07:51:36 +03:00
Fr1ngg 16bb3c8ad3 Update translations.py 2025-08-07 07:51:15 +03:00
Fr1ngg 16fbdcc655 Create api_error_handlers.py 2025-08-07 07:50:56 +03:00
Fr1ngg 416d268352 Update remnawave_api.py 2025-08-07 07:50:30 +03:00
Fr1ngg 44fb88f368 Update keyboards.py 2025-08-07 07:50:06 +03:00
Fr1ngg f050c12253 Update database.py 2025-08-07 07:49:48 +03:00
Fr1ngg 82486d121e Update handlers.py 2025-08-07 07:49:25 +03:00
Fr1ngg 2bf382dbac Update handlers.py 2025-08-07 07:49:06 +03:00
Fr1ngg ab562e0df0 Update handlers.py 2025-08-07 07:48:48 +03:00
Fr1ngg 02e8fba9d0 Update admin_handlers.py 2025-08-07 07:48:21 +03:00
Fr1ngg 530df33ba4 Update README.md 2025-08-05 15:41:41 +03:00
Fr1ngg 8b2e8cb681 Update README.md 2025-08-05 15:40:27 +03:00
Fr1ngg 2cb8c937e1 Update README.md 2025-08-05 15:39:33 +03:00
Fr1ngg ea5c08fb58 Update README.md 2025-08-05 15:35:42 +03:00
Fr1ngg 2507168916 Update README.md 2025-08-05 15:31:45 +03:00
Fr1ngg ec6e08f0d3 Update middlewares.py 2025-08-05 15:25:23 +03:00
Fr1ngg ceee7cc977 Update remnawave_api.py 2025-08-05 15:25:06 +03:00
Fr1ngg ece05cd9e9 Update main.py 2025-08-05 15:24:35 +03:00
Fr1ngg 2345016aa3 Update main.py 2025-08-05 15:24:16 +03:00
Fr1ngg d349352812 Update utils.py 2025-08-05 15:23:57 +03:00
Fr1ngg e002ca2cb2 Update handlers.py 2025-08-05 15:23:35 +03:00
Fr1ngg 849afb4aa4 Update admin_handlers.py 2025-08-05 15:23:15 +03:00
Fr1ngg 943628e6fe Update keyboards.py 2025-08-05 15:22:50 +03:00
Fr1ngg 6621de95c0 Update database.py
Fix admin_history_payments
2025-08-05 15:22:15 +03:00
Fr1ngg 87797153be Update README.md 2025-08-05 07:37:15 +03:00
Fr1ngg 4e833f9b8c Create docker-build.yml 2025-08-05 07:30:52 +03:00
Fr1ngg 560a2c362f Delete .github/workflows/build.yml 2025-08-05 07:22:59 +03:00
Fr1ngg 71e9e1ebe2 Create build.yml 2025-08-05 07:17:51 +03:00
Fr1ngg 73ed24aecf Update README.md 2025-08-05 05:41:43 +03:00
66 changed files with 18347 additions and 2029 deletions
+56 -14
View File
@@ -1,29 +1,71 @@
# Bot Configuration
BOT_TOKEN=
# настройки реф программы юзернейм без @!
BOT_USERNAME=
REFERRAL_FIRST_REWARD=100.0
REFERRAL_REFERRED_BONUS=100.0
REFERRAL_THRESHOLD=200.0
REFERRAL_PERCENTAGE=0.2
# RemnaWave API Configuration
REMNAWAVE_URL=https://example.com
REMNAWAVE_MODE=remote
REMNAWAVE_TOKEN=
SUBSCRIPTION_BASE_URL=https://example.com
# Admin Configuration (comma-separated list of Telegram user IDs)
ADMIN_IDS=123445567,12342123
# Support Configuration
DEFAULT_LANGUAGE=ru
SUPPORT_USERNAME=support
# Database Configuration (optional, defaults to SQLite)
# DATABASE_URL=sqlite+aiosqlite:///bot.db
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=
TRIAL_PRICE=0.0
TRIAL_ENABLED=true # Включить триал
TRIAL_DURATION_DAYS=3 # Дней триала
TRIAL_TRAFFIC_GB=2 # Лимит трафика у триал подписки
TRIAL_SQUAD_UUID= # UUID сквада из панели remnawave /dashboard/management/internal-squads
TRIAL_PRICE=0.0 # Оставить 0!
TRIAL_NOTIFICATION_ENABLED=true # Уведомление об истекшей триальной подписке
TRIAL_NOTIFICATION_HOURS_AFTER=1 # Через сколько отсылать сообщение
TRIAL_NOTIFICATION_HOURS_WINDOW=23 # Через сколько выслать повторно
# Monitor Service Settings (дополнительные настройки)
MONITOR_CHECK_INTERVAL=3600
MONITOR_DAILY_CHECK_HOUR=10
MONITOR_WARNING_DAYS=2
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
MONITOR_DAILY_CHECK_HOUR=10 # Ежедневный чек в 10 утра
MONITOR_WARNING_DAYS=2 # За сколько начинать до окончания подписки слать уведомления
DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после истечения удалять триал подписки
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
LUCKY_GAME_NUMBERS=30 # Общее количество чисел для выбора (1-30)
LUCKY_GAME_WINNING_COUNT=25 # Количество выигрышных чисел из общего количества
# ================================
# TELEGRAM STARS НАСТРОЙКИ
# ================================
# Включить/выключить пополнение через Telegram Stars
STARS_ENABLED=true
# Курсы обмена звезд на рубли
# Формат: STARS_{количество_звезд}_RATE={рубли}
# 100 звёзд
STARS_100_RATE=110
# 150 звёзд
STARS_150_RATE=165
# 250 звёзд
STARS_250_RATE=280
# 350 звёзд
STARS_350_RATE=355
# 500 звёзд
STARS_500_RATE=550
# 750 звёзд
STARS_750_RATE=800
# 1000 звёзд
STARS_1000_RATE=1000
TRIBUTE_ENABLED=true # true/false включить/выключить пополнение баланса с помощью доната на Tribute
TRIBUTE_API_KEY= # API ключ из настроек Tribute
TRIBUTE_WEBHOOK_PORT=8081
TRIBUTE_WEBHOOK_PATH=/tribute-webhook
TRIBUTE_DONATE_LINK=https://t.me/tribute/app?startapp=XXXXXXXX # укажите ссылку на донат в Tribute
+31
View File
@@ -0,0 +1,31 @@
name: BedolagaBot
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: fr1ngg/remnawave-bedolaga-telegram-bot:latest
+2
View File
@@ -0,0 +1,2 @@
/screens
/assets
+586 -144
View File
@@ -1,215 +1,657 @@
<img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 15 13" src="https://github.com/user-attachments/assets/91098622-1bce-4f27-afef-60a3c5b5061f" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 22" src="https://github.com/user-attachments/assets/46b87e75-b420-4ac6-91b9-8c7e9bcffb2a" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 39" src="https://github.com/user-attachments/assets/ca97811f-ca00-4133-a120-1c11f0efa0fc" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 45" src="https://github.com/user-attachments/assets/258e1adb-2c39-4126-82a7-7791b56d42db" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 53" src="https://github.com/user-attachments/assets/073455fc-f42d-4d70-839d-59042add2d94" /><img width="906" height="316" alt="Снимок экрана 2025-08-05 в 03 16 00" src="https://github.com/user-attachments/assets/2034dde8-a48b-4149-a23f-b788aa40e0b1" /><img width="906" height="366" alt="Снимок экрана 2025-08-05 в 03 16 18" src="https://github.com/user-attachments/assets/3a3d1e0a-92fc-4573-a48c-f36481d6d0de" /><img width="906" height="842" alt="Снимок экрана 2025-08-05 в 03 17 24" src="https://github.com/user-attachments/assets/8b407f69-6861-4810-822e-c3f7b8f63629" /><img width="906" height="274" alt="Снимок экрана 2025-08-05 в 03 17 43" src="https://github.com/user-attachments/assets/923a945a-5ef8-4dcb-9804-fffc37ab8887" /><img width="936" height="364" alt="Снимок экрана 2025-08-05 в 03 20 03" src="https://github.com/user-attachments/assets/1faecdfe-f80c-4ac2-ad38-81a30fc6623d" />
# 🤖 Remnawave Bedolaga Bot
#Описание
<div align="center">
RemnaWave Telegram Bot — это многофункциональный бот для управления подписками(Для каждой подписки возможно назначить свой сквад со своими инбаундами - нововведение Remnawave 2.0.0+), балансом, промокодами, тестовой подпиской и рассылками пользователям через Telegram.
[![Docker Image](https://img.shields.io/badge/Docker-fr1ngg/remnawave--bedolaga--telegram--bot-blue?logo=docker&logoColor=white)](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)
[![Python](https://img.shields.io/badge/Python-3.11+-blue?logo=python&logoColor=white)](https://python.org)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-blue?logo=postgresql&logoColor=white)](https://postgresql.org)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
[![Stars](https://img.shields.io/github/stars/Fr1ngg/remnawave-bedolaga-telegram-bot?style=social)](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/stargazers)
Бот интегрирован с системой RemnaWave версии 2.0.8
![Logo](./assets/logo2.svg)
#Основные возможности
**Современный Telegram-бот для управления VPN подписками через Remnawave API**
Мультиязычный интерфейс (на данный момент русский и английский языки)
*Полнофункциональное решение с управлением пользователями, платежами и администрированием*
Создание и покупка подписок с управлением трафиком, длительностью и ценой
[🚀 Быстрый старт](#-быстрый-старт) • [📖 Документация](#-документация) • [💬 Поддержка](#-поддержка) • [🤝 Вклад](#-вклад-в-проект)
Бесплатная тестовая подписка с ограничениями
</div>
Пополнение баланса: 1) Через саппорт в ручную 2) Отправка заявки с суммой админу (С возможность подтвердить/отклонить заявку)
---
Управление балансом пользователей (пополнение, списание)
## ✨ Основные возможности
Промокоды со скидками и ограничениями по использованию
<table>
<tr>
<td width="50%">
Полноценная админ-панель с контролем пользователей, созданием подписок(Подтягивает UUID сквада из панели), платежей и статистикой
### 👤 **Для пользователей**
- 💰 **Управление балансом** - Telegram Stars + Telegram Tribute + P2P через поддержку
- 🛒 **Покупка подписок** - различные тарифы с настройкой squad
- 📱 **Управление подписками** - просмотр, продление, ссылки
- 🎁 **Промокоды** - денежные бонусы
- 👥 **Реферальная программа** - зарабатывай с друзей
- 🎰 **Игра удачи** - ежедневные бонусы
- 🆓 **Тестовая подписка** - бесплатная пробная версия
- ♾️ **Автопродление** - настраиваемое автообновление
- 🌐 **Мультиязычность** - русский и английский
Рассылка сообщений отдельным пользователям и всем сразу
</td>
<td width="50%">
Сервис контроля истечения сроков действия подписки(Уведомляет об истечении за указанный в настройках срок), уведомления с предложением продления подписи. (NEW)
### ⚙️ **Для администраторов**
- 📊 **Детальная статистика** - пользователи, платежи, подписки
- 👥 **Управление пользователями** - поиск, редактирование, баланс
- 💳 **Управление платежами** - одобрение, история, Telegram Stars
- 🎫 **Управление промокодами** - создание, статистика, массовые операции
- 🖥 **Мониторинг системы** - состояние нод, синхронизация с Remnawave
- 📨 **Рассылки** - уведомления пользователям
- 🔍 **Мониторинг подписок** - автоуведомления об истечении
- 📋 **Правила сервиса** - настройка через админ-панель
- ♾️ **Статистика автопродления** - полный контроль
Интеграция с RemnaWave API для управления подписками и пользователями RemnaWave
</td>
</tr>
</table>
История платежей(Не работает, в доработке) и управление платежами (подтверждение, отклонение)
---
## 🚀 Быстрый старт
#Требования
### ⚡ Автоматическая установка (Ubuntu)
Pip
Установите бота **одной командой** с интерактивным мастером настройки:
Python 3.8+
```bash
curl -sSL https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh | sudo bash
```
PostgreSQL, SQLite или другая поддерживаемая SQL-база данных
<details>
<summary>🔧 Что делает установщик</summary>
Токен Telegram-бота
- ✅ Обновляет систему Ubuntu
- ✅ Устанавливает Docker и зависимости
- ✅ Создает структуру проекта в `/opt/bedolaga-bot`
- ✅ Интерактивная настройка всех параметров
- ✅ Создает systemd службу (опционально)
- ✅ Запускает удобное меню управления
URL и токен RemnaWave API
</details>
Ссылки на подписку из ремны формата SUB_PUBLIC_DOMAIN=sub.example.com/sub
### 🎛 Интерактивное меню управления
#Установка
После установки вы получаете полнофункциональное меню:
1. Клонируйте репозиторий:
<img width="500" alt="Интерфейс управления ботом" src="https://github.com/user-attachments/assets/9ab876f5-2758-4c52-93dd-6c9c654a07aa">
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot
cd remnawave-bedolaga-telegram-bot
**Доступные действия:**
- 🚀 Запуск/остановка/перезапуск бота
- 📺 Просмотр логов в реальном времени
- 💾 Создание и восстановление резервных копий БД
- ✏️ Редактирование конфигурации
- 🩺 Диагностика и устранение проблем
- 🔄 Автообновление бота
2. Создайте файл .env в корне проекта и заполните его необходимыми переменными окружения. Пример:
---
BOT_TOKEN=ваш_telegram_bot_token
REMNAWAVE_URL=https://your-remnawave-url.ru
REMNAWAVE_MODE=remote/local
REMNAWAVE_TOKEN=ваш_remnawave_token
SUBSCRIPTION_BASE_URL=https://
DATABASE_URL=sqlite+aiosqlite:///bot.db
ADMIN_IDS=123456789,987654321
DEFAULT_LANGUAGE=ru
SUPPORT_USERNAME=support
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=19bd5bde-5eea-4368-809c-6ba1ffb93897
TRIAL_PRICE=0.0
## 📖 Документация
3. Соберите образ (Makefile Dockerfile docker-compose):
### 🎯 Варианты установки
make build
<table>
<tr>
<th>Сценарий</th>
<th>Описание</th>
<th>Команда</th>
</tr>
<tr>
<td>🏠 <strong>Локальная установка</strong></td>
<td>Панель + бот на одном сервере</td>
<td><code>./bedolaga.sh</code> → вариант 2</td>
</tr>
<tr>
<td>🌐 <strong>Удаленная установка</strong></td>
<td>Бот отдельно от панели</td>
<td><code>./bedolaga.sh</code> → вариант 1</td>
</tr>
<tr>
<td>⚡ <strong>Расширенная</strong></td>
<td>С Redis и Nginx</td>
<td><code>./bedolaga.sh</code> → вариант 3</td>
</tr>
</table>
4. Запуск:
### ⚙️ Ключевые настройки
Запуск минимальной конфигурации (бот + база данных):
<details>
<summary>📋 Основные параметры</summary>
| Переменная | Описание | Пример |
|------------|----------|---------|
| `BOT_TOKEN` | Токен от [@BotFather](https://t.me/BotFather) | `123456:ABC-DEF...` |
| `BOT_USERNAME` | Username бота (без @) | `your_bot` |
| `REMNAWAVE_URL` | URL панели Remnawave | `https://panel.com` |
| `REMNAWAVE_TOKEN` | API токен панели | `your_api_token` |
| `ADMIN_IDS` | ID администраторов | `123456789,987654321` |
</details>
<details>
<summary>🎁 Реферальная программа</summary>
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `REFERRAL_FIRST_REWARD` | Награда за первого реферала | `100.0` |
| `REFERRAL_REFERRED_BONUS` | Бонус приглашенному | `100.0` |
| `REFERRAL_THRESHOLD` | Порог активации (руб.) | `200.0` |
| `REFERRAL_PERCENTAGE` | % с последующих платежей | `0.2` (20%) |
</details>
<details>
<summary>⭐ Telegram Stars</summary>
| Переменная | Описание | Пример |
|------------|----------|---------|
| `STARS_ENABLED` | Включить оплату звездами | `true` |
| `STARS_100_RATE` | Курс 100 звезд → рублей | `110` |
| `STARS_250_RATE` | Курс 250 звезд → рублей | `280` |
| `STARS_500_RATE` | Курс 500 звезд → рублей | `550` |
</details>
<details>
<summary>⭐ Telegram Tribute</summary>
| Переменная | Описание | Пример |
|------------|----------|---------|
| `TRIBUTE_ENABLED` | Включить оплату с помощью доната на Tribute | `false` |
| `TRIBUTE_API_KEY` | API ключ из настроек Tribute | `XXXXXXXXXXX` |
| `TRIBUTE_WEBHOOK_PORT` | Порт | `8081` |
| `TRIBUTE_WEBHOOK_PATH` | URL Webhook Path | `/tribute-webhook` |
| `TRIBUTE_DONATE_LINK` | Ссылка на ваш донат из Tribute | `https://t.me/tribute/app?startapp=XXXXXXXXXXXXX` |
</details>
<details>
<summary>🔍 Мониторинг подписок</summary>
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `MONITOR_ENABLED` | Включить мониторинг | `true` |
| `MONITOR_CHECK_INTERVAL` | Интервал проверки (сек) | `1800` (30 мин) |
| `MONITOR_WARNING_DAYS` | За сколько дней предупреждать | `3` |
| `AUTO_DELETE_ENABLED` | Автоудаление истекших | `true` |
</details>
<details>
<summary> Триал</summary>
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `TRIAL_ENABLED` | Включить триал | `true` |
| `TRIAL_DURATION_DAYS` | Дней триала | `3` |
| `TRIAL_SQUAD_UUID` | Сквад триальной подписки | `` |
| `TRIAL_NOTIFICATION_ENABLED` | Уведомление об истечении триал подписки | `true` |
| `TRIAL_NOTIFICATION_HOURS_AFTER` | Через сколько отсылать сообщение | `1` |
| `TRIAL_NOTIFICATION_HOURS_WINDOW` | Через сколько выслать повторно | `` |
</details>
<details>
<summary>⚙️ Настройка Telegram Tribute</summary>
0) Обновляемся до версии 1.4.0
1) Регистрируемся и проходим верификацию в Tribute
2) Создаем донат ссылку, копируем вставляем в .env бота, конфигурируем остальные параметры из .env.example для работы Tribute
3) Настраиваем обратное прокси на /tribute-webhook
3.1 Пример для докера Caddy
https://test.example.com {
# Tribute webhook endpoint
handle /tribute-webhook* {
reverse_proxy localhost:8081 {
header_up Host {host}
header_up X-Real-IP {remote_host}
}
}
make up-min
# Health check для webhook сервиса
handle /webhook-health {
reverse_proxy localhost:8081/health {
header_up Host {host}
header_up X-Real-IP {remote_host}
}
}
Или запуск с Redis:
make up
3.2 Пример для докера с Nginx
Или запуск со всеми сервисами включая Nginx:
make up-full
server {
listen 80;
server_name yourdomain.com;
5. Управление
location /tribute-webhook {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
client_max_body_size 10M;
}
Просмотр логов:
location /webhook-health {
proxy_pass http://127.0.0.1:8081/health;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
make logs-bot
3.3 Рестартим Бота, рестратим Caddy/Nginx
Статус сервисов:
make status
4) Указываем ссылку на наш хук в настройках Tribute: https://test.example.com/tribute-webhook
Перезапуск:
make restart
Остановка:
make down
5) Делаем тест - если успешно, значит все настроили правильно. Не успешный тест - проверяй доступность хука, где-то ты что-то не так настроил
#Конфигурация
BOT_TOKEN — токен Telegram бота от BotFather.
REMNAWAVE_URL — URL API RemnaWave.
REMNAWAVE_MODE=remote
REMNAWAVE_TOKEN — токен доступа к API RemnaWave.
DATABASE_URL — строка подключения к базе данных.
SUBSCRIPTION_BASE_URL=https://sub.example.com (без / на конце)
ADMIN_IDS — через запятую Telegram ID администраторов.
SUPPORT_USERNAME — ник поддержки, без @ указывать
Параметры тестовой подписки (включение, длительность, трафик, UUID squad, цена).
TRIAL_ENABLED=true/false
TRIAL_DURATION_DAYS=3 (дни)
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=(УКазать UUID сквада из панели!)
TRIAL_PRICE=0.0(не трогать)
Monitor Service Settings (дополнительные настройки)
MONITOR_CHECK_INTERVAL=3600 (Запуск службы проверки)
MONITOR_DAILY_CHECK_HOUR=10 (Разовый чек в определенный промежуток дня)
MONITOR_WARNING_DAYS=2 (За сколько дней слать уведомления)
6) Тестируем пополнение через бота
#Использование
</details>
/start
🐳 Docker Compose примеры
#Структура проекта
<details>
<summary>🏠 Для локальной установки (панель + бот)</summary>
main.py — главный файл запуска.
```yaml
services:
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- remnawave-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
handlers.py — основные обработчики команд и действий пользователя.
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
ports:
- "8081:8081"
networks:
- remnawave-network
admin_handlers.py — обработчики команд и действий администраторов.
volumes:
postgres_data:
database.py — модели и методы работы с базой данных (SQLAlchemy).
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
```
remnawave_api.py — интеграция с API RemnaWave.
</details>
keyboards.py — генерация клавиатур Telegram.
<details>
<summary>🌐 Для удаленной установки</summary>
translations.py — локализация и переводы.
```yaml
services:
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
utils.py — вспомогательные функции.
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
ports:
- "8081:8081"
networks:
- bot_network
middlewares.py — промежуточные слои для обработки сообщений и запросов.
volumes:
postgres_data:
subscription_monitor.py - сервис мониторинга сроков истечения подписок
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
```
.env — файл конфигурации с переменными окружения.
</details>
requirements.txt — список зависимостей Python.
---
run.sh — скрипт установки и управления ботом (опционально).
## 🎯 Использование
#Администрирование
### 👤 Для пользователей
Для входа в админ-панель используйте кнопку "⚙️ Админ панель" в главном меню, если вы указаны как администратор в ADMIN_IDS.
1. **Запуск** → Найдите бота и нажмите `/start`
2. **Язык** → Выберите русский или английский
3. **Баланс** → "💰 Баланс" → "💳 Пополнить" → Telegram Stars
4. **Подписка** → "🛒 Купить подписку" → выбор тарифа → оплата
5. **Управление** → "📋 Мои подписки" → выбор → получение ссылки
6. **Рефералы** → "👥 Рефералы" → поделиться ссылкой
В админ-панели доступны:
### ⚙️ Для администраторов
Управление подписками (создание, редактирование, список, удаление, включение/отключение)
Доступ через кнопку **"⚙️ Админ панель"**:
Управление пользователями (просмотр списка, баланс)
- **📦 Управление подписками** → создание и настройка тарифов
- **👥 Управление пользователями** → поиск, редактирование балансов
- **💰 Управление платежами** → одобрение Telegram Stars платежей
- **🎁 Промокоды** → создание денежных бонусов
- **📨 Рассылки** → уведомления пользователям
- **🖥 Система Remnawave** → мониторинг нод, синхронизация
- **📊 Статистика** → подробная аналитика
Управление балансом (пополнение пользователей через tg id)
---
Управление промокодами (создание, список)
## 📊 Производительность
Одобрение или отклонение платежей (Приходит запрос администраторам)
### 💪 Рекомендуемые ресурсы
Отправка сообщений пользователям или массовая рассылка
| Пользователей | RAM | CPU | Диск | Описание |
|---------------|-----|-----|------|----------|
| **До 500** | 1GB | 1 vCPU | 10GB | Начальная конфигурация |
| **До 1,000** | 2GB | 1 vCPU | 20GB | Малый бизнес |
| **До 10,000** | 4GB | 2 vCPU | 50GB | Средний бизнес |
| **До 50,000** | 8GB | 4 vCPU | 100GB | Крупный бизнес |
Мониторинг подписок (Проверка статуса службы, принудитедьный запуск, деактивация истекщих подписок(на случай падения базы), персональный тест(можно отправить уведомления юзеру по tg id)
### ⚡ Оптимизация
Просмотр краткой статистики
- **Redis** → включите для кэширования (`--profile with-redis`)
- **PostgreSQL** → настройте для production нагрузок
- **Nginx** → используйте как reverse proxy (`--profile with-nginx`)
- **Мониторинг** → отслеживайте через `docker stats`
#ToDo
---
Код колхозный и не без вайбкодинга тут обошлось, но будет допиливаться, текущая реализация работает - уже хорошо
1) Дописать службу для оповещения об истечении срока подписки и контроля - Done v1.1.0
2) Подключить различные шлюзы для пополнения баланса
3) Синхранизацию с Remnawave между пользователями по тг id
4) Полнофункциональную панель упарвления
5) Добавить возможность удаление промокодов - In progress v1.1.1+
6) Доработать алгоритм удаления подписок - In progress v1.1.1+
ибо удаление(А НЕ деактивация) сейчас - скроект эту подписку у всех юзеров которые ее купили,
так что удаляйте на свой страх и риск я предупредил)
8) Отправка уведомлений административных в другие чаты-топики
9) Рефка
(как по мне беспонтовая штука, сервера нормальные хостите, сервис нормальный делайте и будут клиенты - не ебите мозги, но если будет не лень, то допилю)
## 🔧 Управление
### 📋 Основные команды
```bash
# Переход в директорию
cd /opt/bedolaga-bot
# Управление через Docker Compose
docker compose up -d # Запуск
docker compose down # Остановка
docker compose restart bot # Перезапуск бота
docker compose logs -f bot # Логи в реальном времени
# Управление через systemd (если настроено)
sudo systemctl start bedolaga-bot
sudo systemctl stop bedolaga-bot
sudo systemctl restart bedolaga-bot
```
### 🔄 Обновление
```bash
# Автоматическое через меню
sudo ./bedolaga.sh
# Ручное обновление
docker compose down
docker compose pull bot
docker compose up -d
```
### 💾 Резервное копирование
```bash
# Создание backup
docker exec remnawave_bot_db pg_dump -U remnawave_user remnawave_bot > backup.sql
# Восстановление
docker exec -i remnawave_bot_db psql -U remnawave_user remnawave_bot < backup.sql
```
---
## 🐛 Устранение неполадок
### ❓ Частые проблемы
<details>
<summary>🤖 Бот не отвечает</summary>
**Проверьте:**
- ✅ Правильность `BOT_TOKEN`
- ✅ Интернет соединение
- ✅ Логи: `docker compose logs bot`
**Решение:**
```bash
# Перезапуск бота
docker compose restart bot
# Проверка токена
docker exec remnawave_bot env | grep BOT_TOKEN
```
</details>
<details>
<summary>🗄️ Ошибки базы данных</summary>
**Симптомы:**
- SQL ошибки в логах
- Бот не сохраняет данные
**Решение:**
```bash
# Проверка PostgreSQL
docker compose logs postgres
# Подключение к БД
docker exec -it remnawave_bot_db psql -U remnawave_user remnawave_bot
# Экстренное восстановление через меню
sudo ./bedolaga.sh → "10) Экстренное исправление БД"
```
</details>
<details>
<summary>🔌 Проблемы с Remnawave API</summary>
**Проверьте:**
- ✅ Доступность `REMNAWAVE_URL`
- ✅ Валидность `REMNAWAVE_TOKEN`
- ✅ Сетевое подключение
**Диагностика:**
```bash
# Проверка URL
curl -I https://your-panel.com
# Тест API из контейнера
docker exec remnawave_bot curl -I http://remnawave:3000
```
</details>
<details>
<summary>⚠️ Проблема обновления 1.3.3 → 1.3.4</summary>
**Симптом:** SQL ошибки после обновления связанные с автоплатежами
**Решение через меню:**
```bash
sudo ./bedolaga.sh
# Выберите: "10) Экстренное исправление БД (Python)"
# Или: "11) Экстренное исправление БД (SQL)"
```
**Ручное решение:**
1. Скачайте `emergency_fix.py`
2. Добавьте в `docker-compose.yml`:
```yaml
emergency-fix:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
volumes:
- ./emergency_fix.py:/app/emergency_fix.py
environment:
- DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
networks:
- bot_network
profiles:
- emergency
command: python emergency_fix.py
```
3. Запустите: `docker compose run --rm emergency-fix`
</details>
---
## 🗺️ Roadmap
### ✅ Реализовано
- ✅ **Мониторинг подписок** - автоуведомления и контроль
- ✅ **Telegram Stars** - пополнение баланса звездами
- ✅ **Синхронизация Remnawave** - импорт пользователей и статистика
- ✅ **Реферальная система** - полнофункциональная программа
- ✅ **Игра удачи** - ежедневные розыгрыши бонусов
- ✅ **Управление промокодами** - создание, редактирование, статистика
- ✅ **Правила сервиса** - настройка через админ-панель
- ✅ **Автоплатежи** - настраиваемое автопродление подписок
- ✅ **Просмотр подписок пользователей** - детальная статистика
- - ✅ **Автоматическое пополнение лк с помощью доната Tribute**
### 🎯 В планах
| Версия | Функция | Приоритет | Описание |
|--------|---------|-----------|----------|
| **v1.4.5** | Юкасса интеграция | 🔴 High | Автоматические платежи |
| **v1.4.5** | Веб-панель управления | 🟡 Medium | Полный веб-интерфейс |
| **v1.4.5** | Безопасное удаление | 🟡 Medium | Архивирование вместо удаления |
| **v1.5.0** | Дополнительные платежи | 🟡 Medium | Сбербанк, Tinkoff, Crypto |
| **v1.5.0** | Уведомления | 🟡 Medium | Webhook, Email, другие чаты |
| **v1.6.0** | Система блокировок | 🟢 Low | Бан/разбан пользователей |
### 💡 Хотите добавить функцию?
- 🐛 [Сообщите о баге](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/issues)
- ✨ [Предложите улучшение](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/discussions)
- 🔧 [Создайте Pull Request](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/pulls)
---
## 🤝 Вклад в проект
### 💪 Как помочь
**🔧 Разработчикам:**
- Fork репозитория
- Создайте feature branch: `git checkout -b feature/amazing-feature`
- Внесите изменения и сделайте commit: `git commit -m 'Add amazing feature'`
- Push в branch: `git push origin feature/amazing-feature`
- Создайте Pull Request
**🐞 Пользователям:**
- Сообщайте о багах в [Issues](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/issues)
- Предлагайте идеи в [Discussions](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/discussions)
- Ставьте ⭐ проекту
- Рассказывайте друзьям
**💰 Спонсорам:**
- Поддержите разработку
- Закажите приоритетные функции
- Получите корпоративную поддержку
---
## 💬 Поддержка
### 📞 Контакты
- **Telegram:** [@fringg](https://t.me/fringg)
- **Issues:** [GitHub Issues](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/issues)
- **Discussions:** [GitHub Discussions](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/discussions)
### 📚 Полезные ссылки
- [📖 Документация Remnawave](https://docs.remna.st)
- [🤖 Создание Telegram бота](https://t.me/BotFather)
- [🐳 Docker документация](https://docs.docker.com)
- [🐘 PostgreSQL документация](https://www.postgresql.org/docs)
---
## 📄 Лицензия
Проект распространяется под лицензией **MIT**. Подробности в файле [LICENSE](LICENSE).
---
<div align="center">
### 🌟 Понравился проект? Поставьте звезду!
[![Star History Chart](https://api.star-history.com/svg?repos=Fr1ngg/remnawave-bedolaga-telegram-bot&type=Date)](https://star-history.com/#Fr1ngg/remnawave-bedolaga-telegram-bot&Date)
---
**💝 Создано с любовью для Remnawave сообщества**
*Автор не является профессиональным разработчиком, но прикладывает все усилия для создания удобного бота для ваших сервисов* 💪
[🔝 Вернуться наверх](#-remnawave-bedolaga-bot)
</div>
+7457 -186
View File
File diff suppressed because it is too large Load Diff
+201
View File
@@ -0,0 +1,201 @@
import logging
from typing import Optional, Dict, Any, Callable
from aiogram.types import CallbackQuery
from aiogram import Router
from remnawave_api import RemnaWaveAPI
from translations import t
logger = logging.getLogger(__name__)
class APIErrorHandler:
@staticmethod
async def handle_api_error(callback: CallbackQuery, error: Exception,
operation: str, user_language: str = 'ru',
fallback_keyboard=None) -> bool:
error_message = str(error).lower()
if "timeout" in error_message or "connection" in error_message:
text = "⏱ Таймаут подключения к API\n\n"
text += "Возможные причины:\n"
text += "• Медленный интернет\n"
text += "• Перегрузка сервера RemnaWave\n"
text += "• Временные проблемы с сетью\n\n"
text += "🔄 Попробуйте повторить операцию через несколько секунд"
elif "401" in error_message or "unauthorized" in error_message:
text = "🔐 Ошибка авторизации API\n\n"
text += "Токен доступа недействителен или истек.\n"
text += "Обратитесь к администратору для обновления токена."
elif "404" in error_message or "not found" in error_message:
text = f"❌ Ресурс не найден\n\n"
text += f"Операция: {operation}\n"
text += "Возможно, запрашиваемый объект был удален или не существует."
elif "500" in error_message or "internal server error" in error_message:
text = "🔥 Внутренняя ошибка сервера RemnaWave\n\n"
text += "Сервер временно недоступен.\n"
text += "Попробуйте повторить операцию позже."
else:
text = f"❌ Ошибка API операции: {operation}\n\n"
text += f"Детали: {str(error)[:100]}{'...' if len(str(error)) > 100 else ''}\n\n"
text += "Обратитесь к администратору если проблема повторяется."
try:
await callback.message.edit_text(
text,
reply_markup=fallback_keyboard or error_recovery_keyboard(operation, user_language)
)
return True
except Exception as edit_error:
logger.error(f"Failed to edit message with error info: {edit_error}")
try:
await callback.answer(f"❌ Ошибка: {operation}", show_alert=True)
return True
except:
return False
@staticmethod
async def safe_api_call(api_method: Callable, *args, **kwargs) -> tuple[bool, Any]:
try:
result = await api_method(*args, **kwargs)
return True, result
except Exception as e:
logger.error(f"API call failed: {api_method.__name__} - {e}")
return False, str(e)
def create_error_recovery_keyboard(error_context: str, language: str = 'ru'):
from keyboards import error_recovery_keyboard
return error_recovery_keyboard(error_context, language)
async def safe_get_nodes(api: RemnaWaveAPI) -> tuple[bool, list]:
try:
logger.info("Attempting to fetch nodes from API...")
nodes = await api.get_all_nodes()
if nodes is None:
logger.warning("API returned None for nodes")
return False, []
if not isinstance(nodes, list):
logger.warning(f"API returned non-list for nodes: {type(nodes)}")
return False, []
logger.info(f"Successfully fetched {len(nodes)} nodes")
return True, nodes
except Exception as e:
logger.error(f"Error fetching nodes: {e}")
return False, []
async def safe_get_system_users(api: RemnaWaveAPI) -> tuple[bool, list]:
try:
logger.info("Attempting to fetch system users from API...")
users = await api.get_all_system_users_full()
if users is None:
logger.warning("API returned None for users")
return False, []
if not isinstance(users, list):
logger.warning(f"API returned non-list for users: {type(users)}")
return False, []
logger.info(f"Successfully fetched {len(users)} users")
return True, users
except Exception as e:
logger.error(f"Error fetching system users: {e}")
return False, []
async def safe_restart_nodes(api: RemnaWaveAPI, all_nodes: bool = True, node_id: str = None) -> tuple[bool, str]:
try:
if all_nodes:
logger.info("Attempting to restart all nodes...")
result = await api.restart_all_nodes()
else:
logger.info(f"Attempting to restart node {node_id}...")
result = await api.restart_node(node_id)
if result:
message = "Команда перезагрузки отправлена успешно"
logger.info(f"Restart command sent successfully")
return True, message
else:
message = "API вернул отрицательный результат"
logger.warning("API returned negative result for restart")
return False, message
except Exception as e:
logger.error(f"Error restarting nodes: {e}")
return False, str(e)
async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
health_info = {
'api_available': False,
'nodes_accessible': False,
'users_accessible': False,
'system_stats_accessible': False,
'errors': []
}
if api is None:
health_info['errors'].append("API instance is None")
return health_info
try:
success, nodes = await safe_get_nodes(api)
if success:
health_info['api_available'] = True
health_info['nodes_accessible'] = True
else:
health_info['errors'].append("Cannot fetch nodes")
except Exception as e:
health_info['errors'].append(f"Nodes check failed: {e}")
try:
success, users = await safe_get_system_users(api)
if success:
health_info['users_accessible'] = True
else:
health_info['errors'].append("Cannot fetch users")
except Exception as e:
health_info['errors'].append(f"Users check failed: {e}")
try:
stats = await api.get_system_stats()
if stats:
health_info['system_stats_accessible'] = True
else:
health_info['errors'].append("Cannot fetch system stats")
except Exception as e:
health_info['errors'].append(f"System stats check failed: {e}")
return health_info
def handle_api_errors(operation_name: str):
def decorator(func):
async def wrapper(callback: CallbackQuery, user, *args, **kwargs):
try:
return await func(callback, user, *args, **kwargs)
except Exception as e:
logger.error(f"Error in {func.__name__}: {e}")
api = kwargs.get('api')
fallback_keyboard = None
if 'nodes' in operation_name.lower():
from keyboards import admin_system_keyboard
fallback_keyboard = admin_system_keyboard(user.language)
elif 'users' in operation_name.lower():
from keyboards import system_users_keyboard
fallback_keyboard = system_users_keyboard(user.language)
await APIErrorHandler.handle_api_error(
callback, e, operation_name, user.language, fallback_keyboard
)
return wrapper
return decorator
+295
View File
@@ -0,0 +1,295 @@
<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Градиенты -->
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1" />
<stop offset="100%" style="stop-color:#f093fb;stop-opacity:1" />
</linearGradient>
<linearGradient id="skinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#d4a574;stop-opacity:1" />
<stop offset="100%" style="stop-color:#b8935f;stop-opacity:1" />
</linearGradient>
<linearGradient id="redSkinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#e8b598;stop-opacity:1" />
<stop offset="100%" style="stop-color:#d4a574;stop-opacity:1" />
</linearGradient>
<linearGradient id="clothesGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3a3a3a;stop-opacity:1" />
<stop offset="100%" style="stop-color:#595959;stop-opacity:1" />
</linearGradient>
<linearGradient id="bottleGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2d5016;stop-opacity:0.8" />
<stop offset="100%" style="stop-color:#1a2f0d;stop-opacity:0.9" />
</linearGradient>
<!-- Фильтры -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="dropshadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
<feOffset dx="2" dy="4" result="offset" />
<feComponentTransfer>
<feFuncA type="linear" slope="0.3"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Фон -->
<rect width="400" height="400" fill="#1a1a2e" rx="50"/>
<!-- Декоративные элементы фона -->
<circle cx="50" cy="80" r="30" fill="url(#mainGradient)" opacity="0.1"/>
<circle cx="350" cy="120" r="40" fill="#8B4513" opacity="0.1"/>
<!-- Волны RemnaWave в фоне -->
<g transform="translate(200,200)" opacity="0.2">
<path d="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
stroke="url(#mainGradient)"
stroke-width="4"
fill="none">
<animate attributeName="d"
values="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100;M-120,-100 Q-60,-70 0,-100 Q60,-130 120,-100;M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
dur="4s"
repeatCount="indefinite"/>
</path>
</g>
<!-- Главный персонаж - Бомж-алкаш -->
<g transform="translate(200,180)" filter="url(#dropshadow)">
<!-- Тело в рваной куртке -->
<ellipse cx="0" cy="30" rx="45" ry="60" fill="url(#clothesGradient)"/>
<!-- Рваные края одежды -->
<path d="M-45,70 L-40,75 L-35,70 L-30,75 L-25,70 L-20,75 L-15,70 L-10,75 L-5,70 L0,75 L5,70 L10,75 L15,70 L20,75 L25,70 L30,75 L35,70 L40,75 L45,70"
stroke="none" fill="#2a2a2a"/>
<!-- Заплатки -->
<rect x="-20" y="10" width="12" height="10" fill="#5a4a3a" opacity="0.7" rx="2"/>
<rect x="15" y="35" width="10" height="8" fill="#4a4a4a" opacity="0.8" rx="1"/>
<rect x="-10" y="45" width="8" height="12" fill="#6a5a4a" opacity="0.6" rx="1"/>
<!-- Шея -->
<rect x="-12" y="-25" width="24" height="20" fill="url(#redSkinGradient)" rx="8"/>
<!-- Голова (более человечная форма) -->
<ellipse cx="0" cy="-45" rx="28" ry="32" fill="url(#redSkinGradient)"/>
<!-- Сальные волосы -->
<path d="M-25,-70 Q-20,-75 -15,-72 Q-10,-78 -5,-73 Q0,-80 5,-73 Q10,-78 15,-72 Q20,-75 25,-70"
stroke="#3a3a3a" stroke-width="4" fill="#3a3a3a" stroke-linecap="round"/>
<path d="M-28,-65 Q-25,-70 -20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M28,-65 Q25,-70 20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
<!-- Левое ухо -->
<ellipse cx="-28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
<!-- Правое ухо -->
<ellipse cx="28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
<!-- Опухшие глаза -->
<ellipse cx="-10" cy="-50" rx="8" ry="5" fill="white"/>
<ellipse cx="10" cy="-50" rx="8" ry="5" fill="white"/>
<!-- Зрачки (косящие) -->
<g id="eyes">
<circle cx="-8" cy="-49" r="3" fill="#333">
<animate attributeName="cx" values="-8;-10;-8" dur="3s" repeatCount="indefinite"/>
</circle>
<circle cx="12" cy="-49" r="3" fill="#333">
<animate attributeName="cx" values="12;10;12" dur="3s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Красные глаза -->
<path d="M-18,-50 L-15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
<path d="M-17,-48 L-14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
<path d="M18,-50 L15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
<path d="M17,-48 L14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
<!-- Мешки под глазами -->
<ellipse cx="-10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
<ellipse cx="10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
<!-- Красный нос алкаша -->
<ellipse cx="0" cy="-38" rx="6" ry="5" fill="#d67070"/>
<circle cx="-2" cy="-39" r="1" fill="#e08080" opacity="0.8"/>
<!-- Небритость -->
<g opacity="0.6">
<circle cx="-15" cy="-30" r="1" fill="#555"/>
<circle cx="-12" cy="-28" r="0.8" fill="#555"/>
<circle cx="-8" cy="-30" r="0.8" fill="#555"/>
<circle cx="15" cy="-30" r="1" fill="#555"/>
<circle cx="12" cy="-28" r="0.8" fill="#555"/>
<circle cx="8" cy="-30" r="0.8" fill="#555"/>
<circle cx="0" cy="-28" r="0.8" fill="#555"/>
<circle cx="-5" cy="-26" r="0.8" fill="#555"/>
<circle cx="5" cy="-26" r="0.8" fill="#555"/>
</g>
<!-- Рот с сигаретой/бутылкой (анимированный) -->
<g id="mouth-group">
<!-- Основной рот -->
<path d="M-12,-20 Q0,-18 12,-20" stroke="#444" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Сигарета (появляется и исчезает) -->
<g id="cigarette">
<rect x="10" y="-22" width="25" height="3" fill="white" rx="1">
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
</rect>
<rect x="30" y="-22" width="5" height="3" fill="#ff6b35">
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
</rect>
<!-- Дым от сигареты -->
<g opacity="0.4">
<circle cx="35" cy="-22" r="2" fill="#999">
<animate attributeName="opacity" values="0;0;0;0.6;0.3;0;0;0;0" dur="8s" repeatCount="indefinite"/>
<animate attributeName="cy" values="-22;-22;-22;-28;-35;-40;-22;-22;-22" dur="8s" repeatCount="indefinite"/>
<animate attributeName="r" values="2;2;2;4;6;8;2;2;2" dur="8s" repeatCount="indefinite"/>
</circle>
</g>
</g>
</g>
<!-- Левая рука с бутылкой -->
<g id="left-arm">
<!-- Рука -->
<line x1="-45" y1="10" x2="-55" y2="-5" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
<animate attributeName="x2" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y2" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
</line>
<!-- Кисть -->
<circle cx="-55" cy="-5" r="6" fill="url(#skinGradient)">
<animate attributeName="cx" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
<animate attributeName="cy" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
</circle>
<!-- Бутылка -->
<g id="bottle">
<rect x="-62" y="-25" width="14" height="30" fill="url(#bottleGradient)" rx="2">
<animate attributeName="x" values="-62;-57;-62" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-25;-40;-25" dur="8s" repeatCount="indefinite"/>
<animate attributeName="transform" values="rotate(0 -55 -10);rotate(-30 -50 -25);rotate(0 -55 -10)" dur="8s" repeatCount="indefinite"/>
</rect>
<rect x="-58" y="-30" width="6" height="8" fill="#2d5016">
<animate attributeName="x" values="-58;-53;-58" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-30;-45;-30" dur="8s" repeatCount="indefinite"/>
</rect>
<!-- Этикетка -->
<rect x="-60" y="-15" width="10" height="8" fill="white" opacity="0.3">
<animate attributeName="x" values="-60;-55;-60" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-15;-30;-15" dur="8s" repeatCount="indefinite"/>
</rect>
</g>
</g>
<!-- Правая рука (свободная, дрожащая) -->
<g id="right-arm">
<line x1="45" y1="10" x2="55" y2="20" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
<animate attributeName="x2" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
</line>
<circle cx="55" cy="20" r="6" fill="url(#skinGradient)">
<animate attributeName="cx" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Ноги в рваных штанах -->
<rect x="-15" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
<rect x="3" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
<!-- Дырки в штанах -->
<ellipse cx="-9" cy="95" rx="3" ry="4" fill="#1a1a2e"/>
<ellipse cx="9" cy="105" rx="2" ry="3" fill="#1a1a2e"/>
<!-- Старые ботинки -->
<ellipse cx="-9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
<ellipse cx="9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
<!-- Дырки в ботинках -->
<circle cx="-6" cy="123" r="2" fill="#1a1a2e"/>
<circle cx="12" cy="123" r="1.5" fill="#1a1a2e"/>
<!-- Значок "B" на груди -->
<circle cx="0" cy="25" r="20" fill="url(#mainGradient)" opacity="0.8" stroke="white" stroke-width="1"/>
<text x="0" y="35"
font-family="Arial Black, sans-serif"
font-size="28"
font-weight="bold"
text-anchor="middle"
fill="white">
B
</text>
</g>
<!-- Упавшие бутылки вокруг -->
<g opacity="0.6">
<rect x="80" y="340" width="8" height="20" fill="#2d5016" rx="1" transform="rotate(75 84 350)"/>
<rect x="310" y="330" width="7" height="18" fill="#3a5a1a" rx="1" transform="rotate(-60 313 339)"/>
<rect x="150" y="350" width="6" height="16" fill="#2d5016" rx="1" transform="rotate(45 153 358)"/>
</g>
<!-- Окурки -->
<g opacity="0.7">
<rect x="120" y="360" width="12" height="2" fill="white" rx="1"/>
<rect x="130" y="360" width="2" height="2" fill="#ff6b35"/>
<rect x="250" y="355" width="10" height="2" fill="white" rx="1" transform="rotate(30 255 356)"/>
<rect x="258" y="355" width="2" height="2" fill="#ff6b35" transform="rotate(30 259 356)"/>
</g>
<!-- Текст логотипа -->
<text x="200" y="320"
font-family="Arial Black, sans-serif"
font-size="20"
font-weight="bold"
text-anchor="middle"
fill="url(#mainGradient)"
filter="url(#glow)">
Remnawave
</text>
<text x="200" y="345"
font-family="Arial Black, sans-serif"
font-size="18"
font-weight="bold"
text-anchor="middle"
fill="#d67070">
Bedolaga Bot
</text>
<!-- Подпись -->
<text x="200" y="365"
font-family="Arial, sans-serif"
font-size="11"
text-anchor="middle"
fill="#8892b0"
opacity="0.8">
Telegram VPN Manager
</text>
<!-- Мухи летающие вокруг (признак антисанитарии) -->
<g opacity="0.5">
<circle cx="150" cy="150" r="1" fill="#333">
<animateTransform attributeName="transform" type="rotate" values="0 200 180;360 200 180" dur="6s" repeatCount="indefinite"/>
</circle>
<circle cx="250" cy="150" r="1" fill="#333">
<animateTransform attributeName="transform" type="rotate" values="360 200 180;0 200 180" dur="5s" repeatCount="indefinite"/>
</circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+284
View File
@@ -0,0 +1,284 @@
import asyncio
import logging
from datetime import datetime, timedelta
from typing import List, Optional
from database import Database, UserSubscription, User, Subscription
from remnawave_api import RemnaWaveAPI
from referral_utils import process_referral_rewards
logger = logging.getLogger(__name__)
class AutoPayService:
def __init__(self, db: Database, api: Optional[RemnaWaveAPI] = None, bot=None):
self.db = db
self.api = api
self.bot = bot
self.is_running = False
self.check_task = None
async def start(self):
if self.is_running:
logger.warning("AutoPay service is already running")
return
self.is_running = True
logger.info("🔄 Starting AutoPay service...")
self.check_task = asyncio.create_task(self._periodic_check())
async def stop(self):
if not self.is_running:
return
self.is_running = False
logger.info("⏹ Stopping AutoPay service...")
if self.check_task:
self.check_task.cancel()
try:
await self.check_task
except asyncio.CancelledError:
pass
async def _periodic_check(self):
while self.is_running:
try:
await self.process_autopayments()
# Проверяем каждые 30 минут
await asyncio.sleep(1800)
except asyncio.CancelledError:
logger.info("AutoPay periodic check cancelled")
break
except Exception as e:
logger.error(f"Error in AutoPay periodic check: {e}")
await asyncio.sleep(300)
async def process_autopayments(self) -> dict:
logger.info("🔄 Processing autopayments...")
try:
subscriptions_to_pay = await self.db.get_subscriptions_for_autopay()
if not subscriptions_to_pay:
logger.info("No subscriptions ready for autopay")
return {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': []
}
logger.info(f"Found {len(subscriptions_to_pay)} subscriptions for autopay")
stats = {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': []
}
for user_sub in subscriptions_to_pay:
try:
result = await self._process_single_autopayment(user_sub)
stats['processed'] += 1
if result['success']:
stats['successful'] += 1
logger.info(f"✅ Autopay successful for user {user_sub.user_id}, subscription {user_sub.id}")
elif result['reason'] == 'insufficient_balance':
stats['insufficient_balance'] += 1
logger.info(f"💳 Insufficient balance for user {user_sub.user_id}, subscription {user_sub.id}")
else:
stats['failed'] += 1
stats['errors'].append(f"User {user_sub.user_id}: {result['reason']}")
logger.warning(f"❌ Autopay failed for user {user_sub.user_id}: {result['reason']}")
except Exception as e:
stats['processed'] += 1
stats['failed'] += 1
error_msg = f"User {user_sub.user_id}: {str(e)}"
stats['errors'].append(error_msg)
logger.error(f"Error processing autopay for user {user_sub.user_id}: {e}")
logger.info(f"📊 Autopay processing complete: {stats['successful']} successful, "
f"{stats['failed']} failed, {stats['insufficient_balance']} insufficient balance")
return stats
except Exception as e:
logger.error(f"Error in process_autopayments: {e}")
return {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': [str(e)]
}
async def _process_single_autopayment(self, user_sub: UserSubscription) -> dict:
try:
user = await self.db.get_user_by_telegram_id(user_sub.user_id)
if not user:
return {'success': False, 'reason': 'User not found'}
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
return {'success': False, 'reason': 'Subscription plan not found'}
if subscription.is_trial:
logger.info(f"Skipping autopay for trial subscription: user {user_sub.user_id}")
return {'success': False, 'reason': 'Trial subscriptions are not eligible for autopay'}
if not user_sub.is_active or not user_sub.auto_pay_enabled:
return {'success': False, 'reason': 'Subscription inactive or autopay disabled'}
if user.balance < subscription.price:
await self._notify_insufficient_balance(user, subscription, user_sub)
return {'success': False, 'reason': 'insufficient_balance'}
return await self._execute_autopayment(user, subscription, user_sub)
except Exception as e:
logger.error(f"Error in _process_single_autopayment: {e}")
return {'success': False, 'reason': str(e)}
async def _execute_autopayment(self, user: User, subscription: Subscription, user_sub: UserSubscription) -> dict:
try:
user.balance -= subscription.price
await self.db.update_user(user)
now = datetime.utcnow()
if user_sub.expires_at > now:
new_expiry = user_sub.expires_at + timedelta(days=subscription.duration_days)
else:
new_expiry = now + timedelta(days=subscription.duration_days)
user_sub.expires_at = new_expiry
user_sub.is_active = True
await self.db.update_user_subscription(user_sub)
if self.api and user_sub.short_uuid:
try:
remna_user_details = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
if remna_user_details:
user_uuid = remna_user_details.get('uuid')
if user_uuid:
expiry_str = new_expiry.isoformat() + 'Z'
update_data = {
'enable': True,
'expireAt': expiry_str
}
await self.api.update_user(user_uuid, update_data)
logger.info(f"Updated RemnaWave expiry for user {user_sub.user_id}")
except Exception as e:
logger.warning(f"Failed to update RemnaWave expiry: {e}")
payment = await self.db.create_payment(
user_id=user_sub.user_id,
amount=-subscription.price,
payment_type='autopay',
description=f'Автоплатеж: {subscription.name}',
status='completed'
)
if self.bot:
try:
await process_referral_rewards(
user_sub.user_id,
subscription.price,
payment.id,
self.db,
self.bot,
payment_type='autopay'
)
except Exception as e:
logger.warning(f"Failed to process referral rewards for autopay: {e}")
await self._notify_successful_autopay(user, subscription, user_sub, new_expiry)
return {'success': True, 'reason': 'Payment processed successfully'}
except Exception as e:
logger.error(f"Error executing autopayment: {e}")
try:
user.balance += subscription.price
await self.db.update_user(user)
except:
pass
return {'success': False, 'reason': str(e)}
async def _notify_successful_autopay(self, user: User, subscription: Subscription,
user_sub: UserSubscription, new_expiry: datetime):
if not self.bot:
return
try:
from utils import format_datetime
text = f"✅ Автоматическое продление подписки\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Списано: {subscription.price} руб.\n"
text += f"📅 Продлено до: {format_datetime(new_expiry, user.language)}\n"
text += f"💳 Остаток на балансе: {user.balance} руб.\n\n"
text += f"🔄 Следующее продление произойдет автоматически за {user_sub.auto_pay_days_before} дн. до истечения.\n\n"
text += f"ℹ️ Для отключения автоплатежа перейдите в 'Мои подписки' → выберите подписку"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
[InlineKeyboardButton(text="💰 Баланс", callback_data="balance")]
])
await self.bot.send_message(
user.telegram_id,
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Failed to send autopay success notification: {e}")
async def _notify_insufficient_balance(self, user: User, subscription: Subscription, user_sub: UserSubscription):
if not self.bot:
return
try:
from utils import format_datetime
needed = subscription.price - user.balance
days_left = (user_sub.expires_at - datetime.utcnow()).days
text = f"⚠️ Не удалось автоматически продлить подписку\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Нужно для продления: {subscription.price} руб.\n"
text += f"💳 Ваш баланс: {user.balance} руб.\n"
text += f"💸 Недостает: {needed} руб.\n\n"
text += f"📅 Подписка истекает: {format_datetime(user_sub.expires_at, user.language)}\n"
text += f"⏰ Осталось дней: {days_left}\n\n"
text += f"💡 Пополните баланс для автоматического продления"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")]
])
await self.bot.send_message(
user.telegram_id,
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Failed to send insufficient balance notification: {e}")
async def get_service_status(self) -> dict:
return {
'is_running': self.is_running,
'check_interval': 1800,
'has_api': self.api is not None,
'has_bot': self.bot is not None
}
+1434
View File
File diff suppressed because it is too large Load Diff
+131 -47
View File
@@ -1,73 +1,157 @@
import os
from dataclasses import dataclass
from typing import List
from typing import List, Dict
@dataclass
class Config:
BOT_TOKEN: str
BOT_USERNAME: str
DATABASE_URL: str
REMNAWAVE_URL: str
REMNAWAVE_TOKEN: str
REMNAWAVE_MODE: str
DATABASE_URL: str
ADMIN_IDS: List[int]
DEFAULT_LANGUAGE: str
SUPPORT_USERNAME: str
# Subscription URL settings
SUBSCRIPTION_BASE_URL: str
# Trial subscription settings
ADMIN_IDS: List[int]
SUPPORT_USERNAME: str
DEFAULT_LANGUAGE: str
TRIAL_ENABLED: bool
TRIAL_DURATION_DAYS: int
TRIAL_TRAFFIC_GB: int
TRIAL_SQUAD_UUID: str
TRIAL_PRICE: float
REFERRAL_FIRST_REWARD: float
REFERRAL_REFERRED_BONUS: float
REFERRAL_THRESHOLD: float
REFERRAL_PERCENTAGE: float
MONITOR_ENABLED: bool = True
MONITOR_CHECK_INTERVAL: int = 3600
MONITOR_DAILY_CHECK_HOUR: int = 10
MONITOR_WARNING_DAYS: int = 3
DELETE_EXPIRED_TRIAL_DAYS: int = 1
DELETE_EXPIRED_REGULAR_DAYS: int = 7
AUTO_DELETE_ENABLED: bool = False
TRIAL_NOTIFICATION_ENABLED: bool = True
TRIAL_NOTIFICATION_HOURS_AFTER: int = 2
TRIAL_NOTIFICATION_HOURS_WINDOW: int = 22
TRIBUTE_ENABLED: bool = False
TRIBUTE_API_KEY: str = ""
TRIBUTE_WEBHOOK_PORT: int = 8081
TRIBUTE_WEBHOOK_PATH: str = "/tribute-webhook"
TRIBUTE_DONATE_URL: str = ""
TRIBUTE_DONATE_LINK: str = ""
LUCKY_GAME_ENABLED: bool = True
LUCKY_GAME_REWARD: float = 50.0
LUCKY_GAME_NUMBERS: int = 30
LUCKY_GAME_WINNING_COUNT: int = 3
# Monitor service settings
MONITOR_CHECK_INTERVAL: int
MONITOR_DAILY_CHECK_HOUR: int
MONITOR_WARNING_DAYS: int
STARS_ENABLED: bool = True
STARS_RATES: Dict[int, float] = None
def load_config() -> Config:
"""Load configuration from environment variables"""
# Parse admin IDs
admin_ids_str = os.getenv('ADMIN_IDS', '')
admin_ids = []
if admin_ids_str:
def parse_admin_ids(admin_ids_str: str) -> List[int]:
if not admin_ids_str:
return []
try:
admin_ids = [int(x.strip()) for x in admin_ids_str.split(',') if x.strip()]
except ValueError:
admin_ids = []
return [int(id_str.strip()) for id_str in admin_ids_str.split(',') if id_str.strip()]
except (ValueError, AttributeError):
return []
# Get subscription base URL -
subscription_base_url = os.getenv('SUBSCRIPTION_BASE_URL', '')
def get_bool(key: str, default: bool = False) -> bool:
value = os.getenv(key, '').lower()
return value in ('true', '1', 'yes', 'on')
# Если SUBSCRIPTION_BASE_URL не установлен, используем значение по умолчанию
if not subscription_base_url:
subscription_base_url = 'https://sub.fring.tech'
def get_int(key: str, default: int = 0) -> int:
try:
return int(os.getenv(key, str(default)))
except (ValueError, TypeError):
return default
def get_float(key: str, default: float = 0.0) -> float:
try:
return float(os.getenv(key, str(default)))
except (ValueError, TypeError):
return default
def parse_stars_rates() -> Dict[int, float]:
default_rates = {
100: 150.0,
150: 220.0,
250: 400.0,
350: 500.0,
500: 800.0,
750: 1150.0,
1000: 1500.0
}
custom_rates = {}
for stars in [100, 150, 250, 350, 500, 750, 1000]:
env_key = f"STARS_{stars}_RATE"
rate = get_float(env_key, 0.0)
if rate > 0:
custom_rates[stars] = rate
if custom_rates:
return custom_rates
return default_rates
return Config(
BOT_TOKEN=os.getenv('BOT_TOKEN', ''),
BOT_USERNAME=os.getenv('BOT_USERNAME', ''),
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
REMNAWAVE_URL=os.getenv('REMNAWAVE_URL', ''),
REMNAWAVE_TOKEN=os.getenv('REMNAWAVE_TOKEN', ''),
REMNAWAVE_MODE=os.getenv('REMNAWAVE_MODE', 'local'),
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
ADMIN_IDS=admin_ids,
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
SUBSCRIPTION_BASE_URL=os.getenv('SUBSCRIPTION_BASE_URL', ''),
ADMIN_IDS=parse_admin_ids(os.getenv('ADMIN_IDS', '')),
SUPPORT_USERNAME=os.getenv('SUPPORT_USERNAME', 'support'),
# Subscription URL
SUBSCRIPTION_BASE_URL=subscription_base_url,
# Trial subscription settings
TRIAL_ENABLED=os.getenv('TRIAL_ENABLED', 'true').lower() == 'true',
TRIAL_DURATION_DAYS=int(os.getenv('TRIAL_DURATION_DAYS', '3')),
TRIAL_TRAFFIC_GB=int(os.getenv('TRIAL_TRAFFIC_GB', '2')),
TRIAL_SQUAD_UUID=os.getenv('TRIAL_SQUAD_UUID', '19bd5bde-5eea-4368-809c-6ba1ffb93897'),
TRIAL_PRICE=float(os.getenv('TRIAL_PRICE', '0.0')),
# Monitor service settings
MONITOR_CHECK_INTERVAL=int(os.getenv('MONITOR_CHECK_INTERVAL', '3600')),
MONITOR_DAILY_CHECK_HOUR=int(os.getenv('MONITOR_DAILY_CHECK_HOUR', '10')),
MONITOR_WARNING_DAYS=int(os.getenv('MONITOR_WARNING_DAYS', '2'))
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
TRIAL_ENABLED=get_bool('TRIAL_ENABLED', False),
TRIAL_DURATION_DAYS=get_int('TRIAL_DURATION_DAYS', 3),
TRIAL_TRAFFIC_GB=get_int('TRIAL_TRAFFIC_GB', 2),
TRIAL_SQUAD_UUID=os.getenv('TRIAL_SQUAD_UUID', ''),
REFERRAL_FIRST_REWARD=get_float('REFERRAL_FIRST_REWARD', 150.0),
REFERRAL_REFERRED_BONUS=get_float('REFERRAL_REFERRED_BONUS', 150.0),
REFERRAL_THRESHOLD=get_float('REFERRAL_THRESHOLD', 300.0),
REFERRAL_PERCENTAGE=get_float('REFERRAL_PERCENTAGE', 0.25),
MONITOR_ENABLED=get_bool('MONITOR_ENABLED', True),
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600),
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10),
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3),
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
TRIAL_NOTIFICATION_ENABLED=get_bool('TRIAL_NOTIFICATION_ENABLED', True),
TRIAL_NOTIFICATION_HOURS_AFTER=get_int('TRIAL_NOTIFICATION_HOURS_AFTER', 2),
TRIAL_NOTIFICATION_HOURS_WINDOW=get_int('TRIAL_NOTIFICATION_HOURS_WINDOW', 22),
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3),
STARS_ENABLED=get_bool('STARS_ENABLED', True),
STARS_RATES=parse_stars_rates(),
TRIBUTE_ENABLED=get_bool('TRIBUTE_ENABLED', False),
TRIBUTE_API_KEY=os.getenv('TRIBUTE_API_KEY', ''),
TRIBUTE_WEBHOOK_PORT=get_int('TRIBUTE_WEBHOOK_PORT', 8081),
TRIBUTE_WEBHOOK_PATH=os.getenv('TRIBUTE_WEBHOOK_PATH', '/tribute-webhook'),
TRIBUTE_DONATE_URL=os.getenv('TRIBUTE_DONATE_URL', ''),
TRIBUTE_DONATE_LINK=os.getenv('TRIBUTE_DONATE_LINK', '')
)
def debug_environment():
env_vars = [
'BOT_TOKEN', 'BOT_USERNAME', 'DATABASE_URL',
'REMNAWAVE_URL', 'REMNAWAVE_TOKEN', 'ADMIN_IDS',
'REFERRAL_FIRST_REWARD', 'REFERRAL_THRESHOLD',
'MONITOR_ENABLED', 'MONITOR_CHECK_INTERVAL',
'DELETE_EXPIRED_TRIAL_DAYS', 'DELETE_EXPIRED_REGULAR_DAYS', 'AUTO_DELETE_ENABLED',
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD',
'STARS_ENABLED', 'STARS_100_RATE', 'STARS_500_RATE', 'STARS_1000_RATE'
]
print("📋 Environment variables:")
for var in env_vars:
value = os.getenv(var, 'NOT SET')
if 'TOKEN' in var and value != 'NOT SET':
value = value[:10] + "..." if len(value) > 10 else value
print(f" {var}: {value}")
+2097 -43
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
"""
Экстренное исправление проблемы с отображением подписок
Этот патч добавляет недостающие поля в таблицу user_subscriptions
"""
import asyncio
import sys
import os
from pathlib import Path
# Добавляем корневую директорию в путь
sys.path.insert(0, str(Path(__file__).parent))
from config import load_config
from database import Database
from sqlalchemy import text
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def check_and_add_column(db, column_name, column_definition):
"""Проверяет и добавляет колонку в таблицу"""
try:
# Отдельная транзакция для проверки
async with db.engine.begin() as conn:
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
logger.info(f"✅ Поле {column_name} уже существует")
return True
except Exception:
# Отдельная транзакция для добавления колонки
try:
async with db.engine.begin() as conn:
logger.info(f" Добавляю поле {column_name}...")
await conn.execute(text(f"""
ALTER TABLE user_subscriptions
ADD COLUMN {column_name} {column_definition}
"""))
logger.info(f"✅ Поле {column_name} добавлено")
return True
except Exception as e:
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
return False
async def emergency_fix():
"""Экстренное исправление базы данных"""
try:
# Загружаем конфигурацию
config = load_config()
# Подключаемся к базе данных
db = Database(config.DATABASE_URL)
logger.info("🔧 Выполняю экстренное исправление базы данных...")
# Проверяем существование таблицы user_subscriptions
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
count = result.scalar()
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
except Exception as e:
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
await db.close()
return
# Добавляем поля по одному в отдельных транзакциях
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
# Финальная проверка в отдельной транзакции
if success1 and success2:
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("""
SELECT id, auto_pay_enabled, auto_pay_days_before
FROM user_subscriptions LIMIT 1
"""))
row = result.fetchone()
if row:
logger.info("✅ Все поля доступны для чтения")
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
else:
logger.info("✅ Все поля доступны, но таблица пуста")
except Exception as e:
logger.error(f"❌ Поля все еще недоступны: {e}")
else:
logger.error("❌ Не удалось добавить все необходимые поля")
await db.close()
logger.info("🎉 Экстренное исправление завершено!")
except Exception as e:
logger.error(f"💥 Критическая ошибка: {e}")
raise
if __name__ == "__main__":
asyncio.run(emergency_fix())
+1168 -273
View File
File diff suppressed because it is too large Load Diff
+661 -87
View File
@@ -1,10 +1,10 @@
from database import Subscription
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from typing import List, Optional
from typing import List, Optional, Dict
from translations import t
from datetime import datetime
def language_keyboard() -> InlineKeyboardMarkup:
"""Language selection keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🇷🇺 Русский", callback_data="lang_ru"),
@@ -13,42 +13,39 @@ def language_keyboard() -> InlineKeyboardMarkup:
])
return keyboard
def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: bool = False) -> InlineKeyboardMarkup:
"""Beautiful main menu keyboard with emojis and better layout"""
def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: bool = False, show_lucky_game: bool = True) -> InlineKeyboardMarkup:
buttons = [
# Первый ряд - основные функции
[
InlineKeyboardButton(text="💰 " + t('balance', lang), callback_data="balance"),
InlineKeyboardButton(text="📋 " + t('my_subscriptions', lang), callback_data="my_subscriptions")
],
# Второй ряд - покупка подписки (выделена отдельно как главная функция)
[InlineKeyboardButton(text="🛒 " + t('buy_subscription', lang), callback_data="buy_subscription")],
]
# Добавляем тестовую подписку если доступна
if show_trial:
buttons.insert(1, [InlineKeyboardButton(text="🆓 Тестовая подписка", callback_data="trial_subscription")])
# Добавляем остальные кнопки
if show_lucky_game:
buttons.append([InlineKeyboardButton(text="🎰 Проверь свою удачу!", callback_data="lucky_game")])
buttons.extend([
# Дополнительные функции
[
InlineKeyboardButton(text="🎁 " + t('promocode', lang), callback_data="promocode"),
InlineKeyboardButton(text="👥 Рефералы", callback_data="referral_program")
],
[
InlineKeyboardButton(text="💬 " + t('support', lang), callback_data="support")
],
# Последний ряд - настройки
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")]
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")],
[InlineKeyboardButton(text="📜 Правила сервиса", callback_data="service_rules")]
])
# Добавляем админ панель если пользователь админ
if is_admin:
buttons.append([InlineKeyboardButton(text="⚙️ " + t('admin_panel', lang), callback_data="admin_panel")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
# Остальные функции остаются прежними...
def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful balance menu keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 " + t('topup_balance', lang), callback_data="topup_balance")],
[InlineKeyboardButton(text="📊 " + t('payment_history', lang), callback_data="payment_history")],
@@ -56,27 +53,48 @@ def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
])
return keyboard
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful top up balance keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 " + t('topup_card', lang), callback_data="topup_card")],
[InlineKeyboardButton(text="👨‍💼 " + t('topup_support', lang), callback_data="topup_support")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
def topup_keyboard(lang: str, tribute_enabled: bool = False, stars_enabled: bool = False) -> InlineKeyboardMarkup:
keyboard = []
if tribute_enabled:
keyboard.append([
InlineKeyboardButton(
text="💳 Tribute (Карта)" if lang == 'ru' else "💳 Tribute (Card/SBP)",
callback_data="topup_tribute"
)
])
if stars_enabled:
keyboard.append([
InlineKeyboardButton(
text="⭐ Telegram Stars" if lang == 'ru' else "⭐ Telegram Stars",
callback_data="topup_stars"
)
])
keyboard.extend([
[InlineKeyboardButton(
text="💬 Связаться с поддержкой" if lang == 'ru' else "💬 Contact Support",
callback_data="topup_support"
)],
[InlineKeyboardButton(
text="🔙 Назад" if lang == 'ru' else "🔙 Back",
callback_data="balance"
)]
])
return keyboard
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful available subscriptions keyboard"""
buttons = []
# Группируем подписки по две в ряд для компактности
for i in range(0, len(subscriptions), 2):
row = []
for j in range(2):
if i + j < len(subscriptions):
sub = subscriptions[i + j]
price_text = f"{sub['price']:.0f}"
# Используем разные эмодзи для разных ценовых категорий
if sub['price'] <= 100:
emoji = "🥉"
elif sub['price'] <= 300:
@@ -94,7 +112,6 @@ def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> Inlin
return InlineKeyboardMarkup(inline_keyboard=buttons)
def subscription_detail_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful subscription detail keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="💎 " + t('buy_subscription_btn', lang, price=0),
@@ -105,41 +122,67 @@ def subscription_detail_keyboard(subscription_id: int, lang: str = 'ru') -> Inli
return keyboard
def user_subscriptions_keyboard(user_subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful user's subscriptions keyboard"""
buttons = []
for sub in user_subscriptions:
# Добавляем эмодзи статуса для каждой подписки
buttons.append([InlineKeyboardButton(
text=f"📱 {sub['name']}",
callback_data=f"view_sub_{sub['id']}"
)])
if not user_subscriptions:
# Если нет подписок, показываем кнопку покупки
buttons.append([InlineKeyboardButton(text="🛒 " + t('buy_subscription', lang), callback_data="buy_subscription")])
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False) -> InlineKeyboardMarkup:
"""Beautiful user's subscription detail keyboard with connection and optional extend button"""
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru',
show_extend: bool = False, is_imported: bool = False,
is_trial: bool = False, autopay_enabled: bool = False) -> InlineKeyboardMarkup:
buttons = []
# Add extend button if subscription is expiring soon
if show_extend:
buttons.append([InlineKeyboardButton(text="" + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
if is_imported:
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")])
elif is_trial:
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🛒 Купить полную подписку", callback_data="buy_subscription")])
else:
if show_extend:
buttons.append([InlineKeyboardButton(text="" + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
if autopay_enabled:
autopay_text = "🔄✅ Настроить автоплатеж"
else:
autopay_text = "🔄❌ Настроить автоплатеж"
buttons.append([InlineKeyboardButton(text=autopay_text, callback_data=f"autopay_settings_{subscription_id}")])
# Connection button (главная кнопка)
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
# Back button
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_confirmation_keyboard(subscription_id: int, action: str, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_autopay_{action}_{subscription_id}"),
InlineKeyboardButton(text="❌ Нет", callback_data=f"autopay_settings_{subscription_id}")
]
])
return keyboard
def autopay_help_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура помощи по автоплатежам"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
return keyboard
def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful extend subscription confirmation keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, продлить", callback_data=f"confirm_extend_{subscription_id}"),
@@ -149,46 +192,48 @@ def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> Inli
return keyboard
def back_keyboard(callback_data: str, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful back button keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data=callback_data)]
])
return keyboard
def cancel_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful cancel button keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="" + t('cancel', lang), callback_data="main_menu")]
])
return keyboard
# Admin keyboards
def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin menu keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
# Первый ряд - управление контентом
[
InlineKeyboardButton(text="📦 " + t('manage_subscriptions', lang), callback_data="admin_subscriptions"),
InlineKeyboardButton(text="👥 " + t('manage_users', lang), callback_data="admin_users")
],
# Второй ряд - финансы
[
InlineKeyboardButton(text="💰 " + t('manage_balance', lang), callback_data="admin_balance"),
InlineKeyboardButton(text="🎁 " + t('manage_promocodes', lang), callback_data="admin_promocodes")
],
# Третий ряд - коммуникации и аналитика
[
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals")
],
[
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"),
InlineKeyboardButton(text="🔄 Автоплатежи", callback_data="admin_autopay")
],
[
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system"),
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor")
],
[
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
],
# Назад
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
])
return keyboard
def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin subscriptions management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=" " + t('create_subscription', lang), callback_data="create_subscription")],
[InlineKeyboardButton(text="📋 Список подписок", callback_data="list_admin_subscriptions")],
@@ -197,34 +242,205 @@ def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin users management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_user_subscriptions_filters_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура фильтров для подписок пользователей"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🟢 Активные", callback_data="filter_subs_active"),
InlineKeyboardButton(text="🔴 Истекшие", callback_data="filter_subs_expired")
],
[
InlineKeyboardButton(text="🔄✅ С автоплатежом", callback_data="filter_subs_autopay"),
InlineKeyboardButton(text="⏰ Истекают скоро", callback_data="filter_subs_expiring")
],
[
InlineKeyboardButton(text="🆓 Триальные", callback_data="filter_subs_trial"),
InlineKeyboardButton(text="📦 Импортированные", callback_data="filter_subs_imported")
],
[
InlineKeyboardButton(text="📋 Все подписки", callback_data="admin_user_subscriptions_all"),
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")
]
])
return keyboard
def admin_user_subscription_detail_keyboard(subscription_id: int, user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура для детального просмотра подписки пользователя"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_user_sub_{subscription_id}"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_sub_{subscription_id}")
],
[
InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_detail_{user_id}"),
InlineKeyboardButton(text="📋 К списку подписок", callback_data="admin_user_subscriptions_all")
],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")]
])
return keyboard
def user_subscriptions_pagination_keyboard(current_page: int, total_pages: int,
filter_type: str = "all", lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if total_pages > 1:
nav_row = []
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"user_subs_page_{current_page - 1}_{filter_type}"))
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"user_subs_page_{current_page + 1}_{filter_type}"))
buttons.append(nav_row)
buttons.append([
InlineKeyboardButton(text="🔍 Фильтры", callback_data="admin_user_subscriptions_filters"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_subs_{filter_type}")
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin balance management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
[InlineKeyboardButton(text="📊 История платежей", callback_data="admin_payment_history")],
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="admin_stars_payments")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_promocodes_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin promocodes management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=" " + t('create_promocode', lang), callback_data="create_promocode")],
[InlineKeyboardButton(text="📋 Список промокодов", callback_data="list_promocodes")],
[InlineKeyboardButton(text="📋 Управление промокодами", callback_data="list_promocodes")],
[InlineKeyboardButton(text="📊 Статистика промокодов", callback_data="promocodes_stats")],
[InlineKeyboardButton(text="🧹 Очистить истекшие", callback_data="cleanup_expired_promos")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def promocodes_management_keyboard(promocodes: List, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
for promo in promocodes[:15]:
status_emoji = "🟢" if promo.is_active else "🔴"
if promo.expires_at and promo.expires_at < datetime.utcnow():
status_emoji = ""
usage_text = f"{promo.used_count}/{promo.usage_limit}"
button_text = f"{status_emoji} {promo.code} ({promo.discount_amount}₽) [{usage_text}]"
buttons.append([
InlineKeyboardButton(
text=button_text,
callback_data=f"promo_info_{promo.id}"
)
])
control_buttons = []
if not promo.code.startswith('REF'):
toggle_text = "🔴" if promo.is_active else "🟢"
control_buttons.append(
InlineKeyboardButton(text=toggle_text, callback_data=f"toggle_promo_{promo.id}")
)
control_buttons.append(
InlineKeyboardButton(text="✏️", callback_data=f"edit_promo_{promo.id}")
)
control_buttons.append(
InlineKeyboardButton(text="🗑", callback_data=f"delete_promo_{promo.id}")
)
else:
control_buttons.append(
InlineKeyboardButton(text="👥 Реферальный", callback_data="noop")
)
if control_buttons:
buttons.append(control_buttons)
buttons.append([
InlineKeyboardButton(text=" Создать промокод", callback_data="create_promocode")
])
buttons.append([
InlineKeyboardButton(text="🧹 Очистить истекшие", callback_data="cleanup_expired_promos"),
InlineKeyboardButton(text="📊 Статистика", callback_data="promocodes_stats")
])
buttons.append([
InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_promocodes")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def promocode_edit_keyboard(promo_id: int, language: str = 'ru') -> InlineKeyboardMarkup:
buttons = [
[InlineKeyboardButton(text="💰 Изменить скидку", callback_data=f"edit_promo_field_{promo_id}_discount")],
[InlineKeyboardButton(text="📊 Изменить лимит", callback_data=f"edit_promo_field_{promo_id}_limit")],
[InlineKeyboardButton(text="⏰ Изменить срок", callback_data=f"edit_promo_field_{promo_id}_expiry")],
[InlineKeyboardButton(text="🗑 Удалить промокод", callback_data=f"delete_promo_{promo_id}")],
[InlineKeyboardButton(text=t('back', language), callback_data="list_promocodes")]
]
return InlineKeyboardMarkup(inline_keyboard=buttons)
def promocode_info_keyboard(promo_id: int, is_referral: bool = False, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if not is_referral:
buttons.extend([
[InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_promo_{promo_id}")],
[
InlineKeyboardButton(text="🟢/🔴 Переключить", callback_data=f"toggle_promo_{promo_id}"),
InlineKeyboardButton(text="🗑 Удалить", callback_data=f"delete_promo_{promo_id}")
]
])
else:
buttons.append([
InlineKeyboardButton(text="👥 Реферальный код", callback_data="noop")
])
buttons.append([
InlineKeyboardButton(text="🔙 К списку", callback_data="list_promocodes")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def bulk_promocodes_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = [
[InlineKeyboardButton(text="🧹 Удалить все истекшие", callback_data="confirm_cleanup_expired")],
[InlineKeyboardButton(text="🔴 Деактивировать все", callback_data="confirm_deactivate_all")],
[InlineKeyboardButton(text="📊 Экспорт статистики", callback_data="export_promo_stats")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="list_promocodes")]
]
return InlineKeyboardMarkup(inline_keyboard=buttons)
def confirmation_keyboard(confirm_callback: str, cancel_callback: str, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful confirmation keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data=confirm_callback),
@@ -234,7 +450,6 @@ def confirmation_keyboard(confirm_callback: str, cancel_callback: str, lang: str
return keyboard
def pagination_keyboard(page: int, total_pages: int, prefix: str, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful pagination keyboard"""
buttons = []
if total_pages > 1:
@@ -253,13 +468,11 @@ def pagination_keyboard(page: int, total_pages: int, prefix: str, lang: str = 'r
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful keyboard for admin subscriptions list with enhanced controls"""
buttons = []
for sub in subs:
status_emoji = "🟢" if sub.is_active else "🔴"
price = f"{sub.price:.0f}"
# Основная кнопка с информацией о подписке
buttons.append([
InlineKeyboardButton(
text=f"{status_emoji} {sub.name}{price}",
@@ -267,7 +480,6 @@ def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru'
)
])
# Кнопки управления для каждой подписки в одну строку
control_buttons = [
InlineKeyboardButton(text="✏️", callback_data=f"edit_sub_{sub.id}"),
InlineKeyboardButton(
@@ -279,13 +491,11 @@ def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru'
buttons.append(control_buttons)
# Кнопка создания новой подписки
buttons.append([InlineKeyboardButton(text=" Создать подписку", callback_data="create_subscription")])
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_subscriptions")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_payment_keyboard(payment_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful keyboard for admin payment approval"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Одобрить платеж", callback_data=f"approve_payment_{payment_id}"),
@@ -295,7 +505,6 @@ def admin_payment_keyboard(payment_id: int, lang: str = 'ru') -> InlineKeyboardM
return keyboard
def admin_messages_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin messages management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👤 " + t('send_to_user', lang), callback_data="admin_send_to_user")],
[InlineKeyboardButton(text="📢 " + t('send_to_all', lang), callback_data="admin_send_to_all")],
@@ -304,7 +513,6 @@ def admin_messages_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def quick_topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Quick topup amounts keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="💳 100₽", callback_data="quick_topup_100"),
@@ -320,7 +528,6 @@ def quick_topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def connection_keyboard(subscription_url: str, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful connection keyboard with web app"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔗 Подключиться", url=subscription_url)],
[InlineKeyboardButton(text="📱 Инструкция", callback_data="connection_guide")],
@@ -329,7 +536,6 @@ def connection_keyboard(subscription_url: str, lang: str = 'ru') -> InlineKeyboa
return keyboard
def trial_subscription_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Trial subscription confirmation keyboard"""
buttons = [
[InlineKeyboardButton(text="✅ Получить тестовую подписку", callback_data="confirm_trial")],
[InlineKeyboardButton(text=t('back', lang), callback_data="main_menu")]
@@ -337,37 +543,405 @@ def trial_subscription_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_monitor_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin monitor management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статус сервиса", callback_data="monitor_status")],
[InlineKeyboardButton(text="🔄 Принудительная проверка", callback_data="monitor_force_check")],
[InlineKeyboardButton(text="⚰️ Деактивировать истекшие", callback_data="monitor_deactivate_expired")],
[InlineKeyboardButton(text="👤 Тест для пользователя", callback_data="monitor_test_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin menu keyboard"""
def admin_system_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
# Первый ряд - управление контентом
[
InlineKeyboardButton(text="📦 " + t('manage_subscriptions', lang), callback_data="admin_subscriptions"),
InlineKeyboardButton(text="👥 " + t('manage_users', lang), callback_data="admin_users")
],
# Второй ряд - финансы
[
InlineKeyboardButton(text="💰 " + t('manage_balance', lang), callback_data="admin_balance"),
InlineKeyboardButton(text="🎁 " + t('manage_promocodes', lang), callback_data="admin_promocodes")
],
# Третий ряд - коммуникации и аналитика
[
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
],
# Четвертый ряд - мониторинг (НОВОЕ!)
[InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor")],
# Назад
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
[InlineKeyboardButton(text="📊 Системная статистика", callback_data="system_stats")],
[InlineKeyboardButton(text="🖥 Управление нодами", callback_data="nodes_management")],
[InlineKeyboardButton(text="👥 Пользователи системы", callback_data="system_users")],
[InlineKeyboardButton(text="🔄 Синхронизация с RemnaWave", callback_data="sync_remnawave")],
[InlineKeyboardButton(text="🔍 Отладка API", callback_data="debug_api_comprehensive")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def system_stats_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить статистику", callback_data="refresh_system_stats")],
[InlineKeyboardButton(text="🖥 Ноды", callback_data="nodes_management")],
[InlineKeyboardButton(text="👥 Системные пользователи", callback_data="system_users")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")]
])
return keyboard
def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: int = None) -> InlineKeyboardMarkup:
buttons = []
if nodes:
online_count = len([n for n in nodes if n.get('status') == 'online'])
total_count = len(nodes)
buttons.append([
InlineKeyboardButton(
text=f"📊 Ноды: {online_count}/{total_count} онлайн",
callback_data="noop"
)
])
for i, node in enumerate(nodes[:5]):
status = node.get('status', 'unknown')
if status == 'online':
status_emoji = "🟢"
elif status == 'disabled':
status_emoji = ""
elif status == 'disconnected':
status_emoji = "🔴"
elif status == 'xray_stopped':
status_emoji = "🟡"
else:
status_emoji = ""
node_name = node.get('name', f'Node-{i+1}')
node_id = node.get('id', node.get('uuid'))
if len(node_name) > 20:
display_name = node_name[:17] + "..."
else:
display_name = node_name
usage_info = ""
if node.get('cpuUsage'):
usage_info += f" CPU:{node['cpuUsage']:.0f}%"
if node.get('memUsage'):
usage_info += f" MEM:{node['memUsage']:.0f}%"
buttons.append([
InlineKeyboardButton(
text=f"{status_emoji} {display_name}{usage_info}",
callback_data=f"node_details_{node_id}"
),
InlineKeyboardButton(
text="🔄",
callback_data=f"restart_node_{node_id}"
),
InlineKeyboardButton(
text="⚙️",
callback_data=f"node_settings_{node_id}"
)
])
if len(nodes) > 5:
buttons.append([
InlineKeyboardButton(
text=f"... и еще {len(nodes) - 5} нод",
callback_data="show_all_nodes"
)
])
else:
buttons.append([
InlineKeyboardButton(
text="❌ Ноды не найдены",
callback_data="noop"
)
])
buttons.append([
InlineKeyboardButton(text="🔄 Перезагрузить все", callback_data="restart_all_nodes"),
InlineKeyboardButton(text="📊 Статистика", callback_data="nodes_statistics")
])
refresh_callback = f"refresh_nodes_stats_{timestamp}" if timestamp else "refresh_nodes_stats"
buttons.append([
InlineKeyboardButton(text="🔄 Обновить", callback_data=refresh_callback)
])
buttons.append([
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def system_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="users_statistics")],
[InlineKeyboardButton(text="👥 Список всех пользователей", callback_data="list_all_system_users")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user_uuid")],
[InlineKeyboardButton(text="🔍 Отладка API пользователей", callback_data="debug_users_api")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_system")]
])
return keyboard
def bulk_operations_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="bulk_reset_traffic")],
[InlineKeyboardButton(text="❌ Отключить пользователей", callback_data="bulk_disable_users")],
[InlineKeyboardButton(text="✅ Включить пользователей", callback_data="bulk_enable_users")],
[InlineKeyboardButton(text="🗑 Удалить пользователей", callback_data="bulk_delete_users")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="system_users")]
])
return keyboard
def confirm_restart_keyboard(node_id: str = None, lang: str = 'ru') -> InlineKeyboardMarkup:
action = f"confirm_restart_node_{node_id}" if node_id else "confirm_restart_all_nodes"
back_action = f"node_details_{node_id}" if node_id else "nodes_management"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, перезагрузить", callback_data=action),
InlineKeyboardButton(text="❌ Отмена", callback_data=back_action)
]
])
return keyboard
def admin_referrals_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статистика рефералов", callback_data="referral_statistics")],
[InlineKeyboardButton(text="👥 Список рефереров", callback_data="list_referrers")],
[InlineKeyboardButton(text="💰 История выплат", callback_data="referral_payments")],
[InlineKeyboardButton(text="⚙️ Настройки программы", callback_data="referral_settings")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def lucky_game_keyboard(can_play: bool, time_left_str: str = "", lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if can_play:
buttons.append([InlineKeyboardButton(text="🎲 Играть!", callback_data="start_lucky_game")])
else:
buttons.append([InlineKeyboardButton(text=f"⏰ Приходи через {time_left_str}", callback_data="noop")])
buttons.extend([
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def lucky_numbers_keyboard(numbers_count: int) -> InlineKeyboardMarkup:
buttons = []
for i in range(0, numbers_count, 5):
row = []
for j in range(5):
if i + j + 1 <= numbers_count:
number = i + j + 1
row.append(InlineKeyboardButton(
text=str(number),
callback_data=f"choose_number_{number}"
))
buttons.append(row)
buttons.append([InlineKeyboardButton(text="❌ Отмена", callback_data="lucky_game")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def lucky_game_result_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
sorted_rates = sorted(stars_rates.items())
for i in range(0, len(sorted_rates), 2):
row = []
for j in range(2):
if i + j < len(sorted_rates):
stars, rubles = sorted_rates[i + j]
if stars >= 500:
emoji = "🔥" # Выгодное предложение
elif stars >= 250:
emoji = "💎" # Хорошее предложение
else:
emoji = "" # Базовое предложение
button_text = f"{emoji} {stars} ⭐ → {rubles:.0f}"
row.append(InlineKeyboardButton(
text=button_text,
callback_data=f"buy_stars_{stars}"
))
buttons.append(row)
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="topup_balance")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def stars_payment_keyboard(stars_amount: int, rub_amount: float, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_stars")]
])
return keyboard
def service_rules_keyboard(current_page: int, total_pages: int, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if total_pages > 1:
nav_row = []
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"rules_page_{current_page - 1}"))
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="Вперед ➡️", callback_data=f"rules_page_{current_page + 1}"))
buttons.append(nav_row)
buttons.append([InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_rules_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 Список правил", callback_data="admin_rules_list")],
[InlineKeyboardButton(text=" Добавить страницу", callback_data="admin_rules_create")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_rules_list_keyboard(rules, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
for rule in rules:
status_emoji = "🟢" if rule.is_active else "🔴"
buttons.append([
InlineKeyboardButton(
text=f"{status_emoji} {rule.page_order}. {rule.title}",
callback_data=f"admin_rule_view_{rule.id}"
)
])
buttons.extend([
[InlineKeyboardButton(text=" Добавить страницу", callback_data="admin_rules_create")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_rules")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_rule_edit_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✏️ Редактировать заголовок", callback_data=f"admin_rule_edit_title_{rule_id}"),
InlineKeyboardButton(text="📝 Редактировать содержимое", callback_data=f"admin_rule_edit_content_{rule_id}")
],
[
InlineKeyboardButton(text="🔄 Изменить порядок", callback_data=f"admin_rule_edit_order_{rule_id}"),
InlineKeyboardButton(text="🟢/🔴 Вкл/Выкл", callback_data=f"admin_rule_toggle_{rule_id}")
],
[InlineKeyboardButton(text="🗑 Удалить", callback_data=f"admin_rule_delete_{rule_id}")],
[InlineKeyboardButton(text="🔙 К списку", callback_data="admin_rules_list")]
])
return keyboard
def admin_rule_delete_confirm_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"admin_rule_confirm_delete_{rule_id}"),
InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_rule_view_{rule_id}")
]
])
return keyboard
def autopay_settings_keyboard(user_sub_id: int, user_sub, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if user_sub.auto_pay_enabled:
toggle_text = "❌ Отключить автоплатеж"
toggle_callback = f"toggle_autopay_{user_sub_id}"
else:
toggle_text = "✅ Включить автоплатеж"
toggle_callback = f"toggle_autopay_{user_sub_id}"
buttons.append([InlineKeyboardButton(text=toggle_text, callback_data=toggle_callback)])
if user_sub.auto_pay_enabled:
buttons.append([InlineKeyboardButton(text="📅 Настроить дни до продления", callback_data="noop")])
days_row = []
for days in [1, 3, 5, 7]:
emoji = "🔹" if user_sub.auto_pay_days_before == days else ""
days_row.append(InlineKeyboardButton(
text=f"{emoji} {days}д",
callback_data=f"autopay_days_{user_sub_id}_{days}"
))
buttons.append(days_row)
buttons.append([InlineKeyboardButton(text="🔙 К подписке", callback_data=f"view_sub_{user_sub_id}")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_status_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_status")],
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return keyboard
def autopay_subscriptions_keyboard(subscriptions_data: List[Dict], lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
expired = [s for s in subscriptions_data if s['expires_in_days'] <= 0]
due_soon = [s for s in subscriptions_data if 0 < s['expires_in_days'] <= s['auto_pay_days_before']]
critical_subs = expired + due_soon
for sub_data in critical_subs[:8]:
username = sub_data['username'] if sub_data['username'] != 'N/A' else f"ID:{sub_data['user_id']}"
days = sub_data['expires_in_days']
if days <= 0:
status_emoji = ""
status_text = f"Истекла"
elif days <= sub_data['auto_pay_days_before']:
status_emoji = "⚠️"
status_text = f"Через {days}д"
else:
status_emoji = ""
status_text = f"Через {days}д"
button_text = f"{status_emoji} @{username} ({status_text})"
buttons.append([
InlineKeyboardButton(
text=button_text,
callback_data=f"autopay_user_detail_{sub_data['user_id']}"
)
])
if len(subscriptions_data) > 8:
buttons.append([
InlineKeyboardButton(
text=f"... и еще {len(subscriptions_data) - 8}",
callback_data="noop"
)
])
buttons.extend([
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="📊 Статистика", callback_data="autopay_statistics")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_user_detail_keyboard(user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Добавить баланс", callback_data=f"admin_add_balance_to_{user_id}")],
[InlineKeyboardButton(text="📋 Управление подписками", callback_data=f"admin_user_subscriptions_{user_id}")],
[InlineKeyboardButton(text="🔄 Обновить", callback_data=f"autopay_user_detail_{user_id}")],
[InlineKeyboardButton(text="🔙 К списку", callback_data="autopay_subscriptions_list")]
])
return keyboard
def autopay_statistics_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⚠️ Недостаточно средств", callback_data="autopay_insufficient_balance_users")],
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_statistics")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return keyboard
+327
View File
@@ -0,0 +1,327 @@
import asyncio
from datetime import datetime, timedelta, timezone
import logging
import random
from typing import Optional, List
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from database import Database, User
from keyboards import back_keyboard
from translations import t
from utils import log_user_action, format_datetime
logger = logging.getLogger(__name__)
class LuckyGameStates(StatesGroup):
waiting_number_choice = State()
lucky_game_router = Router()
@lucky_game_router.callback_query(F.data == "lucky_game")
async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
games_played, total_won, win_count = await get_user_game_stats(db, user.telegram_id)
text = "🎰 **Проверь свою удачу!**\n\n"
text += "🎯 **Как играть:**\n"
text += f"• Выберите число от 1 до {numbers_count}\n"
text += f"• Из {numbers_count} чисел {winning_numbers} - выигрышные\n"
text += f"• Угадали - получаете {reward_amount:.0f}₽ на баланс!\n"
text += "• Играть можно 1 раз в сутки\n\n"
text += "📊 **Ваша статистика:**\n"
text += f"• Игр сыграно: {games_played}\n"
text += f"• Выигрышей: {win_count}\n"
text += f"• Всего выиграно: {total_won:.0f}\n"
if games_played > 0:
win_rate = (win_count / games_played) * 100
text += f"• Процент побед: {win_rate:.1f}%\n"
buttons = []
if can_play:
buttons.append([InlineKeyboardButton(text="🎲 Играть!", callback_data="start_lucky_game")])
else:
now = datetime.utcnow()
time_left = next_game_time - now
hours_left = int(time_left.total_seconds() // 3600)
minutes_left = int((time_left.total_seconds() % 3600) // 60)
if hours_left > 0:
time_text = f"{hours_left}ч {minutes_left}м"
else:
time_text = f"{minutes_left}м"
buttons.append([InlineKeyboardButton(text=f"⏰ Приходи через {time_text}", callback_data="noop")])
buttons.extend([
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🔙 " + t('back', user.language), callback_data="main_menu")]
])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error in lucky game menu: {e}")
await callback.answer("❌ Ошибка загрузки игры")
@lucky_game_router.callback_query(F.data == "start_lucky_game")
async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state: FSMContext, **kwargs):
"""Начать игру - показать числа для выбора"""
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
if not can_play:
await callback.answer("⏰ Вы уже играли сегодня! Приходите завтра.", show_alert=True)
return
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
winning_nums = random.sample(range(1, numbers_count + 1), winning_numbers)
await state.update_data(
winning_numbers=winning_nums,
reward_amount=reward_amount,
numbers_count=numbers_count
)
await state.set_state(LuckyGameStates.waiting_number_choice)
text = f"🎯 **Выберите число от 1 до {numbers_count}**\n\n"
text += f"🎁 Награда: {reward_amount:.0f}\n"
text += f"🍀 Удачных чисел: {winning_numbers} из {numbers_count}\n\n"
text += "Нажмите на число, чтобы испытать удачу!"
buttons = []
for i in range(0, numbers_count, 5):
row = []
for j in range(5):
if i + j + 1 <= numbers_count:
number = i + j + 1
row.append(InlineKeyboardButton(
text=str(number),
callback_data=f"choose_number_{number}"
))
buttons.append(row)
buttons.append([InlineKeyboardButton(text="❌ Отмена", callback_data="lucky_game")])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error starting lucky game: {e}")
await callback.answer("❌ Ошибка запуска игры")
@lucky_game_router.callback_query(F.data.startswith("choose_number_"), StateFilter(LuckyGameStates.waiting_number_choice))
async def choose_number_callback(callback: CallbackQuery, db: Database, state: FSMContext, **kwargs):
"""Обработка выбора числа"""
user = kwargs.get('user')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
chosen_number = int(callback.data.split("_")[2])
state_data = await state.get_data()
winning_numbers = state_data['winning_numbers']
reward_amount = state_data['reward_amount']
numbers_count = state_data['numbers_count']
is_winner = chosen_number in winning_numbers
await save_game_result(db, user.telegram_id, chosen_number, winning_numbers, is_winner, reward_amount if is_winner else 0.0)
if is_winner:
await db.add_balance(user.telegram_id, reward_amount)
await db.create_payment(
user_id=user.telegram_id,
amount=reward_amount,
payment_type='lucky_game',
description=f'Выигрыш в игре удачи (число {chosen_number})',
status='completed'
)
text = "🎉 **ПОЗДРАВЛЯЕМ! ВЫ ВЫИГРАЛИ!** 🎉\n\n"
text += f"🎯 Ваше число: **{chosen_number}**\n"
text += f"💰 Награда: **{reward_amount:.0f}₽** зачислена на баланс!\n"
text += f"🆕 Новый баланс: **{user.balance + reward_amount:.0f}₽**\n\n"
else:
text = "😔 **Не повезло в этот раз...**\n\n"
text += f"🎯 Ваше число: **{chosen_number}**\n"
text += f"🍀 Выигрышные числа: **{', '.join(map(str, sorted(winning_numbers)))}**\n\n"
text += "🔄 Попробуйте завтра!"
winning_nums_str = ', '.join(map(str, sorted(winning_numbers)))
text += f"\n🎲 Сегодняшние удачные числа: {winning_nums_str}"
text += f"\n\n⏰ Следующая игра будет доступна завтра!"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
log_user_action(user.telegram_id, "lucky_game_played",
f"Number: {chosen_number}, Winner: {is_winner}, Reward: {reward_amount if is_winner else 0}")
except Exception as e:
logger.error(f"Error in choose number: {e}")
await callback.answer("❌ Ошибка обработки выбора")
await state.clear()
@lucky_game_router.callback_query(F.data == "lucky_game_history")
async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
games = await get_user_game_history(db, user.telegram_id, limit=10)
if not games:
text = "📈 **История ваших игр**\n\n"
text += "🎯 Вы еще не играли в игру удачи.\n\n"
text += "Начните играть, чтобы увидеть историю!"
else:
text = "📈 **История ваших игр** (последние 10)\n\n"
for i, game in enumerate(games, 1):
date_str = format_datetime(game['played_at'], user.language)
if game['is_winner']:
emoji = "🎉"
result = f"Выиграли {game['reward_amount']:.0f}"
else:
emoji = "😔"
result = "Не повезло"
text += f"{i}. {emoji} Число: **{game['chosen_number']}** - {result}\n"
text += f" 📅 {date_str}\n\n"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🎰 К игре", callback_data="lucky_game")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error showing game history: {e}")
await callback.answer("❌ Ошибка загрузки истории")
@lucky_game_router.callback_query(F.data == "noop")
async def noop_callback(callback: CallbackQuery, **kwargs):
"""Пустая операция для неактивных кнопок"""
await callback.answer()
async def check_can_play_today(db: Database, user_id: int) -> tuple[bool, Optional[datetime]]:
"""Проверяет, может ли пользователь играть сегодня"""
try:
can_play = await db.can_play_lucky_game_today(user_id)
if not can_play:
now = datetime.utcnow()
tomorrow = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
return False, tomorrow
return True, None
except Exception as e:
logger.error(f"Error checking can play today: {e}")
return True, None
async def get_user_game_stats(db: Database, user_id: int) -> tuple[int, float, int]:
"""Получает статистику игр пользователя"""
try:
stats = await db.get_user_game_stats(user_id)
return stats['total_games'], stats['total_won'], stats['total_wins']
except Exception as e:
logger.error(f"Error getting user game stats: {e}")
return 0, 0.0, 0
async def save_game_result(db: Database, user_id: int, chosen_number: int,
winning_numbers: List[int], is_winner: bool, reward_amount: float):
"""Сохраняет результат игры в базу данных"""
try:
game = await db.create_lucky_game(
user_id=user_id,
chosen_number=chosen_number,
winning_numbers=winning_numbers,
is_winner=is_winner,
reward_amount=reward_amount
)
if game:
logger.info(f"Game result saved for user {user_id}: number={chosen_number}, winner={is_winner}, reward={reward_amount}")
else:
logger.error(f"Failed to save game result for user {user_id}")
except Exception as e:
logger.error(f"Error saving game result: {e}")
async def get_user_game_history(db: Database, user_id: int, limit: int = 10) -> List[dict]:
"""Получает историю игр пользователя"""
try:
return await db.get_user_game_history(user_id, limit)
except Exception as e:
logger.error(f"Error getting user game history: {e}")
return []
+178 -43
View File
@@ -6,9 +6,21 @@ from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from lucky_game import lucky_game_router
from stars_handlers import stars_router
from autopay_service import AutoPayService
# Import our modules
from config import load_config
print("🚀 Запуск бота...")
print(f"📍 Рабочая директория: {os.getcwd()}")
print(f"📁 Файлы в директории: {os.listdir('.')}")
if os.path.exists('.env'):
print("✅ Файл .env найден")
else:
print("❌ Файл .env НЕ НАЙДЕН!")
print("💡 Создайте файл .env в корне проекта")
from config import load_config, debug_environment
from database import Database
from remnawave_api import RemnaWaveAPI
from subscription_monitor import create_subscription_monitor
@@ -16,7 +28,6 @@ from middlewares import DatabaseMiddleware, UserMiddleware, LoggingMiddleware, T
from handlers import router
from admin_handlers import admin_router
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -28,7 +39,6 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
class BotApplication:
"""Main bot application class"""
def __init__(self):
self.config = None
@@ -37,13 +47,52 @@ class BotApplication:
self.bot = None
self.dp = None
self.monitor_service = None
self.autopay_service = None
self.webhook_server = None
async def _init_autopay_service(self):
try:
logger.info("🔧 Initializing autopay service...")
if not self.bot:
logger.error("❌ Bot instance is None, cannot initialize autopay")
return
if not self.db:
logger.error("❌ Database instance is None, cannot initialize autopay")
return
self.autopay_service = AutoPayService(self.db, self.api, self.bot)
self.dp.workflow_data["autopay_service"] = self.autopay_service
logger.info("✅ Autopay service added to workflow_data")
logger.info("🚀 Starting autopay service...")
await self.autopay_service.start()
status = await self.autopay_service.get_service_status()
if status['is_running']:
logger.info("✅ Autopay service started successfully")
logger.info(f"📊 Autopay status: interval=30min")
else:
logger.warning("⚠️ Autopay service created but not running")
except Exception as e:
logger.error(f"❌ Failed to initialize autopay service: {e}", exc_info=True)
logger.warning("⚠️ Continuing without autopay service")
self.autopay_service = None
async def initialize(self):
"""Initialize all components"""
# Load configuration
debug_environment()
self.config = load_config()
# Validate required environment variables
print(f"🔧 Загруженная конфигурация:")
print(f" BOT_USERNAME: '{self.config.BOT_USERNAME}'")
print(f" REFERRAL_FIRST_REWARD: {self.config.REFERRAL_FIRST_REWARD}")
print(f" ADMIN_IDS: {self.config.ADMIN_IDS}")
if not self.config.BOT_TOKEN:
logger.error("BOT_TOKEN is required")
raise ValueError("BOT_TOKEN is required")
@@ -52,15 +101,19 @@ class BotApplication:
logger.error("REMNAWAVE_URL and REMNAWAVE_TOKEN are required")
raise ValueError("REMNAWAVE_URL and REMNAWAVE_TOKEN are required")
if not self.config.BOT_USERNAME:
logger.warning("⚠️ BOT_USERNAME не установлен! Реферальные ссылки работать не будут!")
print("💡 Добавьте BOT_USERNAME=your_bot_username в .env файл")
logger.info("Starting RemnaWave Bot...")
logger.info(f"RemnaWave URL: {self.config.REMNAWAVE_URL}")
logger.info(f"Admin IDs: {self.config.ADMIN_IDS}")
logger.info(f"Bot Username: {self.config.BOT_USERNAME}")
# Initialize database
self.db = Database(self.config.DATABASE_URL)
await self._init_database()
# Initialize RemnaWave API
self.api = RemnaWaveAPI(
self.config.REMNAWAVE_URL,
self.config.REMNAWAVE_TOKEN,
@@ -68,41 +121,83 @@ class BotApplication:
)
logger.info("RemnaWave API initialized")
# Test API connection (optional - don't fail if it doesn't work)
await self._test_api_connection()
# Initialize bot and dispatcher
self.bot = Bot(
token=self.config.BOT_TOKEN,
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
)
# Test bot token
await self._test_bot_token()
# Initialize dispatcher
self._setup_dispatcher()
await self._init_webhook_server()
if self.config.TRIBUTE_ENABLED:
logger.info("✅ Tribute платежи включены")
if not self.config.TRIBUTE_API_KEY:
logger.warning("⚠️ TRIBUTE_API_KEY не установлен!")
if not self.config.TRIBUTE_DONATE_URL:
logger.warning("⚠️ TRIBUTE_DONATE_URL не установлен!")
else:
logger.info("❌ Tribute платежи отключены")
# Initialize subscription monitor service
await self._init_monitor_service()
await self._init_autopay_service()
if self.config.STARS_ENABLED:
logger.info("✅ Telegram Stars пополнение включено")
logger.info(f"📊 Настроенные курсы: {self.config.STARS_RATES}")
if not self.config.STARS_RATES or len(self.config.STARS_RATES) == 0:
logger.warning("⚠️ STARS_RATES пустые! Telegram Stars будут недоступны")
self.config.STARS_ENABLED = False
else:
valid_rates = all(
isinstance(stars, int) and isinstance(rubles, (int, float)) and stars > 0 and rubles > 0
for stars, rubles in self.config.STARS_RATES.items()
)
if not valid_rates:
logger.warning("⚠️ Неверные STARS_RATES! Telegram Stars будут недоступны")
self.config.STARS_ENABLED = False
else:
logger.info("❌ Telegram Stars пополнение отключено")
async def _init_webhook_server(self):
"""Инициализация webhook сервера для Tribute"""
try:
logger.info("🔧 Initializing webhook server...")
from webhook_server import WebhookServer
self.webhook_server = WebhookServer(self.bot, self.db, self.config)
logger.info("🚀 Starting webhook server...")
await self.webhook_server.start()
logger.info("✅ Webhook server started successfully")
except Exception as e:
logger.error(f"❌ Failed to initialize webhook server: {e}", exc_info=True)
logger.warning("⚠️ Continuing without webhook server")
self.webhook_server = None
async def _init_database(self):
"""Initialize database with retry logic"""
max_retries = 3
for attempt in range(max_retries):
try:
logger.info(f"🗄️ Database initialization attempt {attempt + 1}/{max_retries}")
await self.db.init_db()
logger.info("Database initialized successfully")
logger.info("Database initialized successfully with all migrations")
break
except Exception as e:
logger.error(f"Database initialization attempt {attempt + 1} failed: {e}")
logger.error(f"Database initialization attempt {attempt + 1} failed: {e}")
if attempt == max_retries - 1:
logger.error("Failed to initialize database after all retries")
logger.error("💥 Failed to initialize database after all retries")
raise
await asyncio.sleep(2) # Wait before retry
await asyncio.sleep(2)
async def _test_api_connection(self):
"""Test API connection"""
try:
system_stats = await self.api.get_system_stats()
if system_stats:
@@ -113,28 +208,31 @@ class BotApplication:
logger.warning(f"RemnaWave API connection error: {e} - continuing anyway")
async def _test_bot_token(self):
"""Test bot token before starting"""
try:
bot_info = await self.bot.get_me()
logger.info(f"Bot started: @{bot_info.username} ({bot_info.first_name})")
if not self.config.BOT_USERNAME and bot_info.username:
self.config.BOT_USERNAME = bot_info.username
logger.info(f"✅ BOT_USERNAME автоматически установлен: {bot_info.username}")
print("💡 Добавьте BOT_USERNAME в .env файл для постоянного сохранения")
except Exception as e:
logger.error(f"Invalid bot token or network error: {e}")
raise
def _setup_dispatcher(self):
"""Setup dispatcher with middlewares and routers"""
storage = MemoryStorage()
self.dp = Dispatcher(storage=storage)
# Store config, api, db, and monitor_service in dispatcher workflow_data for access in handlers
self.dp.workflow_data.update({
"config": self.config,
"api": self.api,
"db": self.db,
"monitor_service": None # Will be updated after monitor service is created
"monitor_service": None,
"autopay_service": None
})
# Setup middlewares in correct order
self.dp.message.middleware(LoggingMiddleware())
self.dp.callback_query.middleware(LoggingMiddleware())
@@ -153,33 +251,62 @@ class BotApplication:
self.dp.message.middleware(UserMiddleware(self.db, self.config))
self.dp.callback_query.middleware(UserMiddleware(self.db, self.config))
# Register routers
self.dp.include_router(router)
self.dp.include_router(admin_router)
self.dp.include_router(lucky_game_router)
self.dp.include_router(stars_router)
async def _init_monitor_service(self):
"""Initialize subscription monitor service"""
try:
logger.info("🔧 Initializing subscription monitor service...")
if not self.bot:
logger.error("❌ Bot instance is None, cannot initialize monitor")
return
if not self.db:
logger.error("❌ Database instance is None, cannot initialize monitor")
return
if not self.config:
logger.error("❌ Config instance is None, cannot initialize monitor")
return
self.monitor_service = await create_subscription_monitor(
self.bot, self.db, self.config, self.api
)
# Update workflow_data with monitor service
if not self.monitor_service:
logger.error("❌ Failed to create monitor service instance")
return
self.dp.workflow_data["monitor_service"] = self.monitor_service
# Start the monitor service
logger.info("✅ Monitor service added to workflow_data")
logger.info("🚀 Starting monitor service...")
await self.monitor_service.start()
logger.info("Subscription monitor service started successfully")
status = await self.monitor_service.get_service_status()
if status['is_running']:
logger.info("✅ Subscription monitor service started successfully")
logger.info(f"📊 Monitor status: interval={status['check_interval']}s, daily_hour={status['daily_check_hour']}, warning_days={status['warning_days']}")
else:
logger.warning("⚠️ Monitor service created but not running")
logger.warning(f"📊 Monitor status: {status}")
except Exception as e:
logger.error(f"Failed to initialize monitor service: {e}")
# Don't fail the entire application if monitor service fails
logger.warning("Continuing without monitor service")
logger.error(f"Failed to initialize monitor service: {e}", exc_info=True)
logger.warning("⚠️ Continuing without monitor service")
self.monitor_service = None
async def start(self):
"""Start bot polling"""
logger.info("Bot polling started successfully")
if self.config.BOT_USERNAME:
logger.info(f"🎁 Реферальная система активна! Ссылки: https://t.me/{self.config.BOT_USERNAME}?start=ref_USERID")
else:
logger.warning("⚠️ Реферальная система неактивна! Установите BOT_USERNAME")
try:
await self.dp.start_polling(self.bot)
except Exception as e:
@@ -189,10 +316,22 @@ class BotApplication:
await self.shutdown()
async def shutdown(self):
"""Shutdown all services"""
logger.info("Shutting down bot...")
if self.webhook_server:
try:
await self.webhook_server.stop()
logger.info("Webhook server stopped")
except Exception as e:
logger.error(f"Error stopping webhook server: {e}")
if self.autopay_service:
try:
await self.autopay_service.stop()
logger.info("Autopay service stopped")
except Exception as e:
logger.error(f"Error stopping autopay service: {e}")
# Stop monitor service first
if self.monitor_service:
try:
await self.monitor_service.stop()
@@ -200,7 +339,6 @@ class BotApplication:
except Exception as e:
logger.error(f"Error stopping monitor service: {e}")
# Close API connection
if self.api:
try:
await self.api.close()
@@ -208,7 +346,6 @@ class BotApplication:
except Exception as e:
logger.error(f"Error closing API: {e}")
# Close database connection
if self.db:
try:
await self.db.close()
@@ -216,7 +353,6 @@ class BotApplication:
except Exception as e:
logger.error(f"Error closing database: {e}")
# Close bot session
if self.bot:
try:
await self.bot.session.close()
@@ -227,7 +363,6 @@ class BotApplication:
logger.info("Bot shutdown complete")
async def main():
"""Main function"""
app = None
try:
app = BotApplication()
+4 -19
View File
@@ -8,7 +8,6 @@ from config import Config
logger = logging.getLogger(__name__)
class DatabaseMiddleware(BaseMiddleware):
"""Middleware for database operations"""
def __init__(self, db: Database):
self.db = db
@@ -23,7 +22,6 @@ class DatabaseMiddleware(BaseMiddleware):
return await handler(event, data)
class UserMiddleware(BaseMiddleware):
"""Middleware for user management"""
def __init__(self, db: Database, config: Config):
self.db = db
@@ -39,25 +37,21 @@ class UserMiddleware(BaseMiddleware):
if telegram_user and not telegram_user.is_bot:
try:
# Get or create user
user = await self.db.get_user_by_telegram_id(telegram_user.id)
if not user:
# Check if user is admin
is_admin = telegram_user.id in self.config.ADMIN_IDS
# Create new user
user = await self.db.create_user(
telegram_id=telegram_user.id,
username=telegram_user.username,
first_name=telegram_user.first_name,
last_name=telegram_user.last_name,
language=self.config.DEFAULT_LANGUAGE,
language='',
is_admin=is_admin
)
logger.info(f"Created new user: {telegram_user.id}")
logger.info(f"Created new user: {telegram_user.id} without language")
else:
# Update user info if changed
updated = False
if user.username != telegram_user.username:
user.username = telegram_user.username
@@ -69,7 +63,6 @@ class UserMiddleware(BaseMiddleware):
user.last_name = telegram_user.last_name
updated = True
# Check admin status
should_be_admin = telegram_user.id in self.config.ADMIN_IDS
if user.is_admin != should_be_admin:
user.is_admin = should_be_admin
@@ -79,11 +72,10 @@ class UserMiddleware(BaseMiddleware):
await self.db.update_user(user)
data['user'] = user
data['lang'] = user.language if user else self.config.DEFAULT_LANGUAGE
data['lang'] = user.language if user.language and user.language != '' else self.config.DEFAULT_LANGUAGE
except Exception as e:
logger.error(f"Error in UserMiddleware: {e}")
# Create a minimal user object with defaults if database fails
class FallbackUser:
def __init__(self, telegram_id: int, username: str = None, config: Config = None):
self.telegram_id = telegram_id
@@ -95,7 +87,6 @@ class UserMiddleware(BaseMiddleware):
self.is_admin = telegram_id in (config.ADMIN_IDS if config else [])
self.remnawave_uuid = None
# Try to use fallback, but if database is completely broken, set None
try:
fallback_user = FallbackUser(telegram_user.id, telegram_user.username, self.config)
data['user'] = fallback_user
@@ -110,7 +101,6 @@ class UserMiddleware(BaseMiddleware):
return await handler(event, data)
class LoggingMiddleware(BaseMiddleware):
"""Middleware for logging"""
async def __call__(
self,
@@ -134,7 +124,6 @@ class LoggingMiddleware(BaseMiddleware):
return await handler(event, data)
class ThrottlingMiddleware(BaseMiddleware):
"""Simple throttling middleware"""
def __init__(self, rate_limit: float = 1.0):
self.rate_limit = rate_limit
@@ -163,7 +152,6 @@ class ThrottlingMiddleware(BaseMiddleware):
return await handler(event, data)
class WorkflowDataMiddleware(BaseMiddleware):
"""Middleware to pass workflow data to handlers"""
async def __call__(
self,
@@ -171,20 +159,17 @@ class WorkflowDataMiddleware(BaseMiddleware):
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
# Get dispatcher from event
if hasattr(event, 'bot'):
dp = getattr(event.bot, '_dispatcher', None)
if dp and hasattr(dp, 'workflow_data'):
# Add workflow data to handler data
workflow_data = dp.workflow_data
for key, value in workflow_data.items():
if key not in data: # Don't override existing data
if key not in data:
data[key] = value
return await handler(event, data)
class BotMiddleware(BaseMiddleware):
"""Middleware to add bot instance to handler data"""
def __init__(self, bot):
self.bot = bot
+279
View File
@@ -0,0 +1,279 @@
import logging
from datetime import datetime
from database import Database
from database import ReferralProgram, ReferralEarning
logger = logging.getLogger(__name__)
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None, payment_type: str = None):
try:
import os
if payment_type not in ['topup', 'admin_topup']:
logger.debug(f"Skipping referral rewards for payment type: {payment_type}")
return
if amount <= 0:
logger.debug(f"Skipping referral rewards for non-positive amount: {amount}")
return
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
referral = await db.get_referral_by_referred_id(user_id)
if not referral:
logger.debug(f"No referral found for user {user_id}")
return
user = await db.get_user_by_telegram_id(user_id)
if not user:
logger.error(f"User {user_id} not found")
return
logger.info(f"Processing referral rewards for topup: user {user_id}, amount {amount}, referrer {referral.referrer_id}")
if not referral.first_reward_paid and user.balance >= threshold:
logger.info(f"Processing first reward for referral {referral.id} (threshold: {threshold}, reward: {first_reward})")
await db.add_balance(referral.referrer_id, first_reward)
await db.create_payment(
user_id=referral.referrer_id,
amount=first_reward,
payment_type='referral',
description=f'Первая награда за реферала ID:{user_id}',
status='completed'
)
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
amount=first_reward,
earning_type='first_reward',
related_payment_id=payment_id
)
if success:
logger.info(f"First reward paid: {first_reward}₽ to referrer {referral.referrer_id}")
if bot:
try:
await bot.send_message(
referral.referrer_id,
f"🎉 Поздравляем! Ваш реферал пополнил баланс на {threshold}₽+\n\n"
f"💰 Вам начислено {first_reward}₽ за приведенного друга!\n"
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его пополнения баланса."
)
await bot.send_message(
user_id,
f"🎁 Бонус активирован! Вам начислено {referred_bonus}₽ за переход по реферальной ссылке!"
)
await db.add_balance(user_id, referred_bonus)
await db.create_payment(
user_id=user_id,
amount=referred_bonus,
payment_type='referral',
description='Бонус за переход по реферальной ссылке',
status='completed'
)
logger.info(f"Referral bonus notifications sent and balance updated")
except Exception as e:
logger.error(f"Failed to send referral notifications: {e}")
else:
logger.error(f"Failed to create first reward earning")
if referral.first_reward_paid:
percentage_reward = amount * percentage
if percentage_reward >= 0.01:
await db.add_balance(referral.referrer_id, percentage_reward)
await db.create_payment(
user_id=referral.referrer_id,
amount=percentage_reward,
payment_type='referral',
description=f'{percentage*100:.0f}% от пополнения реферала ID:{user_id}',
status='completed'
)
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
amount=percentage_reward,
earning_type='percentage',
related_payment_id=payment_id
)
if success:
logger.info(f"Percentage reward paid: {percentage_reward:.2f}₽ ({percentage*100:.0f}%) to referrer {referral.referrer_id}")
if bot and percentage_reward >= 1.0:
try:
await bot.send_message(
referral.referrer_id,
f"💰 Реферальный доход!\n\n"
f"Ваш реферал пополнил баланс на {amount:.2f}\n"
f"Вам начислено: {percentage_reward:.2f}₽ ({percentage*100:.0f}%)"
)
except Exception as e:
logger.error(f"Failed to send percentage notification: {e}")
else:
logger.error(f"Failed to create percentage earning")
else:
logger.info(f"Skipping percentage reward for user {user_id} - first reward not yet paid")
except Exception as e:
logger.error(f"Error processing referral rewards: {e}")
async def create_referral_from_start_param(user_telegram_id: int, start_param: str, db: Database, bot=None):
try:
if not start_param.startswith("ref_"):
return False
referrer_id = int(start_param.replace("ref_", ""))
if referrer_id == user_telegram_id:
logger.warning(f"User {user_telegram_id} tried to refer themselves")
return False
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
if existing_reverse_referral and existing_reverse_referral.referrer_id == user_telegram_id:
logger.warning(f"Mutual referral attempt blocked: {user_telegram_id} is already referrer for {referrer_id}")
return False
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
if existing_referral:
logger.info(f"User {user_telegram_id} already has referrer")
return False
referral_code = await db.generate_unique_referral_code(referrer_id)
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
if referral:
logger.info(f"Created referral: {referrer_id} -> {user_telegram_id} with code {referral_code}")
if bot:
try:
referrer = await db.get_user_by_telegram_id(referrer_id)
if referrer:
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
await bot.send_message(
referrer_id,
f"🎉 Отлично! По вашей ссылке зарегистрировался новый пользователь!\n\n"
f"Вы получите {first_reward:.0f}₽ после того, как он пополнит баланс на {threshold:.0f}₽.\n"
f"И будете получать {percentage*100:.0f}% с каждого его пополнения баланса!"
)
except Exception as e:
logger.error(f"Failed to notify referrer: {e}")
return True
return False
except (ValueError, TypeError) as e:
logger.warning(f"Invalid referral parameter: {start_param}")
return False
except Exception as e:
logger.error(f"Error creating referral from start param: {e}")
return False
async def create_referral_from_promocode(user_telegram_id: int, referral_code: str, db: Database, bot=None):
try:
if not referral_code.startswith("REF"):
return False
logger.info(f"Trying to use referral code {referral_code} for user {user_telegram_id}")
async with db.session_factory() as session:
from sqlalchemy import select
result = await session.execute(
select(ReferralProgram).where(ReferralProgram.referral_code == referral_code)
)
referral_record = result.scalar_one_or_none()
if not referral_record:
logger.warning(f"No referrer found for code {referral_code}")
return False
referrer_id = referral_record.referrer_id
if referrer_id == user_telegram_id:
logger.warning(f"User {user_telegram_id} tried to use own referral code")
return False
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
if existing_reverse_referral and existing_reverse_referral.referrer_id == user_telegram_id:
logger.warning(f"Mutual referral attempt blocked: {user_telegram_id} is already referrer for {referrer_id}")
return False
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
if existing_referral:
logger.info(f"User {user_telegram_id} already has referrer")
return False
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
if referral:
logger.info(f"Created referral from promocode: {referrer_id} -> {user_telegram_id}")
if bot:
try:
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
await bot.send_message(
referrer_id,
f"🎉 По вашему промокоду {referral_code} зарегистрировался новый пользователь!\n\n"
f"Вы получите {first_reward:.0f}₽ после того, как он пополнит баланс на {threshold:.0f}₽."
)
except Exception as e:
logger.error(f"Failed to notify referrer: {e}")
return True
return False
except Exception as e:
logger.error(f"Error creating referral from promocode: {e}")
return False
def generate_referral_link(bot_username: str, user_id: int) -> str:
if not bot_username:
return ""
if bot_username.startswith('@'):
bot_username = bot_username[1:]
return f"https://t.me/{bot_username}?start=ref_{user_id}"
def validate_referral_code(code: str) -> bool:
if not code or not code.startswith("REF"):
return False
if len(code) < 4 or len(code) > 20:
return False
return True
def format_referral_stats(stats: dict, lang: str = 'ru') -> str:
if lang == 'ru':
return (f"👥 Приглашено: {stats['total_referrals']}\n"
f"✅ Активных: {stats['active_referrals']}\n"
f"💰 Заработано: {stats['total_earned']:.2f}")
else:
return (f"👥 Invited: {stats['total_referrals']}\n"
f"✅ Active: {stats['active_referrals']}\n"
f"💰 Earned: ${stats['total_earned']:.2f}")
+1135 -250
View File
File diff suppressed because it is too large Load Diff
-218
View File
@@ -1,218 +0,0 @@
#!/bin/bash
# === RemnaWave Bot Setup & Management Script ===
SERVICE_NAME="remnawave-bot"
BOT_FILE="main.py"
ENV_FILE=".env"
VENV_DIR="venv"
# Select language
echo "🌐 Select language / Выберите язык:"
echo "1) English"
echo "2) Русский"
read -p ">>> " LANG_CHOICE
case $LANG_CHOICE in
2) LANG_CODE="ru" ;;
*) LANG_CODE="en" ;;
esac
# === Internationalized messages ===
msg() {
case $1 in
check_python) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка Python..." || echo "🔍 Checking Python..." ;;
python_missing) [[ $LANG_CODE == "ru" ]] && echo "❌ Python 3 не установлен. Установите Python 3.8+." || echo "❌ Python 3 is not installed. Please install Python 3.8+." ;;
version) echo "📋 Python version: $PYTHON_VERSION" ;;
creating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание виртуального окружения..." || echo "🔧 Creating virtual environment..." ;;
activating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Активация виртуального окружения..." || echo "🔧 Activating virtual environment..." ;;
upgrading_pip) [[ $LANG_CODE == "ru" ]] && echo "📦 Обновление pip..." || echo "📦 Upgrading pip..." ;;
installing_requirements) [[ $LANG_CODE == "ru" ]] && echo "📦 Установка зависимостей..." || echo "📦 Installing requirements..." ;;
env_missing) [[ $LANG_CODE == "ru" ]] && echo "⚠️ Файл .env не найден!" || echo "⚠️ .env file not found!" ;;
env_created) [[ $LANG_CODE == "ru" ]] && echo "📋 Создан .env файл. Пожалуйста, отредактируйте его." || echo "📋 .env created. Please edit it." ;;
validating) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка конфигурации..." || echo "🔍 Validating configuration..." ;;
config_ok) [[ $LANG_CODE == "ru" ]] && echo "✅ Конфигурация прошла проверку." || echo "✅ Configuration validated successfully." ;;
creating_service) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание systemd-сервиса..." || echo "🔧 Creating systemd service..." ;;
service_created) [[ $LANG_CODE == "ru" ]] && echo "✅ Сервис создан и включён." || echo "✅ Service created and enabled." ;;
start_prompt) [[ $LANG_CODE == "ru" ]] && echo "🚀 Запуск бота..." || echo "🚀 Starting the bot..." ;;
already_running) [[ $LANG_CODE == "ru" ]] && echo "🟢 Бот уже запущен!" || echo "🟢 Bot is already running!" ;;
last_logs) [[ $LANG_CODE == "ru" ]] && echo "📄 Последние 20 строк лога:" || echo "📄 Last 20 log lines:" ;;
action_menu)
[[ $LANG_CODE == "ru" ]] && {
echo -e "\nЧто вы хотите сделать?"
echo "1) Остановить бота"
echo "2) Перезапустить бота"
echo "3) Выйти"
} || {
echo -e "\nWhat would you like to do?"
echo "1) Stop the bot"
echo "2) Restart the bot"
echo "3) Exit"
}
;;
restarting) [[ $LANG_CODE == "ru" ]] && echo "🔄 Перезапуск бота..." || echo "🔄 Restarting bot..." ;;
completed)
echo ""
[[ $LANG_CODE == "ru" ]] && {
echo "🎉 Установка завершена!"
echo "📋 Возможности бота:"
echo " ✅ Мультиязычный интерфейс"
echo " ✅ Управление балансом и подписками"
echo " ✅ Промокоды и поддержка"
echo ""
} || {
echo "🎉 Installation complete!"
echo "📋 Bot Features:"
echo " ✅ Multi-language support"
echo " ✅ Subscription and balance management"
echo " ✅ Promocode system and support"
echo ""
}
;;
esac
}
# === Check if bot is running ===
if systemctl is-active --quiet "$SERVICE_NAME"; then
msg already_running
msg last_logs
sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager
msg action_menu
read -p ">>> " choice
case $choice in
1)
[[ $LANG_CODE == "ru" ]] && echo "🛑 Остановка бота..." || echo "🛑 Stopping the bot..."
sudo systemctl stop "$SERVICE_NAME"
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот остановлен." || echo "✅ Bot stopped."
exit 0
;;
2)
msg restarting
sudo systemctl restart "$SERVICE_NAME"
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот перезапущен." || echo "✅ Bot restarted."
exit 0
;;
3)
[[ $LANG_CODE == "ru" ]] && echo "👋 Выход." || echo "👋 Exiting."
exit 0
;;
*)
[[ $LANG_CODE == "ru" ]] && echo "⚠️ Неверный выбор." || echo "⚠️ Invalid choice."
exit 1
;;
esac
fi
# === Install/Setup ===
msg check_python
if ! command -v python3 &> /dev/null; then
msg python_missing
exit 1
fi
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
msg version
if [ ! -d "$VENV_DIR" ]; then
msg creating_venv
python3 -m venv "$VENV_DIR"
fi
msg activating_venv
source "$VENV_DIR/bin/activate"
msg upgrading_pip
pip install --upgrade pip
msg installing_requirements
pip install -r requirements.txt
# Create .env if needed
if [ ! -f "$ENV_FILE" ]; then
msg env_missing
cat > "$ENV_FILE" <<EOF
# Bot Configuration
BOT_TOKEN=
# RemnaWave API Configuration
REMNAWAVE_URL=
REMNAWAVE_MODE=
REMNAWAVE_TOKEN=
# Admin Configuration (comma-separated list of Telegram user IDs)
ADMIN_IDS=
# Support Configuration
SUPPORT_USERNAME=support
# Database Configuration (optional, defaults to SQLite)
# DATABASE_URL=sqlite+aiosqlite:///bot.db
# Trial Configuration
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=
TRIAL_PRICE=0.0
EOF
msg env_created
echo ""
read -p "🔧 Press Enter after configuring .env / Нажмите Enter после настройки .env..."
fi
# Load env
export $(grep -v '^#' "$ENV_FILE" | xargs)
msg validating
if [[ -z "$BOT_TOKEN" || "$BOT_TOKEN" == "your_telegram_bot_token_here" ]]; then
echo "❌ BOT_TOKEN not set properly."
exit 1
fi
if [[ -z "$REMNAWAVE_URL" || "$REMNAWAVE_URL" == "https://your-panel.com" ]]; then
echo "❌ REMNAWAVE_URL not set properly."
exit 1
fi
if [[ -z "$REMNAWAVE_TOKEN" || "$REMNAWAVE_TOKEN" == "your_jwt_token_here" ]]; then
echo "❌ REMNAWAVE_TOKEN not set properly."
exit 1
fi
msg config_ok
# Create systemd service
read -p "🔧 Create systemd service? (y/n): " CREATE_SERVICE
if [[ "$CREATE_SERVICE" == "y" || "$CREATE_SERVICE" == "Y" ]]; then
msg creating_service
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
CURRENT_DIR=$(pwd)
CURRENT_USER=$(whoami)
sudo tee "$SERVICE_FILE" > /dev/null <<EOF
[Unit]
Description=RemnaWave Bot
After=network.target
[Service]
Type=simple
User=$CURRENT_USER
WorkingDirectory=$CURRENT_DIR
Environment=PATH=$CURRENT_DIR/$VENV_DIR/bin
EnvironmentFile=$CURRENT_DIR/.env
ExecStart=$CURRENT_DIR/$VENV_DIR/bin/python3 $BOT_FILE
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME"
msg service_created
fi
msg completed
msg start_prompt
python3 "$BOT_FILE"
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

+1
View File
@@ -0,0 +1 @@
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+1
View File
@@ -0,0 +1 @@
Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

+415
View File
@@ -0,0 +1,415 @@
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, LabeledPrice, PreCheckoutQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.exceptions import TelegramBadRequest
from datetime import datetime
import logging
from typing import Dict
from database import Database, User
from keyboards import stars_topup_keyboard, stars_payment_keyboard, balance_keyboard
from translations import t
from utils import log_user_action
from referral_utils import process_referral_rewards
logger = logging.getLogger(__name__)
stars_router = Router()
@stars_router.callback_query(F.data == "topup_stars")
async def topup_stars_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
if not config or not config.STARS_ENABLED:
await callback.message.edit_text(
"❌ Пополнение через Telegram Stars временно недоступно",
reply_markup=balance_keyboard(user.language)
)
return
if not config.STARS_RATES:
await callback.message.edit_text(
"❌ Курсы звезд не настроены",
reply_markup=balance_keyboard(user.language)
)
return
text = "⭐ **Пополнение через Telegram Stars**\n\n"
text += "🚀 **Преимущества:**\n"
text += "• Мгновенное зачисление\n"
text += "• Безопасные платежи через Telegram\n"
text += "• Без комиссий и скрытых платежей\n\n"
text += "💎 **Доступные варианты:**\n"
sorted_rates = sorted(config.STARS_RATES.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
if stars >= 500:
bonus_text = " 🔥 Выгодно!"
elif stars >= 250:
bonus_text = " 💎 Хорошо!"
else:
bonus_text = ""
text += f"{stars} ⭐ → {rubles:.0f}{bonus_text}\n"
text += f"\n💡 Выберите подходящий вариант:"
await callback.message.edit_text(
text,
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
@stars_router.callback_query(F.data.startswith("buy_stars_"))
async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
if not config or not config.STARS_ENABLED or not config.STARS_RATES:
await callback.answer("❌ Пополнение недоступно")
return
try:
stars_amount = int(callback.data.split("_")[2])
if stars_amount not in config.STARS_RATES:
await callback.answer("❌ Неверное количество звезд")
return
rub_amount = config.STARS_RATES[stars_amount]
star_payment = await db.create_star_payment(
user_id=user.telegram_id,
stars_amount=stars_amount,
rub_amount=rub_amount
)
prices = [LabeledPrice(label=f"Пополнение на {rub_amount:.0f}", amount=stars_amount)]
try:
await callback.answer("💳 Создаю платеж...")
await bot.send_invoice(
chat_id=callback.message.chat.id,
title=f"Пополнение баланса",
description=f"Пополнение баланса на {rub_amount:.0f}₽ за {stars_amount}",
payload=f"star_payment_{star_payment.id}",
currency="XTR",
prices=prices
)
try:
await callback.message.edit_text(
f"💳 **Оплата через Telegram Stars**\n\n"
f"⭐ Количество звезд: {stars_amount}\n"
f"💰 Сумма пополнения: {rub_amount:.0f}\n\n"
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️\n\n"
f"Если передумали - нажмите кнопку ниже \n\n❌ Отменить платеж",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
]),
parse_mode='Markdown'
)
except TelegramBadRequest:
await callback.message.answer(
f"💳 **Оплата через Telegram Stars**\n\n"
f"⭐ Количество звезд: {stars_amount}\n"
f"💰 Сумма пополнения: {rub_amount:.0f}\n\n"
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
]),
parse_mode='Markdown'
)
except TelegramBadRequest as e:
logger.error(f"Failed to send invoice: {e}")
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
try:
await callback.message.edit_text(
"❌ Ошибка создания платежа. Попробуйте позже.",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
)
except TelegramBadRequest:
await callback.message.answer(
"❌ Ошибка создания платежа. Попробуйте позже.",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
)
await db.cancel_star_payment(star_payment.id)
except (ValueError, IndexError) as e:
logger.error(f"Error processing stars amount: {e}")
await callback.answer("❌ Ошибка обработки")
except Exception as e:
logger.error(f"Error in buy_stars_callback: {e}")
await callback.answer("❌ Произошла ошибка")
@stars_router.pre_checkout_query()
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery, db: Database, **kwargs):
try:
if not pre_checkout_query.invoice_payload.startswith("star_payment_"):
await pre_checkout_query.answer(ok=False, error_message="Неверный формат платежа")
return
payment_id = int(pre_checkout_query.invoice_payload.split("_")[2])
star_payment = await db.get_star_payment_by_id(payment_id)
if not star_payment:
await pre_checkout_query.answer(ok=False, error_message="Платеж не найден")
return
if star_payment.status != 'pending':
await pre_checkout_query.answer(ok=False, error_message="Платеж уже обработан")
return
if pre_checkout_query.total_amount != star_payment.stars_amount:
await pre_checkout_query.answer(ok=False, error_message="Неверная сумма платежа")
return
await pre_checkout_query.answer(ok=True)
except Exception as e:
logger.error(f"Error in pre_checkout_query_handler: {e}")
await pre_checkout_query.answer(ok=False, error_message="Внутренняя ошибка")
@stars_router.message(F.successful_payment)
async def successful_payment_handler(message: Message, db: Database, **kwargs):
user = kwargs.get('user')
bot = kwargs.get('bot')
if not user:
logger.error("User not found in successful payment handler")
return
try:
payment_info = message.successful_payment
if not payment_info.invoice_payload.startswith("star_payment_"):
logger.error(f"Invalid payment payload: {payment_info.invoice_payload}")
return
payment_id = int(payment_info.invoice_payload.split("_")[2])
success = await db.complete_star_payment(
payment_id=payment_id,
telegram_payment_charge_id=payment_info.telegram_payment_charge_id
)
if success:
star_payment = await db.get_star_payment_by_id(payment_id)
if star_payment:
updated_user = await db.get_user_by_telegram_id(user.telegram_id)
success_text = "✅ **Платеж успешно обработан!**\n\n"
success_text += f"⭐ Оплачено звезд: {star_payment.stars_amount}\n"
success_text += f"💰 Зачислено: {star_payment.rub_amount:.0f}\n"
success_text += f"💳 Текущий баланс: {updated_user.balance:.0f}\n\n"
success_text += "🎉 Средства уже доступны на вашем балансе!"
await message.answer(
success_text,
parse_mode='Markdown'
)
if bot:
try:
regular_payment = await db.create_payment(
user_id=user.telegram_id,
amount=star_payment.rub_amount,
payment_type='stars',
description=f'Пополнение через Telegram Stars ({star_payment.stars_amount} ⭐)',
status='completed'
)
await process_referral_rewards(
user.telegram_id,
star_payment.rub_amount,
regular_payment.id,
db,
bot,
payment_type='stars'
)
except Exception as ref_error:
logger.error(f"Error processing referral rewards for stars payment: {ref_error}")
log_user_action(user.telegram_id, "stars_payment_completed",
f"Stars: {star_payment.stars_amount}, Amount: {star_payment.rub_amount}")
else:
await message.answer("❌ Ошибка получения данных платежа")
else:
await message.answer("❌ Ошибка обработки платежа. Обратитесь в поддержку.")
logger.error(f"Failed to complete star payment {payment_id}")
except Exception as e:
logger.error(f"Error in successful_payment_handler: {e}")
await message.answer("❌ Произошла ошибка при обработке платежа. Обратитесь в поддержку.")
def get_stars_rate_info(stars_rates: Dict[int, float], lang: str = 'ru') -> str:
if not stars_rates:
return "Курсы не настроены"
text = "⭐ **Курсы Telegram Stars:**\n\n"
sorted_rates = sorted(stars_rates.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
text += f"{stars} ⭐ = {rubles:.0f}₽ (курс: {rate_per_star:.2f}₽ за ⭐)\n"
return text
@stars_router.callback_query(F.data.startswith("cancel_star_payment_"))
async def cancel_star_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
payment_id = int(callback.data.split("_")[-1])
star_payment = await db.get_star_payment_by_id(payment_id)
if not star_payment:
await callback.answer("❌ Платеж не найден")
return
if star_payment.user_id != user.telegram_id:
await callback.answer("❌ Нет доступа к этому платежу")
return
if star_payment.status != 'pending':
await callback.answer("❌ Платеж уже обработан")
return
success = await db.cancel_star_payment(payment_id)
if success:
await callback.answer("✅ Платеж отменен", show_alert=True)
if bot:
invoice_deleted = False
try:
await bot.delete_message(
chat_id=callback.message.chat.id,
message_id=callback.message.message_id
)
invoice_deleted = True
logger.info(f"Deleted invoice message {callback.message.message_id} for cancelled payment {payment_id}")
except TelegramBadRequest as e:
logger.warning(f"Failed to delete invoice message: {e}")
except Exception as e:
logger.error(f"Unexpected error deleting invoice message: {e}")
if invoice_deleted:
try:
if config and config.STARS_RATES:
text = "❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\n"
text += "🚀 **Преимущества:**\n"
text += "• Мгновенное зачисление\n"
text += "• Безопасные платежи через Telegram\n"
text += "• Без комиссий и скрытых платежей\n\n"
text += "💎 **Доступные варианты:**\n"
sorted_rates = sorted(config.STARS_RATES.items())
for stars, rubles in sorted_rates:
if stars >= 500:
bonus_text = " 🔥 Выгодно!"
elif stars >= 250:
bonus_text = " 💎 Хорошо!"
else:
bonus_text = ""
text += f"{stars} ⭐ → {rubles:.0f}{bonus_text}\n"
text += f"\n💡 Выберите другой вариант:"
await bot.send_message(
chat_id=callback.message.chat.id,
text=text,
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен\n\n💰 Управление балансом:",
reply_markup=balance_keyboard(user.language)
)
except Exception as e:
logger.error(f"Failed to send menu after invoice deletion: {e}")
else:
try:
if config and config.STARS_RATES:
await callback.message.edit_text(
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await callback.message.edit_text(
"❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except TelegramBadRequest:
try:
if config and config.STARS_RATES:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except Exception as e:
logger.error(f"Failed to send fallback message: {e}")
else:
try:
if config and config.STARS_RATES:
await callback.message.edit_text(
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await callback.message.edit_text(
"❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except TelegramBadRequest:
logger.warning("Failed to edit message when bot instance not available")
log_user_action(user.telegram_id, "stars_payment_cancelled", f"Payment ID: {payment_id}")
else:
await callback.answer("❌ Ошибка отмены платежа")
except (ValueError, IndexError) as e:
logger.error(f"Error parsing payment ID from callback: {e}")
await callback.answer("❌ Ошибка обработки")
except Exception as e:
logger.error(f"Error in cancel_star_payment_callback: {e}")
await callback.answer("❌ Произошла ошибка")
+828 -410
View File
File diff suppressed because it is too large Load Diff
+210 -63
View File
@@ -1,10 +1,7 @@
TRANSLATIONS = {
'ru': {
# Language selection
'select_language': '🌍 Выберите язык / Choose language',
'language_selected': '🇷🇺 Язык выбран: Русский',
# Main menu
'main_menu': '📱 Главное меню',
'balance': 'Баланс',
'my_subscriptions': 'Мои подписки',
@@ -14,18 +11,32 @@ TRANSLATIONS = {
'admin_panel': 'Админ панель',
'trial_subscription': '🆓 Тестовая подписка',
'trial_not_available': '❌ Тестовая подписка недоступна',
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе "Мои подписки".',
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе: \n\n"Мои подписки"',
'trial_error': '❌ Ошибка при создании тестовой подписки',
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nТариф действует 3 дня!\n\nОграничение трафика - 2гб!',
# Balance menu
'trial_info': 'Попробуйте наш сервис бесплатно! \n\n🧪 Тестовая подписка выдается на {days} дня!\n\nОграничение по трафику - {traffic} гб',
'subscriptions_list': '📋 Список подписок в продаже:',
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
'topup_balance': 'Пополнить баланс',
'payment_history': 'История платежей',
'topup_card': 'Пополнение картой',
'topup_support': 'Через саппорт',
'back': 'Назад',
'system_management': 'Управление системой',
'nodes_management': 'Управление нодами',
'system_users': 'Системные пользователи',
'system_statistics': 'Системная статистика',
'subscription_expired_message': '❌ Ваша подписка \'{name}\' истекла!\n\n🔄 Для возобновления доступа продлите подписку в разделе \'Мои подписки\'.\n\n💡 Подписку можно продлить в любой момент - срок действия будет увеличен с текущего момента.',
'subscription_expires_today': '⚠️ Срочно! Ваша подписка \'{name}\' истекает сегодня!\n\n🔄 Продлите её прямо сейчас в разделе \'Мои подписки\', чтобы не потерять доступ.\n\n⏰ У вас есть несколько часов до истечения!',
'subscription_expires_tomorrow': '⚠️ Внимание! Ваша подписка \'{name}\' истекает завтра!\n\n🔄 Чтобы не потерять доступ, продлите её сейчас в разделе \'Мои подписки\'.\n\n✨ При продлении срок действия будет увеличен с момента истечения.',
'subscription_expires_day_after_tomorrow': '⏰ Ваша подписка \'{name}\' истекает послезавтра ({days} дня)!\n\n🔄 Рекомендуем продлить её заранее в разделе \'Мои подписки\'.\n\n💰 Проверьте баланс - возможно, стоит пополнить его заранее.',
'subscription_expires_in_days': '⏳ Ваша подписка \'{name}\' истекает через {days} дн.!\n\n🔄 Продлить подписку можно в разделе \'Мои подписки\'.\n\n💡 Продление заранее увеличивает срок с момента истечения текущей подписки.',
'restart_nodes': 'Перезагрузить ноды',
'bulk_operations': 'Массовые операции',
'search_user': 'Поиск пользователя',
'user_details': 'Детали пользователя',
'reset_traffic': 'Сбросить трафик',
'disable_user': 'Отключить пользователя',
'enable_user': 'Включить пользователя',
'send_message': 'Отправить сообщение',
'send_to_user': 'Отправить пользователю',
'send_to_all': 'Отправить всем',
@@ -40,8 +51,6 @@ TRANSLATIONS = {
'subscription_extended': '✅ Подписка продлена',
'extend_confirmation': 'Продлить подписку "{name}" на {days} дней за {price:.2f} руб.?',
'subscription_expires_soon': 'Подписка истекает через {days} дней',
# Subscriptions
'no_subscriptions': '❌ У вас нет активных подписок',
'subscription_info': '📋 Подписка: {name}\n💰 Цена: {price:.2f} руб.\n⏱ Длительность: {days} дней\n📊 Трафик: {traffic}\n\n{description}',
'unlimited_traffic': 'Безлимитный',
@@ -51,41 +60,86 @@ TRANSLATIONS = {
'subscription_expires': 'Истекает: {date}',
'subscription_expired': 'Истекла',
'subscription_active': 'Активна до: {date}',
# Payments
'your_subscriptions': '👀 Список ваших подписок:',
'payment_created': '✅ Заявка на пополнение создана.\nОбратитесь к @{support} для оплаты.',
'enter_amount': '💰 Введите сумму для пополнения (руб.):',
'invalid_amount': '❌ Неверная сумма',
'payment_card_info': '💳 Для пополнения картой свяжитесь с @{support}',
'no_payments': '❌ История платежей пуста',
'payment_item': '{date}: {amount:.2f} руб. - {description} ({status})',
# Promocodes
'enter_promocode': '🎁 Введите промокод:',
'promocode_success': '✅ Промокод активирован! Скидка: {discount}',
'promocode_not_found': '❌ Промокод не найден',
'promocode_expired': '❌ Промокод истек',
'promocode_used': '❌ Промокод уже использован',
'promocode_limit': '❌ Лимит использований исчерпан',
# Purchase
'referral_program': 'Реферальная программа',
'referral_conditions': 'Условия программы',
'referral_link': 'Ваша реферальная ссылка',
'referral_code': 'Ваш промокод',
'referral_stats': 'Статистика',
'total_referrals': 'Всего приглашено',
'active_referrals': 'Активных рефералов',
'total_earned': 'Заработано',
'my_referrals': 'Мои рефералы',
'referral_reward_referrer': 'За каждого приглашенного: 150₽',
'referral_reward_referred': 'Бонус новичку: 150₽ после пополнения на 300₽',
'referral_percentage': 'Процент с платежей: 25%',
'no_referrals': 'У вас пока нет рефералов',
'referral_welcome': 'Добро пожаловать по реферальной ссылке!',
'insufficient_balance': '❌ Недостаточно средств на балансе',
'subscription_purchased': '✅ Подписка успешно приобретена!',
'purchase_error': '❌ Ошибка при покупке подписки',
# Support
'support_message': '🎧 Для получения поддержки обратитесь к @{support}',
# Admin panel
'admin_menu': '⚙️ Админ панель',
'manage_subscriptions': 'Управление подписками',
'manage_users': 'Управление пользователями',
'manage_balance': 'Управление балансом',
'manage_promocodes': 'Управление промокодами',
'statistics': 'Статистика',
'admin_user_subscriptions': 'Подписки пользователей',
'all_user_subscriptions': 'Все подписки пользователей',
'user_subscription_filters': 'Фильтры подписок',
'active_subscriptions': 'Активные подписки',
'expired_subscriptions': 'Истекшие подписки',
'expiring_subscriptions': 'Истекающие подписки',
'autopay_subscriptions': 'Подписки с автоплатежом',
'trial_subscriptions': 'Триальные подписки',
'imported_subscriptions': 'Импортированные подписки',
'subscription_details': 'Детали подписки',
'user_info': 'Информация о пользователе',
'subscription_info': 'Информация о подписке',
'subscription_status': 'Статус подписки',
'subscription_dates': 'Временные рамки',
'autopay_settings': 'Настройки автоплатежа',
'subscription_type': 'Тип подписки',
'edit_subscription': 'Редактировать подписку',
'toggle_status': 'Изменить статус',
'edit_expiry': 'Изменить срок',
'edit_autopay': 'Настроить автоплатеж',
'subscription_activated': 'Подписка активирована',
'subscription_deactivated': 'Подписка деактивирована',
'insufficient_funds': 'Недостаточно средств',
'funds_needed': 'Нужно еще',
'created_date': 'Дата создания',
'expires_date': 'Дата истечения',
'updated_date': 'Дата обновления',
'days_left': 'Дней осталось',
'autopay_enabled': 'Автоплатеж включен',
'autopay_disabled': 'Автоплатеж отключен',
'renew_before_days': 'Продлять за дней',
'view_details': 'Просмотр деталей',
'back_to_list': 'К списку',
'refresh_data': 'Обновить данные',
'no_subscriptions_found': 'Подписки не найдены',
'subscription_not_found': 'Подписка не найдена',
'database_unavailable': 'База данных недоступна',
'error_loading_subscriptions': 'Ошибка загрузки подписок',
'error_loading_details': 'Ошибка загрузки деталей',
'pagination_error': 'Ошибка навигации',
'edit_error': 'Ошибка редактирования',
'status_toggle_error': 'Ошибка изменения статуса',
'not_admin': '❌ У вас нет прав администратора',
# Admin - subscriptions
'create_subscription': 'Создать подписку',
'enter_sub_name': 'Введите название подписки:',
'enter_sub_description': 'Введите описание подписки:',
@@ -94,43 +148,61 @@ TRANSLATIONS = {
'enter_sub_traffic': 'Введите лимит трафика (ГБ, 0 для безлимита):',
'enter_squad_uuid': 'Введите UUID squad из панели:',
'subscription_created': '✅ Подписка создана',
# Admin - users
'extend_subscription_btn': '🔄 Продлить подписку',
'my_subscriptions_btn': '📋 Мои подписки',
'buy_new_subscription_btn': '🛒 Купить новую подписку',
'restore_subscription_btn': '🔄 Восстановить подписку',
'user_list': 'Список пользователей:',
'user_item': 'ID: {id}, @{username}, Баланс: {balance:.2f} руб.',
'enter_user_id': 'Введите Telegram ID пользователя:',
'enter_balance_amount': 'Введите сумму для пополнения:',
'balance_added': '✅ Баланс пополнен',
'user_not_found': '❌ Пользователь не найден',
# Admin - promocodes
'create_promocode': 'Создать промокод',
'enter_promo_code': 'Введите код промокода:',
'enter_promo_discount': 'Введите размер скидки (руб.):',
'enter_promo_limit': 'Введите лимит использований:',
'promocode_created': '✅ Промокод создан',
'promocode_exists': '❌ Промокод уже существует',
# Statistics
'stats_info': '📊 Статистика:\n👥 Пользователей: {users}\n📋 Подписок: {subscriptions}\n💰 Выручка: {revenue:.2f} руб.',
# Errors
'error_occurred': '❌ Произошла ошибка',
'try_again': 'Попробуйте еще раз',
'invalid_input': '❌ Неверный ввод',
'cancel': 'Отмена',
# Connection
'connection_link': '🔗 Ссылка для подключения:\n\n`{link}`\n\nНажмите на ссылку чтобы скопировать',
'connection_error': '❌ Ошибка получения ссылки подключения',
'lucky_game': 'Игра удачи',
'lucky_game_title': '🎰 Проверь свою удачу!',
'lucky_game_description': 'Выберите число от 1 до {numbers} и попытайтесь угадать выигрышное!',
'lucky_game_reward': 'Награда за выигрыш: {reward}',
'lucky_game_rules': 'Правила игры',
'lucky_game_play': 'Играть!',
'lucky_game_history': 'История игр',
'lucky_game_stats': 'Ваша статистика',
'lucky_game_choose_number': 'Выберите число от 1 до {numbers}',
'lucky_game_winner': '🎉 ПОЗДРАВЛЯЕМ! ВЫ ВЫИГРАЛИ!',
'lucky_game_loser': '😔 Не повезло в этот раз...',
'lucky_game_your_number': 'Ваше число: {number}',
'lucky_game_winning_numbers': 'Выигрышные числа: {numbers}',
'lucky_game_reward_added': 'Награда {reward}₽ зачислена на баланс!',
'lucky_game_new_balance': 'Новый баланс: {balance}',
'lucky_game_try_tomorrow': 'Попробуйте завтра!',
'lucky_game_already_played': 'Вы уже играли сегодня! Приходите завтра.',
'lucky_game_come_back_in': 'Приходи через {time}',
'lucky_game_no_history': 'Вы еще не играли в игру удачи.',
'lucky_game_start_playing': 'Начните играть, чтобы увидеть историю!',
'lucky_game_games_played': 'Игр сыграно: {count}',
'lucky_game_wins': 'Выигрышей: {count}',
'lucky_game_total_won': 'Всего выиграно: {amount}',
'lucky_game_win_rate': 'Процент побед: {rate}%',
'trial_subscription_expired': '🆓 Ваша триальная подписка "{name}" истекла!\n\n'
'💡 Чтобы продолжить использование VPN, купите полный тариф.\n\n'
'✨ Доступны различные планы подписки с выгодными ценами!'
},
'en': {
# Language selection
'select_language': '🌍 Choose language / Выберите язык',
'language_selected': '🇺🇸 Language selected: English',
# Main menu
'main_menu': '📱 Main Menu',
'balance': 'Balance',
'my_subscriptions': 'My Subscriptions',
@@ -145,17 +217,23 @@ TRANSLATIONS = {
'enter_user_id_message': 'Enter user id message',
'enter_message_text': 'Enter message text',
'trial_subscription': 'Trial subscription',
# Balance menu
'system_management': 'System Management',
'nodes_management': 'Nodes Management',
'system_users': 'System Users',
'system_statistics': 'System Statistics',
'restart_nodes': 'Restart Nodes',
'bulk_operations': 'Bulk Operations',
'search_user': 'Search User',
'user_details': 'User Details',
'reset_traffic': 'Reset Traffic',
'disable_user': 'Disable User',
'enable_user': 'Enable User',
'your_balance': '💰 Your balance: ${balance:.2f}',
'topup_balance': 'Top up balance',
'payment_history': 'Payment history',
'topup_card': 'Card payment',
'topup_support': 'Through support',
'back': 'Back',
# Subscriptions
'no_subscriptions': '❌ You have no active subscriptions',
'subscription_info': '📋 Subscription: {name}\n💰 Price: ${price:.2f}\n⏱ Duration: {days} days\n📊 Traffic: {traffic}\n\n{description}',
'unlimited_traffic': 'Unlimited',
@@ -165,32 +243,48 @@ TRANSLATIONS = {
'subscription_expires': 'Expires: {date}',
'subscription_expired': 'Expired',
'subscription_active': 'Active until: {date}',
# Payments
'subscription_expired_message': '❌ Your subscription \'{name}\' has expired!\n\nTo renew, go to \'My Subscriptions\'.\n\n💡 You can renew at any time - the term will be extended from the current moment.',
'subscription_expires_today': '⚠️ Urgent! Your subscription \'{name}\' expires today!\n\nRenew it now in \'My Subscriptions\' to keep access.\n\n⏰ You have a few hours left!',
'subscription_expires_tomorrow': '⚠️ Your subscription \'{name}\' expires tomorrow!\n\nDon\'t forget to renew it in \'My Subscriptions\'.\n\n✨ Renewal will extend the term from expiration date.',
'subscription_expires_day_after_tomorrow': '⏰ Your subscription \'{name}\' expires in {days} days!\n\nWe recommend renewing it in advance in \'My Subscriptions\'.\n\n💰 Check your balance - you might want to top it up.',
'subscription_expires_in_days': '⏳ Your subscription \'{name}\' expires in {days} days!\n\nYou can renew it in \'My Subscriptions\'.\n\n💡 Early renewal extends from current expiration date.',
'extend_subscription_btn': '🔄 Extend Subscription',
'trial_subscription_expired': '🆓 Your trial subscription "{name}" has expired!\n\n'
'💡 To continue using VPN, please purchase a full plan.\n\n'
'✨ Various subscription plans with great prices are available!',
'my_subscriptions_btn': '📋 My Subscriptions',
'buy_new_subscription_btn': '🛒 Buy New Subscription',
'restore_subscription_btn': '🔄 Restore Subscription',
'payment_created': '✅ Top-up request created.\nContact @{support} for payment.',
'enter_amount': '💰 Enter amount to top up ($):',
'invalid_amount': '❌ Invalid amount',
'payment_card_info': '💳 For card payment contact @{support}',
'no_payments': '❌ Payment history is empty',
'payment_item': '{date}: ${amount:.2f} - {description} ({status})',
# Promocodes
'referral_program': 'Referral Program',
'referral_conditions': 'Program Conditions',
'referral_link': 'Your referral link',
'referral_code': 'Your promo code',
'referral_stats': 'Statistics',
'total_referrals': 'Total invited',
'active_referrals': 'Active referrals',
'total_earned': 'Total earned',
'my_referrals': 'My referrals',
'referral_reward_referrer': 'For each invited: $150',
'referral_reward_referred': 'Newbie bonus: $150 after topping up $300',
'referral_percentage': 'Percentage from payments: 25%',
'no_referrals': 'You have no referrals yet',
'referral_welcome': 'Welcome via referral link!',
'enter_promocode': '🎁 Enter promocode:',
'promocode_success': '✅ Promocode activated! Discount: {discount}',
'promocode_not_found': '❌ Promocode not found',
'promocode_expired': '❌ Promocode expired',
'promocode_used': '❌ Promocode already used',
'promocode_limit': '❌ Usage limit reached',
# Purchase
'insufficient_balance': '❌ Insufficient balance',
'subscription_purchased': '✅ Subscription purchased successfully!',
'purchase_error': '❌ Error purchasing subscription',
# Support
'support_message': '🎧 For support contact @{support}',
# Admin panel
'admin_menu': '⚙️ Admin Panel',
'manage_subscriptions': 'Manage Subscriptions',
'manage_users': 'Manage Users',
@@ -198,8 +292,6 @@ TRANSLATIONS = {
'manage_promocodes': 'Manage Promocodes',
'statistics': 'Statistics',
'not_admin': '❌ You don\'t have admin rights',
# Admin - subscriptions
'create_subscription': 'Create Subscription',
'enter_sub_name': 'Enter subscription name:',
'enter_sub_description': 'Enter subscription description:',
@@ -208,40 +300,95 @@ TRANSLATIONS = {
'enter_sub_traffic': 'Enter traffic limit (GB, 0 for unlimited):',
'enter_squad_uuid': 'Enter squad UUID from panel:',
'subscription_created': '✅ Subscription created',
# Admin - users
'user_list': '👥 User list:',
'user_item': 'ID: {id}, @{username}, Balance: ${balance:.2f}',
'enter_user_id': 'Enter user Telegram ID:',
'enter_balance_amount': 'Enter amount to add:',
'balance_added': '✅ Balance added',
'user_not_found': '❌ User not found',
# Admin - promocodes
'create_promocode': 'Create Promocode',
'enter_promo_code': 'Enter promocode:',
'enter_promo_discount': 'Enter discount amount ($):',
'enter_promo_limit': 'Enter usage limit:',
'promocode_created': '✅ Promocode created',
'promocode_exists': '❌ Promocode already exists',
# Statistics
'stats_info': '📊 Statistics:\n👥 Users: {users}\n📋 Subscriptions: {subscriptions}\n💰 Revenue: ${revenue:.2f}',
# Errors
'error_occurred': '❌ An error occurred',
'try_again': 'Try again',
'invalid_input': '❌ Invalid input',
'cancel': 'Cancel',
# Connection
'connection_link': '🔗 Connection link:\n\n`{link}`\n\nClick the link to copy',
'connection_error': '❌ Error getting connection link',
'lucky_game': 'Lucky Game',
'lucky_game_title': '🎰 Test Your Luck!',
'lucky_game_description': 'Choose a number from 1 to {numbers} and try to guess the winning one!',
'lucky_game_reward': 'Winning reward: ${reward}',
'lucky_game_rules': 'Game Rules',
'lucky_game_play': 'Play!',
'lucky_game_history': 'Game History',
'lucky_game_stats': 'Your Statistics',
'lucky_game_choose_number': 'Choose a number from 1 to {numbers}',
'lucky_game_winner': '🎉 CONGRATULATIONS! YOU WON!',
'lucky_game_loser': '😔 Better luck next time...',
'lucky_game_your_number': 'Your number: {number}',
'lucky_game_winning_numbers': 'Winning numbers: {numbers}',
'lucky_game_reward_added': 'Reward ${reward} added to balance!',
'lucky_game_new_balance': 'New balance: ${balance}',
'lucky_game_try_tomorrow': 'Try again tomorrow!',
'lucky_game_already_played': 'You already played today! Come back tomorrow.',
'lucky_game_come_back_in': 'Come back in {time}',
'lucky_game_no_history': 'You haven\'t played the lucky game yet.',
'lucky_game_start_playing': 'Start playing to see history!',
'lucky_game_games_played': 'Games played: {count}',
'lucky_game_wins': 'Wins: {count}',
'lucky_game_total_won': 'Total won: ${amount}',
'lucky_game_win_rate': 'Win rate: {rate}%',
'admin_user_subscriptions': 'User Subscriptions',
'all_user_subscriptions': 'All User Subscriptions',
'user_subscription_filters': 'Subscription Filters',
'active_subscriptions': 'Active Subscriptions',
'expired_subscriptions': 'Expired Subscriptions',
'expiring_subscriptions': 'Expiring Subscriptions',
'autopay_subscriptions': 'Autopay Subscriptions',
'trial_subscriptions': 'Trial Subscriptions',
'imported_subscriptions': 'Imported Subscriptions',
'subscription_details': 'Subscription Details',
'user_info': 'User Information',
'subscription_info': 'Subscription Information',
'subscription_status': 'Subscription Status',
'subscription_dates': 'Time Frames',
'autopay_settings': 'Autopay Settings',
'subscription_type': 'Subscription Type',
'edit_subscription': 'Edit Subscription',
'toggle_status': 'Toggle Status',
'edit_expiry': 'Edit Expiry',
'edit_autopay': 'Configure Autopay',
'subscription_activated': 'Subscription activated',
'subscription_deactivated': 'Subscription deactivated',
'insufficient_funds': 'Insufficient funds',
'funds_needed': 'Need more',
'created_date': 'Created Date',
'expires_date': 'Expiry Date',
'updated_date': 'Updated Date',
'days_left': 'Days Left',
'autopay_enabled': 'Autopay Enabled',
'autopay_disabled': 'Autopay Disabled',
'renew_before_days': 'Renew Before Days',
'view_details': 'View Details',
'back_to_list': 'Back to List',
'refresh_data': 'Refresh Data',
'no_subscriptions_found': 'No subscriptions found',
'subscription_not_found': 'Subscription not found',
'database_unavailable': 'Database unavailable',
'error_loading_subscriptions': 'Error loading subscriptions',
'error_loading_details': 'Error loading details',
'pagination_error': 'Navigation error',
'edit_error': 'Edit error',
'status_toggle_error': 'Status toggle error'
}
}
def t(key: str, lang: str = 'ru', **kwargs) -> str:
"""Get translation for key with optional formatting"""
translation = TRANSLATIONS.get(lang, TRANSLATIONS['ru']).get(key, key)
if kwargs:
try:
+165
View File
@@ -0,0 +1,165 @@
import logging
import hmac
import hashlib
import json
from typing import Optional
from aiohttp import web
from aiogram import Bot
from sqlalchemy.orm import sessionmaker
from config import Config
from database import Database
logger = logging.getLogger(__name__)
def convert_period_to_months(period: Optional[str]) -> int:
"""Map Tribute subscription period strings to months."""
if not period:
return 1
mapping = {
"monthly": 1,
"quarterly": 3,
"3-month": 3,
"3months": 3,
"3-months": 3,
"q": 3,
"halfyearly": 6,
"yearly": 12,
"annual": 12,
"y": 12,
}
return mapping.get(period.lower(), 1)
class TributeService:
def __init__(
self,
bot: Bot,
config: Config,
db: Database,
):
self.bot = bot
self.config = config
self.db = db
async def handle_webhook(self, raw_body: bytes, signature_header: Optional[str]) -> web.Response:
def ok(data: Optional[dict] = None) -> web.Response:
payload = {"status": "ok"}
if data:
payload.update(data)
return web.json_response(payload, status=200)
def ignored(reason: str) -> web.Response:
return web.json_response({"status": "ignored", "reason": reason}, status=200)
def bad_request(reason: str) -> web.Response:
return web.json_response({"status": "error", "reason": reason}, status=400)
if hasattr(self.config, 'TRIBUTE_API_KEY') and self.config.TRIBUTE_API_KEY:
if not signature_header:
return web.json_response({"status": "error", "reason": "no_signature"}, status=403)
expected_sig = hmac.new(self.config.TRIBUTE_API_KEY.encode(), raw_body,
hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_sig, signature_header):
return web.json_response({"status": "error", "reason": "invalid_signature"}, status=403)
try:
payload = json.loads(raw_body.decode())
except Exception:
return bad_request("invalid_json")
logging.info(
"Tribute webhook data: %s",
json.dumps(payload, ensure_ascii=False),
)
event_name = payload.get("name")
data = payload.get("payload", {})
user_id = data.get("telegram_user_id")
if not user_id:
return ignored("missing_telegram_user_id")
amount_value = data.get("amount", 0)
currency = data.get("currency", "RUB").upper()
amount_float = round(amount_value / 100.0, 2)
if event_name == "new_donation":
await self._handle_new_donation(user_id, amount_float, currency, data)
elif event_name == "cancelled_subscription":
await self._handle_cancellation(user_id)
return ok({"event": event_name or "unknown"})
async def _handle_new_donation(self, user_id: int, amount: float, currency: str, data: dict):
try:
if not user_id:
logger.warning(f"No telegram_user_id in webhook data")
return
async with self.db.session_factory() as session:
payment = await self.db.create_payment(
user_id=int(user_id),
amount=amount,
payment_type='tribute',
description=f'Пополнение через Tribute: {amount} {currency}',
status='completed'
)
await self.db.add_balance(int(user_id), amount)
try:
success_msg = (
f"✅ **Платеж через Tribute получен!**\n\n"
f"💰 Сумма: {amount} {currency}\n"
f"🎉 Средства зачислены на баланс!\n\n"
f"💳 Ваш текущий баланс можно посмотреть в главном меню."
)
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Мой баланс", callback_data="balance")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
await self.bot.send_message(
int(user_id),
success_msg,
reply_markup=keyboard,
parse_mode="Markdown"
)
except Exception as e:
logger.error(f"Failed to send Tribute payment success message to user {user_id}: {e}")
await session.commit()
logger.info(f"Successfully processed Tribute donation: {amount} {currency} for user {user_id}")
except Exception as e:
logger.error(f"Error handling tribute donation: {e}")
async def _handle_cancellation(self, user_id: int):
try:
cancellation_msg = (
"🚨 Ваш платеж Tribute был отменен.\n\n"
"Если это произошло по ошибке, обратитесь в поддержку."
)
await self.bot.send_message(
int(user_id),
cancellation_msg,
parse_mode="HTML"
)
logger.info(f"Tribute subscription cancelled for user {user_id}")
except Exception as e:
logger.error(f"Error handling tribute cancellation for user {user_id}: {e}")
async def tribute_webhook_route(request: web.Request):
tribute_service: TributeService = request.app['tribute_service']
raw_body = await request.read()
signature_header = request.headers.get('trbt-signature')
return await tribute_service.handle_webhook(raw_body, signature_header)
+231 -232
View File
@@ -1,194 +1,110 @@
import re
import uuid
import logging
import secrets
import string
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any, Tuple
import logging
from typing import Tuple, Optional, Dict, Any
logger = logging.getLogger(__name__)
def generate_username() -> str:
"""Generate random username for RemnaWave"""
return f"user_{secrets.token_hex(8)}"
def generate_password() -> str:
"""Generate random password"""
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(12))
def generate_promocode() -> str:
"""Generate random promocode"""
alphabet = string.ascii_uppercase + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(8))
def is_valid_email(email: str) -> bool:
"""Validate email format"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def is_valid_amount(amount_str: str) -> Tuple[bool, float]:
"""Validate and parse amount"""
def is_valid_amount(text: str) -> Tuple[bool, float]:
try:
amount = float(amount_str.replace(',', '.'))
text = text.strip().replace(' ', '').replace(',', '.')
amount = float(text)
if amount <= 0:
return False, 0
if amount > 100000: # Max amount limit
return False, 0
return False, 0.0
if amount > 1000000:
return False, 0.0
amount = round(amount, 2)
return True, amount
except ValueError:
return False, 0
def format_date(date: datetime, lang: str = 'ru') -> str:
"""Format date for display"""
if lang == 'ru':
months = [
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
]
return f"{date.day} {months[date.month-1]} {date.year}"
else:
return date.strftime("%B %d, %Y")
def format_datetime(date: datetime, lang: str = 'ru') -> str:
"""Format datetime for display"""
if lang == 'ru':
return date.strftime("%d.%m.%Y %H:%M")
else:
return date.strftime("%m/%d/%Y %H:%M")
def calculate_expiry_date(days: int) -> str:
"""Calculate expiry date in ISO format"""
expiry = datetime.utcnow() + timedelta(days=days)
return expiry.isoformat() + 'Z'
def parse_telegram_id(text: str) -> Optional[int]:
"""Parse Telegram ID from text"""
try:
telegram_id = int(text.strip())
if telegram_id > 0:
return telegram_id
except ValueError:
pass
return None
def format_traffic(gb: int, lang: str = 'ru') -> str:
"""Format traffic limit for display"""
if gb == 0:
return "Безлимитный" if lang == 'ru' else "Unlimited"
else:
return f"{gb} ГБ" if lang == 'ru' else f"{gb} GB"
def paginate_list(items: List[Any], page: int, per_page: int = 10) -> Tuple[List[Any], int]:
"""Paginate list of items"""
total_pages = (len(items) + per_page - 1) // per_page
start_idx = (page - 1) * per_page
end_idx = start_idx + per_page
return items[start_idx:end_idx], total_pages
def escape_markdown(text: str) -> str:
"""Escape markdown special characters"""
special_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
for char in special_chars:
text = text.replace(char, f'\\{char}')
return text
def truncate_text(text: str, max_length: int = 4000) -> str:
"""Truncate text to fit Telegram message limits"""
if len(text) <= max_length:
return text
return text[:max_length-3] + "..."
def validate_squad_uuid(uuid_str: str) -> bool:
"""Validate UUID format"""
try:
uuid.UUID(uuid_str)
return True
except ValueError:
return False
def format_subscription_info(subscription: Dict[str, Any], lang: str = 'ru') -> str:
"""Format subscription information for display"""
from translations import t
traffic = format_traffic(subscription['traffic_limit_gb'], lang)
info = t('subscription_info', lang,
name=subscription['name'],
price=subscription['price'],
days=subscription['duration_days'],
traffic=traffic,
description=subscription.get('description', '')
)
return info
def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[str, Any],
expires_at: datetime, lang: str = 'ru') -> str:
"""Format user subscription information"""
from translations import t
traffic = format_traffic(subscription['traffic_limit_gb'], lang)
# Check if expired
now = datetime.utcnow()
if expires_at < now:
status = t('subscription_expired', lang)
else:
status = t('subscription_active', lang, date=format_date(expires_at, lang))
info = f"📋 {subscription['name']}\n"
info += f"{subscription['duration_days']} дней\n" if lang == 'ru' else f"{subscription['duration_days']} days\n"
info += f"📊 {traffic}\n"
info += f"🕒 {status}\n"
if subscription.get('description'):
info += f"\n{subscription['description']}"
return info
except (ValueError, TypeError):
return False, 0.0
def validate_promocode_format(code: str) -> bool:
"""Validate promocode format"""
if not code:
return False
code = code.strip().upper()
if len(code) < 3 or len(code) > 20:
return False
if not re.match(r'^[A-Z0-9]+$', code.upper()):
if not code.replace('_', '').isalnum():
return False
return True
def calculate_discount(original_price: float, promocode: Dict[str, Any]) -> float:
"""Calculate discount amount"""
if promocode.get('discount_percent'):
return original_price * (promocode['discount_percent'] / 100)
def validate_squad_uuid(uuid: str) -> bool:
if not uuid or not isinstance(uuid, str):
return False
uuid = uuid.strip()
if len(uuid) < 8:
return False
allowed_chars = set('0123456789abcdefABCDEF-')
if not all(c in allowed_chars for c in uuid):
return False
return True
def parse_telegram_id(text: str) -> Optional[int]:
try:
text = text.strip().replace(' ', '')
if text.startswith('@'):
text = text[1:]
if text.startswith('id'):
text = text[2:]
telegram_id = int(text)
if telegram_id <= 0 or telegram_id > 9999999999:
return None
return telegram_id
except (ValueError, TypeError):
return None
def generate_username() -> str:
prefix = "user_"
random_part = ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(8))
return prefix + random_part
def generate_password() -> str:
return ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12))
def calculate_expiry_date(days: int) -> str:
expiry_date = datetime.now() + timedelta(days=days)
return expiry_date.isoformat() + 'Z'
def format_datetime(dt: datetime, language: str = 'ru') -> str:
if not dt:
return "N/A"
if language == 'ru':
return dt.strftime('%d.%m.%Y %H:%M')
else:
return min(promocode.get('discount_amount', 0), original_price)
return dt.strftime('%Y-%m-%d %H:%M')
def format_payment_status(status: str, lang: str = 'ru') -> str:
"""Format payment status for display"""
status_map = {
'pending': 'В ожидании' if lang == 'ru' else 'Pending',
'completed': 'Завершен' if lang == 'ru' else 'Completed',
'cancelled': 'Отменен' if lang == 'ru' else 'Cancelled',
'failed': 'Ошибка' if lang == 'ru' else 'Failed'
}
return status_map.get(status, status)
def clean_phone_number(phone: str) -> str:
"""Clean and format phone number"""
# Remove all non-digit characters
digits = re.sub(r'\D', '', phone)
def format_date(dt: datetime, language: str = 'ru') -> str:
if not dt:
return "N/A"
# Handle Russian phone numbers
if digits.startswith('8') and len(digits) == 11:
digits = '7' + digits[1:]
elif digits.startswith('9') and len(digits) == 10:
digits = '7' + digits
return digits
if language == 'ru':
return dt.strftime('%d.%m.%Y')
else:
return dt.strftime('%Y-%m-%d')
def format_bytes(bytes_value: int) -> str:
"""Format bytes to human readable format"""
if bytes_value == 0:
return "0 B"
@@ -200,76 +116,159 @@ def format_bytes(bytes_value: int) -> str:
value /= 1024
unit_index += 1
if unit_index == 0:
return f"{int(value)} {units[unit_index]}"
else:
if value >= 100:
return f"{value:.0f} {units[unit_index]}"
elif value >= 10:
return f"{value:.1f} {units[unit_index]}"
else:
return f"{value:.2f} {units[unit_index]}"
def get_subscription_connection_url(base_url: str, short_uuid: str) -> str:
"""Generate subscription connection URL"""
return f"{base_url.rstrip('/')}/api/sub/{short_uuid}"
def format_payment_status(status: str, language: str = 'ru') -> str:
status_map = {
'ru': {
'pending': 'Ожидает',
'completed': 'Завершен',
'cancelled': 'Отменен',
'failed': 'Ошибка'
},
'en': {
'pending': 'Pending',
'completed': 'Completed',
'cancelled': 'Cancelled',
'failed': 'Failed'
}
}
return status_map.get(language, status_map['ru']).get(status, status)
def format_subscription_info(subscription: Dict[str, Any], language: str = 'ru') -> str:
text = ""
if language == 'ru':
text += f"📋 **Подписка: {subscription['name']}**\n\n"
text += f"💰 Цена: {subscription['price']} руб.\n"
text += f"⏱ Длительность: {subscription['duration_days']} дн.\n"
if subscription['traffic_limit_gb'] > 0:
text += f"📊 Лимит трафика: {subscription['traffic_limit_gb']} ГБ\n"
else:
text += f"📊 Лимит трафика: Безлимит\n"
if subscription.get('description'):
text += f"\n📝 Описание:\n{subscription['description']}"
else:
text += f"📋 **Subscription: {subscription['name']}**\n\n"
text += f"💰 Price: ${subscription['price']}\n"
text += f"⏱ Duration: {subscription['duration_days']} days\n"
if subscription['traffic_limit_gb'] > 0:
text += f"📊 Traffic limit: {subscription['traffic_limit_gb']} GB\n"
else:
text += f"📊 Traffic limit: Unlimited\n"
if subscription.get('description'):
text += f"\n📝 Description:\n{subscription['description']}"
return text
def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[str, Any],
expires_at: datetime, language: str = 'ru') -> str:
text = ""
if language == 'ru':
text += f"📋 **{subscription['name']}**\n\n"
now = datetime.utcnow()
if expires_at < now:
status = "❌ Истекла"
days_left = 0
elif not user_sub.get('is_active', True):
status = "⏸ Приостановлена"
days_left = (expires_at - now).days
else:
days_left = (expires_at - now).days
status = f"✅ Активна"
text += f"🔘 Статус: {status}\n"
text += f"📅 Истекает: {format_datetime(expires_at, language)}\n"
if days_left > 0:
text += f"⏰ Осталось: {days_left} дн.\n"
if subscription['traffic_limit_gb'] > 0:
text += f"📊 Лимит трафика: {subscription['traffic_limit_gb']} ГБ\n"
else:
text += f"📊 Лимит трафика: Безлимит\n"
if subscription.get('name') == "Старая подписка" or (subscription.get('description') and 'импорт' in subscription.get('description', '').lower()):
text += f"\n🔄 Тип: Импортированная из старой системы\n"
text += f"ℹ️ Продление недоступно"
if subscription.get('description') and not ('импорт' in subscription.get('description', '').lower()):
text += f"\n📝 {subscription['description']}"
else:
text += f"📋 **{subscription['name']}**\n\n"
now = datetime.utcnow()
if expires_at < now:
status = "❌ Expired"
days_left = 0
elif not user_sub.get('is_active', True):
status = "⏸ Suspended"
days_left = (expires_at - now).days
else:
days_left = (expires_at - now).days
status = f"✅ Active"
text += f"🔘 Status: {status}\n"
text += f"📅 Expires: {format_datetime(expires_at, language)}\n"
if days_left > 0:
text += f"⏰ Days left: {days_left}\n"
if subscription['traffic_limit_gb'] > 0:
text += f"📊 Traffic limit: {subscription['traffic_limit_gb']} GB\n"
else:
text += f"📊 Traffic limit: Unlimited\n"
if subscription.get('name') == "Старая подписка" or (subscription.get('description') and 'import' in subscription.get('description', '').lower()):
text += f"\n🔄 Type: Imported from old system\n"
text += f"️ Extension not available"
if subscription.get('description') and not ('import' in subscription.get('description', '').lower()):
text += f"\n📝 {subscription['description']}"
return text
def log_user_action(user_id: int, action: str, details: str = ""):
"""Log user action"""
logger.info(f"User {user_id} - {action}: {details}")
logger.info(f"USER_ACTION: {user_id} - {action}" + (f" - {details}" if details else ""))
def format_subscription_status(expires_at: datetime, lang: str = 'ru') -> str:
"""Format subscription status with emoji"""
now = datetime.utcnow()
def bytes_to_gb(bytes_value: int) -> float:
if not bytes_value or bytes_value == 0:
return 0.0
return round(bytes_value / (1024**3), 2)
def format_memory_usage(used_gb: float, total_gb: float) -> str:
if total_gb == 0:
return "N/A"
if expires_at < now:
return "❌ Истекла" if lang == 'ru' else "❌ Expired"
usage_percent = (used_gb / total_gb) * 100
available_gb = total_gb - used_gb
days_left = (expires_at - now).days
return f"{used_gb:.1f}/{total_gb:.1f} ГБ ({usage_percent:.1f}%) • Доступно: {available_gb:.1f} ГБ"
def format_uptime(uptime_seconds: float) -> str:
if uptime_seconds <= 0:
return "N/A"
if days_left == 0:
return "⚠️ Истекает сегодня" if lang == 'ru' else "⚠️ Expires today"
elif days_left == 1:
return "⚠️ Истекает завтра" if lang == 'ru' else "⚠️ Expires tomorrow"
elif days_left <= 3:
return f"🔶 Осталось {days_left} дней" if lang == 'ru' else f"🔶 {days_left} days left"
uptime_hours = int(uptime_seconds // 3600)
uptime_days = uptime_hours // 24
uptime_hours = uptime_hours % 24
uptime_minutes = int((uptime_seconds % 3600) // 60)
if uptime_days > 0:
return f"{uptime_days}д {uptime_hours}ч"
elif uptime_hours > 0:
return f"{uptime_hours}ч {uptime_minutes}м"
else:
return f"✅ Активна ({days_left} дней)" if lang == 'ru' else f"✅ Active ({days_left} days)"
def format_monitor_notification_type(notification_type: str, lang: str = 'ru') -> str:
"""Format notification type for display"""
type_map = {
'expired': 'Истекла' if lang == 'ru' else 'Expired',
'expires_today': 'Истекает сегодня' if lang == 'ru' else 'Expires today',
'expires_tomorrow': 'Истекает завтра' if lang == 'ru' else 'Expires tomorrow',
'warning': 'Предупреждение' if lang == 'ru' else 'Warning',
'urgent': 'Срочно' if lang == 'ru' else 'Urgent'
}
return type_map.get(notification_type, notification_type)
def calculate_days_until_expiry(expires_at: datetime) -> int:
"""Calculate days until expiry"""
now = datetime.utcnow()
delta = expires_at - now
return max(0, delta.days)
def is_subscription_expiring_soon(expires_at: datetime, warning_days: int = 2) -> bool:
"""Check if subscription is expiring soon"""
days_left = calculate_days_until_expiry(expires_at)
return days_left <= warning_days
class States:
"""State constants for FSM"""
WAITING_LANGUAGE = "waiting_language"
WAITING_AMOUNT = "waiting_amount"
WAITING_PROMOCODE = "waiting_promocode"
# Admin states
ADMIN_CREATE_SUB_NAME = "admin_create_sub_name"
ADMIN_CREATE_SUB_DESC = "admin_create_sub_desc"
ADMIN_CREATE_SUB_PRICE = "admin_create_sub_price"
ADMIN_CREATE_SUB_DAYS = "admin_create_sub_days"
ADMIN_CREATE_SUB_TRAFFIC = "admin_create_sub_traffic"
ADMIN_CREATE_SUB_SQUAD = "admin_create_sub_squad"
ADMIN_ADD_BALANCE_USER = "admin_add_balance_user"
ADMIN_ADD_BALANCE_AMOUNT = "admin_add_balance_amount"
ADMIN_CREATE_PROMO_CODE = "admin_create_promo_code"
ADMIN_CREATE_PROMO_DISCOUNT = "admin_create_promo_discount"
ADMIN_CREATE_PROMO_LIMIT = "admin_create_promo_limit"
return f"{uptime_minutes}м"
+70
View File
@@ -0,0 +1,70 @@
import logging
from aiohttp import web, ClientSession
from aiogram import Bot
from database import Database
from config import Config
from tribute_service import TributeService, tribute_webhook_route
logger = logging.getLogger(__name__)
class WebhookServer:
def __init__(self, bot: Bot, db: Database, config: Config):
self.bot = bot
self.db = db
self.config = config
self.app = None
self.runner = None
self.site = None
async def create_app(self):
self.app = web.Application()
tribute_service = TributeService(self.bot, self.config, self.db)
self.app['tribute_service'] = tribute_service
self.app.router.add_post(self.config.TRIBUTE_WEBHOOK_PATH, tribute_webhook_route)
async def health_check(request):
return web.json_response({"status": "ok", "service": "tribute-webhooks"})
self.app.router.add_get('/health', health_check)
logger.info(f"Webhook server configured with route: {self.config.TRIBUTE_WEBHOOK_PATH}")
return self.app
async def start(self):
try:
if not self.app:
await self.create_app()
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(
self.runner,
host='0.0.0.0',
port=self.config.TRIBUTE_WEBHOOK_PORT
)
await self.site.start()
logger.info(f"✅ Webhook server started on port {self.config.TRIBUTE_WEBHOOK_PORT}")
logger.info(f"🎯 Tribute webhook URL: http://your-server:{self.config.TRIBUTE_WEBHOOK_PORT}{self.config.TRIBUTE_WEBHOOK_PATH}")
except Exception as e:
logger.error(f"❌ Failed to start webhook server: {e}")
raise
async def stop(self):
try:
if self.site:
await self.site.stop()
logger.info("Webhook site stopped")
if self.runner:
await self.runner.cleanup()
logger.info("Webhook runner cleaned up")
except Exception as e:
logger.error(f"Error stopping webhook server: {e}")