Compare commits

...

269 Commits

Author SHA1 Message Date
Egor 5aad1e025b Update users.py 2025-09-04 21:56:27 +03:00
Egor e811e7c877 Merge pull request #22 from Fr1ngg/dev
Dev
2025-09-04 21:45:36 +03:00
Egor 932e9f292b Update admin.py 2025-09-04 21:42:20 +03:00
Egor 03749574d6 Update users.py 2025-09-04 21:33:09 +03:00
Egor 1059a51688 Update users.py 2025-09-04 21:30:31 +03:00
Egor a1963d5b17 Update users.py 2025-09-04 21:26:07 +03:00
Egor 426e736cd5 Update users.py 2025-09-04 21:18:18 +03:00
Egor fb03cff403 Update users.py 2025-09-04 21:11:28 +03:00
Egor 40780f1184 Update users.py 2025-09-04 21:00:58 +03:00
Egor c76290b51e Update users.py 2025-09-04 20:55:01 +03:00
Egor 5b6dd3058e Update users.py 2025-09-04 20:46:52 +03:00
Egor c8d91a1ec1 Update admin.py 2025-09-04 20:43:55 +03:00
Egor 21952f84eb Update states.py 2025-09-04 20:42:54 +03:00
Egor 7bdfaa312b Update users.py 2025-09-04 20:42:19 +03:00
Egor 1829a469e7 Merge pull request #21 from Fr1ngg/dev
Dev
2025-09-04 18:40:02 +03:00
Egor ddd37a4e98 Update monitoring_service.py 2025-09-04 18:38:44 +03:00
Egor 2804c701c0 Update monitoring_service.py 2025-09-04 18:35:59 +03:00
Egor b3c85b2db5 Delete app/handlers/subscription_notifications.py 2025-09-04 18:28:42 +03:00
Egor 987b8170c6 Update monitoring_service.py 2025-09-04 18:26:38 +03:00
Egor ab49f3cd1e Merge pull request #20 from Fr1ngg/dev
Dev
2025-09-04 16:50:49 +03:00
Egor 732e8f7b8b Update README.md 2025-09-04 16:48:09 +03:00
Egor 9273a4e15b Update .env.example 2025-09-04 16:47:42 +03:00
Egor d89293f76a Update user_service.py 2025-09-04 16:45:57 +03:00
Egor 174462183d Update config.py 2025-09-04 16:18:34 +03:00
Egor 783927be37 Update subscription_service.py 2025-09-04 16:13:58 +03:00
Egor 98c68dd8dc Update inline.py 2025-09-04 16:01:38 +03:00
Egor 13585eab54 Update config.py 2025-09-04 15:53:22 +03:00
Egor 169336da43 Update config.py 2025-09-04 15:48:36 +03:00
Egor 9ef875f6bf Update config.py 2025-09-04 15:46:36 +03:00
Egor 3f54706a72 Update config.py 2025-09-04 15:39:09 +03:00
Egor 835e5e6f7f Update config.py 2025-09-04 15:24:29 +03:00
Egor b292442e8d Update config.py 2025-09-04 15:16:56 +03:00
Egor 9faaf5c963 Update inline.py 2025-09-04 15:13:50 +03:00
Egor 117e88fe4a Update subscription.py 2025-09-04 15:04:07 +03:00
Egor 96bf08a07c Update inline.py 2025-09-04 15:02:59 +03:00
Egor 4523403de5 Update config.py 2025-09-04 15:01:25 +03:00
Egor 02682e88c1 Update subscription.py 2025-09-04 14:48:57 +03:00
Egor 323b9a7e18 Update subscription.py 2025-09-04 14:48:10 +03:00
Egor 100dc3730f Update subscription.py 2025-09-04 14:43:09 +03:00
Egor 919047317a Update subscription.py 2025-09-04 14:38:12 +03:00
Egor 6ae5b59ed1 Update subscription.py 2025-09-04 14:37:47 +03:00
Egor b3123e2e85 Update subscription.py 2025-09-04 14:34:09 +03:00
Egor fbe0ca790e Update subscription.py 2025-09-04 14:31:40 +03:00
Egor 785648ba7a Update subscription.py 2025-09-04 14:27:22 +03:00
Egor 06f8728efa Update inline.py 2025-09-04 14:21:00 +03:00
Egor 99f102a06e Update config.py 2025-09-04 14:19:23 +03:00
Egor d759dd3a66 Update user_service.py 2025-09-04 13:58:58 +03:00
Egor 73dc22b0b8 Update texts.py 2025-09-04 04:27:33 +03:00
Egor 5a082f0eb8 Update texts.py 2025-09-04 04:20:58 +03:00
Egor 073d5a119b Update subscription.py 2025-09-04 04:20:36 +03:00
Egor 2dab106bef Update README.md 2025-09-04 04:13:00 +03:00
Egor ec8c7b7242 Update .env.example 2025-09-04 04:12:46 +03:00
Egor a4b96a089f Update user_service.py 2025-09-04 04:07:09 +03:00
Egor 269f8f6220 Update user_service.py 2025-09-04 04:04:19 +03:00
Egor f1beda2b89 Update user_service.py 2025-09-04 04:01:03 +03:00
Egor 6a00db32ae Update config.py 2025-09-04 03:28:51 +03:00
Egor 276bc5eb40 Update .env.example 2025-09-04 03:27:58 +03:00
Egor 8678afd055 Update subscription.py 2025-09-04 03:26:34 +03:00
Egor 31340ad4a5 Update subscription_service.py 2025-09-04 03:25:08 +03:00
Egor 98ce47ce71 Update inline.py 2025-09-04 03:24:07 +03:00
Egor 3462f5b3b1 Update subscription.py 2025-09-04 02:57:42 +03:00
Egor 7ec606f93c Update config.py 2025-09-03 21:11:15 +03:00
Egor 7d13bf47aa Update README.md 2025-09-03 21:08:32 +03:00
Egor 87773806fc Update .env.example 2025-09-03 21:08:09 +03:00
Egor dad6200ad8 Update bot.py 2025-09-03 21:07:40 +03:00
Egor c3991162c2 Update payment_service.py 2025-09-03 21:06:48 +03:00
Egor 08feb1f876 Update telegram_stars.py 2025-09-03 21:05:33 +03:00
Egor 6f15070052 Update balance.py 2025-09-03 21:05:13 +03:00
Egor 0c4e73ab92 Create stars_payments.py 2025-09-03 21:04:50 +03:00
Egor 48902c4968 Update telegram_stars.py 2025-09-03 20:57:01 +03:00
Egor 3c715577a6 Update telegram_stars.py 2025-09-03 20:55:25 +03:00
Egor 8e67c7a97a Update texts.py 2025-09-03 20:09:01 +03:00
Egor c938d48026 Update texts.py 2025-09-03 20:02:46 +03:00
Egor 99a4927f8a Update texts.py 2025-09-03 19:53:10 +03:00
Egor df5b8dcdcd Update inline.py 2025-09-03 19:52:51 +03:00
Egor d4a1a2e64d Update menu.py 2025-09-03 19:52:34 +03:00
Egor 5e2df269a3 Update start.py 2025-09-03 19:52:16 +03:00
Egor a2056e0441 Update docker-registry.yml 2025-09-03 18:38:28 +03:00
Egor a6bbd8f8fb Add files via upload 2025-09-03 06:39:35 +03:00
Egor 9f4e0cf5f8 Update menu.py 2025-09-03 06:34:44 +03:00
Egor c7e8365d3f Update start.py 2025-09-03 06:33:37 +03:00
Egor 49b10b490e Update inline.py 2025-09-03 06:29:20 +03:00
Egor f26bca8627 Update texts.py 2025-09-03 06:27:46 +03:00
Egor df139b3d34 Update texts.py 2025-09-03 06:22:09 +03:00
Egor 05d1edf278 Update menu.py 2025-09-03 06:21:21 +03:00
Egor a518df8ff0 Update menu.py 2025-09-03 06:16:38 +03:00
Egor 0a13fe4f40 Update menu.py 2025-09-03 06:13:15 +03:00
Egor 6410bd1085 Update menu.py 2025-09-03 06:03:26 +03:00
Egor 55c4549cff Update inline.py 2025-09-03 06:01:29 +03:00
Egor e898948073 Update texts.py 2025-09-03 05:59:21 +03:00
Egor 5f1c8d8ac0 Update README.md 2025-09-03 05:02:51 +03:00
Egor 8d1f8f2355 Update README.md 2025-09-03 04:56:05 +03:00
Egor fbbbad608b Update .env.example 2025-09-03 04:55:38 +03:00
Egor 03fc4abd2a Update subscription.py 2025-09-03 04:32:16 +03:00
Egor f280a37818 Update subscription.py 2025-09-03 04:25:31 +03:00
Egor f100ec62d4 Update subscription.py 2025-09-03 04:24:55 +03:00
Egor cd827612d9 Update subscription.py 2025-09-03 04:23:41 +03:00
Egor 50da2f82c8 Update subscription_service.py 2025-09-03 04:22:05 +03:00
Egor 4c814ec5f4 Update inline.py 2025-09-03 04:13:35 +03:00
Egor fb8c02d5ab Update subscription.py 2025-09-03 04:12:34 +03:00
Egor 465ad3d1ad Update inline.py 2025-09-03 04:10:55 +03:00
Egor 2bfaab7d16 Update subscription_service.py 2025-09-03 04:07:33 +03:00
Egor e356542df7 Update subscription.py 2025-09-03 00:46:11 +03:00
Egor a3dfbdce3b Update inline.py 2025-09-03 00:44:57 +03:00
Egor a666e8d248 Update subscription_service.py 2025-09-03 00:44:18 +03:00
Egor 9ef8df22a8 Update subscription.py 2025-09-03 00:43:54 +03:00
Egor a1471c8b9a Update subscription.py 2025-09-03 00:39:35 +03:00
Egor a99a14922c Update subscription_service.py 2025-09-03 00:17:11 +03:00
Egor ce954ad775 Update subscription.py 2025-09-03 00:15:42 +03:00
Egor efba7c10d0 Update subscription.py 2025-09-03 00:15:01 +03:00
Egor cd10c131b2 Update inline.py 2025-09-03 00:14:30 +03:00
Egor 3e4cb8dfa2 Update inline.py 2025-09-02 23:38:52 +03:00
Egor 6471ab547c Update subscription.py 2025-09-02 23:37:57 +03:00
Egor c10bf07124 Update inline.py 2025-09-02 23:32:14 +03:00
Egor 4e05882522 Update subscription.py 2025-09-02 23:30:51 +03:00
Egor 061b292d92 Update subscription.py 2025-09-02 23:28:30 +03:00
Egor 779a6c309a Update subscription.py 2025-09-02 23:24:16 +03:00
Egor c79c71f209 Update remnawave_api.py 2025-09-02 17:35:45 +03:00
Egor d4dbce5606 Update remnawave_api.py 2025-09-02 17:32:41 +03:00
Egor e53d99223d Update config.py 2025-09-02 15:32:45 +03:00
Egor ec7274e0c9 Update remnawave_api.py 2025-09-02 15:32:29 +03:00
Egor 8fe631a98e Update remnawave_api.py 2025-09-02 15:26:00 +03:00
Egor bd831bd92b Update config.py 2025-09-02 15:21:43 +03:00
Egor 9b31ba8f3d Update remnawave_api.py 2025-09-02 15:21:15 +03:00
Egor 3759be333a Update remnawave_api.py 2025-09-02 15:15:25 +03:00
Egor 555894b26c Update README.md 2025-09-02 14:55:47 +03:00
Egor bb636c003c Update .env.example 2025-09-02 14:50:48 +03:00
Egor 40c7b4aebd Update remnawave_api.py 2025-09-02 14:23:38 +03:00
Egor 028bd4888b Update remnawave_api.py 2025-09-02 14:10:01 +03:00
Egor 27d64e361a Update remnawave_api.py 2025-09-02 14:03:04 +03:00
Egor 6fc7dd49dd Update remnawave_api.py 2025-09-02 13:40:32 +03:00
Egor b835ba1d65 Update remnawave_api.py 2025-09-02 13:33:15 +03:00
Egor 811e985087 Update .env.example 2025-09-02 13:29:29 +03:00
Egor c16733b93d Update config.py 2025-09-02 13:29:13 +03:00
Egor 23ab3c5198 Update remnawave_service.py 2025-09-02 13:28:09 +03:00
Egor 9b8f121e69 Update remnawave_api.py 2025-09-02 13:25:49 +03:00
Egor 86b1b95fb9 Update subscription.py 2025-09-02 08:52:42 +03:00
Egor 9418681a00 Update subscription_service.py 2025-09-02 08:50:35 +03:00
Egor 7ebd937e2c Update README.md 2025-09-02 02:21:40 +03:00
Egor 25c404f999 Update README.md 2025-09-02 02:09:29 +03:00
Egor 6df92bde2a Update README.md 2025-09-02 00:34:13 +03:00
Egor 60ff006fec Update README.md 2025-09-02 00:33:03 +03:00
Egor 0181f5e10c Update docker-compose.yml 2025-09-02 00:30:33 +03:00
Egor 4bbcc60bbb Update subscriptions.py 2025-09-02 00:16:07 +03:00
Egor 8e2138e2aa Update subscriptions.py 2025-09-02 00:14:45 +03:00
Egor 70b9e8c10d Update inline.py 2025-09-01 23:44:56 +03:00
Egor dc1eddb0df Update subscription.py 2025-09-01 23:44:02 +03:00
Egor 630551cc4b Update README.md 2025-09-01 23:17:46 +03:00
Egor 4f94b25182 Update docker-compose.yml 2025-09-01 22:50:17 +03:00
Egor d3b240eae3 Update Dockerfile 2025-09-01 22:49:41 +03:00
Egor e2883d6727 Update main.py 2025-09-01 22:29:47 +03:00
Egor 21757b37ab Update bot.py 2025-09-01 22:27:37 +03:00
Egor 9190857ffa Update maintenance_service.py 2025-09-01 22:27:08 +03:00
Egor a607443aa7 Update README.md 2025-09-01 19:59:39 +03:00
Egor 41969bc0c2 Update .env.example 2025-09-01 19:59:20 +03:00
Egor 0d443afb87 Update balance.py 2025-09-01 19:58:21 +03:00
Egor 3d85294d41 Update config.py 2025-09-01 19:55:23 +03:00
Egor 86896771a6 Update inline.py 2025-09-01 05:02:10 +03:00
Egor 42d52990fa Update README.md 2025-09-01 04:34:16 +03:00
Egor 6296c8257f Update README.md 2025-09-01 04:13:29 +03:00
Egor 14a40cb1b6 Create docker-compose.yml 2025-09-01 04:10:55 +03:00
Egor d0e3650de5 Update config.py 2025-09-01 03:52:56 +03:00
Egor df8ed666ae Update menu.py 2025-09-01 03:51:29 +03:00
Egor 2217b7f800 Update config.py 2025-09-01 03:46:13 +03:00
Egor 924febc2f3 Update menu.py 2025-09-01 03:43:27 +03:00
Egor 729ffb7952 Update messages.py 2025-09-01 01:06:54 +03:00
Egor 1568ca7633 Update README.md 2025-09-01 00:11:27 +03:00
Egor 7d5e476580 Update referral.py 2025-08-31 23:18:00 +03:00
Egor 9bcd187358 Update referral.py 2025-08-31 23:14:44 +03:00
Egor a9cbb6272f Update referral.py 2025-08-31 23:07:27 +03:00
Egor b524decfa7 Update referral.py 2025-08-31 23:04:00 +03:00
Egor 5ee01f7735 Update referral.py 2025-08-31 23:00:20 +03:00
Egor f46f2d6523 Update start.py 2025-08-31 22:45:54 +03:00
Egor 435dbf6cee Update auth.py 2025-08-31 22:42:02 +03:00
Egor 94d38eeb71 Update auth.py 2025-08-31 22:41:31 +03:00
Egor 0e7f6f90ed Update auth.py 2025-08-31 22:40:41 +03:00
Egor f3789287dc Update start.py 2025-08-31 22:40:04 +03:00
Egor a8312d7c10 Update promocode.py 2025-08-31 22:10:21 +03:00
Egor 612f4a7978 Update promocodes.py 2025-08-31 22:07:04 +03:00
Egor 2462347b5f Update promocode.py 2025-08-31 22:06:15 +03:00
Egor 3ec3c0d76b Update promocodes.py 2025-08-31 22:05:16 +03:00
Egor 8224aa98f4 Update promocodes.py 2025-08-31 22:02:00 +03:00
Egor bb9d9422fb Update promocode.py 2025-08-31 22:01:16 +03:00
Egor f3a184ff00 Update promocodes.py 2025-08-31 21:53:25 +03:00
Egor 866e4586df Update promocode.py 2025-08-31 21:52:18 +03:00
Egor a30647893d Update promocodes.py 2025-08-31 21:39:14 +03:00
Egor 73826312e5 Update promocodes.py 2025-08-31 19:08:16 +03:00
Egor dae34dc0ef Update promocodes.py 2025-08-31 18:59:39 +03:00
Egor 17d0c8c26f Update promocodes.py 2025-08-31 18:50:55 +03:00
Egor 67831dc162 Update promocodes.py 2025-08-31 18:45:16 +03:00
Egor ae84fbe141 Update promocodes.py 2025-08-31 18:36:54 +03:00
Egor 8c9bd890e8 Update promocodes.py 2025-08-31 18:35:57 +03:00
Egor e8c1956de5 Update promocodes.py 2025-08-31 18:28:06 +03:00
Egor 1540a41d3a Update .env.example 2025-08-31 17:40:08 +03:00
Egor 9cb61cb696 Update README.md 2025-08-31 17:39:52 +03:00
Egor 3511325285 Update auth.py 2025-08-31 16:18:42 +03:00
Egor 62507c5d28 Update auth.py 2025-08-31 16:10:28 +03:00
Egor 387df29a05 Update start.py 2025-08-31 15:40:31 +03:00
Egor 75dca7e1ad Update start.py 2025-08-31 15:32:25 +03:00
Egor 2d5e54d5ad Update README.md 2025-08-31 06:52:42 +03:00
Egor d62b2b0d6f Update README.md 2025-08-31 06:50:22 +03:00
Egor e273719ad1 Update README.md 2025-08-31 06:49:41 +03:00
Egor f6726f1c94 Update README.md 2025-08-31 06:15:32 +03:00
Egor 92ff4f661f Update README.md 2025-08-31 05:54:54 +03:00
Egor dfebc5e608 Update .gitignore 2025-08-31 05:52:00 +03:00
Egor 8e6121cd0b Update README.md 2025-08-31 05:48:08 +03:00
Egor 6c0d5714bd Update remnawave_service.py 2025-08-31 05:19:43 +03:00
Egor ed29697434 Update remnawave_service.py 2025-08-31 05:18:56 +03:00
Egor 07aebef82f Update remnawave_service.py 2025-08-31 05:16:17 +03:00
Egor 7ce390fd50 Update remnawave_service.py 2025-08-31 05:09:31 +03:00
Egor cebe392d1c Update remnawave_service.py 2025-08-31 04:55:19 +03:00
Egor fd160f585a Update remnawave_service.py 2025-08-31 04:50:31 +03:00
Egor 5c0516fbc0 Update remnawave_service.py 2025-08-31 04:45:25 +03:00
Egor ba075326c1 Update admin.py 2025-08-31 04:39:08 +03:00
Egor c32425341e Update remnawave.py 2025-08-31 04:38:20 +03:00
Egor cc8df1eff3 Update remnawave_service.py 2025-08-31 04:33:56 +03:00
Egor 49cda0791c Update .env.example 2025-08-31 04:25:10 +03:00
Egor 60ea363a62 Update README.md 2025-08-31 04:24:37 +03:00
Egor f66619b225 Update subscription.py 2025-08-31 04:19:18 +03:00
Egor 5999402e1a Update inline.py 2025-08-31 04:17:18 +03:00
Egor 3aebccc090 Update inline.py 2025-08-31 04:16:37 +03:00
Egor ed577dd51a Update config.py 2025-08-31 04:15:45 +03:00
Egor e517487596 Update main.py 2025-08-31 04:03:37 +03:00
Egor f9441909a4 Update balance.py 2025-08-31 02:42:33 +03:00
Egor f13a015bfa Update payment_service.py 2025-08-31 00:47:07 +03:00
Egor 18bf946464 Update yookassa_webhook.py 2025-08-31 00:39:04 +03:00
Egor e4c46b2488 Update yookassa_webhook.py 2025-08-31 00:33:37 +03:00
Egor e0539b116c Update yookassa_webhook.py 2025-08-31 00:28:04 +03:00
Egor acdf7af1d9 Update yookassa_webhook.py 2025-08-31 00:22:18 +03:00
Egor d5b7828079 Update yookassa_webhook.py 2025-08-31 00:12:53 +03:00
Egor 03be071edc Update webhook_server.py 2025-08-30 23:54:27 +03:00
Egor e8742f30a6 Update webhook_server.py 2025-08-30 23:51:25 +03:00
Egor 3d7727dbf2 Update webhook_server.py 2025-08-30 23:46:31 +03:00
Egor 5bff67860f Update tribute.py 2025-08-30 23:45:48 +03:00
Egor 429e967da2 Update tribute.py 2025-08-30 23:44:35 +03:00
Egor c3df3beeae Update tribute.py 2025-08-30 23:42:55 +03:00
Egor 4d664c15d7 Update yookassa_webhook.py 2025-08-30 23:37:58 +03:00
Egor 53be365e38 Update tribute.py 2025-08-30 23:36:58 +03:00
Egor 3c3ffed32e Update yookassa_webhook.py 2025-08-30 23:36:12 +03:00
Egor 015726b3a6 Update webhook_server.py 2025-08-30 23:35:20 +03:00
Egor 4a3267712b Update main.py 2025-08-30 23:34:38 +03:00
Egor 9b45aae410 Update main.py 2025-08-30 23:24:43 +03:00
Egor f43b391d0e Update referral_service.py 2025-08-30 23:22:13 +03:00
Egor b09b7a0c84 Add files via upload 2025-08-30 23:21:04 +03:00
Egor 541b44b933 Update payment_service.py 2025-08-30 19:32:55 +03:00
Egor b21fce0eff Update webhook_server.py 2025-08-30 19:23:58 +03:00
Egor 017aa27c0f Update webhook_server.py 2025-08-30 19:17:31 +03:00
Egor 496978e8a2 Update texts.py 2025-08-30 19:11:45 +03:00
Egor b3711a590d Update user.py 2025-08-30 19:08:50 +03:00
Egor 631a0f2c4c Update payment_service.py 2025-08-30 19:05:55 +03:00
Egor 503f7ca455 Update transaction.py 2025-08-30 19:01:34 +03:00
Egor 80c771b7bc Update texts.py 2025-08-30 18:59:08 +03:00
Egor 6e344d350b Update texts.py 2025-08-30 18:58:30 +03:00
Egor 7738bc83af Update user.py 2025-08-30 18:57:32 +03:00
Egor 6f7a1eecf6 Update transaction.py 2025-08-30 18:56:07 +03:00
Egor bf147a736a Update main.py 2025-08-30 18:51:43 +03:00
Egor ea3decf1ac Update payment_service.py 2025-08-30 18:49:30 +03:00
Egor b1c0315a6e Update Dockerfile 2025-08-30 18:46:10 +03:00
Egor f6fa7e8f41 Update main.py 2025-08-30 18:45:44 +03:00
Egor 33d4d7994c Update tribute.py 2025-08-30 18:44:45 +03:00
Egor 0ce4ce2b08 Update webhook_server.py 2025-08-30 18:43:32 +03:00
Egor 3a9163e919 Update maintenance_service.py 2025-08-30 17:58:52 +03:00
Egor 376cff748b Update subscription_service.py 2025-08-30 16:57:35 +03:00
Egor d5cef080da Update referral_service.py 2025-08-30 16:51:34 +03:00
Egor 33e31213b3 Update balance.py 2025-08-30 16:05:42 +03:00
Egor 8077ba0c35 Update maintenance_service.py 2025-08-30 15:50:01 +03:00
Egor 67ad3847f8 Update maintenance_service.py 2025-08-30 15:48:12 +03:00
Egor 1eda042b34 Update main.py 2025-08-30 15:40:32 +03:00
Egor 961ff46f43 Update maintenance_service.py 2025-08-30 15:39:43 +03:00
40 changed files with 4406 additions and 2194 deletions
+135 -80
View File
@@ -1,107 +1,120 @@
# TELEGRAM BOT CONFIGURATION
# ===============================================
# 🤖 REMNAWAVE BEDOLAGA BOT CONFIGURATION
# ===============================================
# ===== TELEGRAM BOT =====
BOT_TOKEN=
ADMIN_IDS=
SUPPORT_USERNAME=
SUPPORT_USERNAME=@support
# DATABASE CONFIGURATION
DATABASE_URL=sqlite+aiosqlite:///./bot.db
REDIS_URL=redis://localhost:6379/0
# ===== DATABASE =====
# Для Docker используйте PostgreSQL:
DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
# Для локального запуска без Docker используйте SQLite: sqlite+aiosqlite:///./bot.db
# DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
# REMNAWAVE API CONFIGURATION
REMNAWAVE_API_URL=
REDIS_URL=redis://redis:6379/0
# Пароли для Docker (PostgreSQL/Redis)
POSTGRES_DB=remnawave_bot
POSTGRES_USER=remnawave_user
POSTGRES_PASSWORD=secure_password_123
# ===== REMNAWAVE API =====
REMNAWAVE_API_URL=https://panel.example.com
REMNAWAVE_API_KEY=
# Для панелей установленных скриптом eGames прописывать ключ в формате XXXXXXX:DDDDDDDD - https://panel.example.com/auth/login?XXXXXXX=DDDDDDDD
REMNAWAVE_SECRET_KEY=your_secret_key_here
# === NEW: Traffic Selection Mode Settings ===
# ========= ПОДПИСКИ =========
# ===== ТРИАЛ ПОДПИСКА =====
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_LIMIT_GB=10
TRIAL_DEVICE_LIMIT=1
TRIAL_SQUAD_UUID=
# ===== ПЛАТНАЯ ПОДПИСКА =====
# Сколько устройств доступно по дефолту при покупке платной подписки
DEFAULT_DEVICE_LIMIT=3
# Максимум устройств достопных к покупке (0 = Нет лимита)
MAX_DEVICES_LIMIT=15
# Дефолт параметры для подписок выданных через админку
DEFAULT_TRAFFIC_LIMIT_GB=100
# ===== ГЛОБАЛЬНЫЙ ПАРАМЕТР ДЛЯ ВСЕХ ПОДПИСОК =====
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
# ===== НАСТРОЙКИ ТРАФИКА =====
# Режим выбора трафика:
# "selectable" - пользователи выбирают пакеты трафика (по умолчанию)
# "fixed" - фиксированный лимит трафика для всех подписок, доступно 5/10/25/50/100/250/0 (0 безлимит) гб
# "fixed" - фиксированный лимит трафика для всех подписок
TRAFFIC_SELECTION_MODE=selectable
# Фиксированный лимит трафика в ГБ (используется только в режиме "fixed")
# 0 = безлимит
# для "fixed" обязательно должы быть проставлены цены на пакеты 5/10/25/50/100/250/0 можно постать 0 руб - будет беслпатно
FIXED_TRAFFIC_LIMIT_GB=0
FIXED_TRAFFIC_LIMIT_GB=100
# TRIAL SUBSCRIPTION SETTINGS
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_LIMIT_GB=10
TRIAL_DEVICE_LIMIT=2
TRIAL_SQUAD_UUID=
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
# ===== ПЕРИОДЫ ПОДПИСКИ =====
# Доступные периоды подписки (через запятую)
# Возможные значения: 14,30,60,90,180,360
AVAILABLE_SUBSCRIPTION_PERIODS=30,90,180
AVAILABLE_RENEWAL_PERIODS=30,90,180
# SUBSCRIPTION PRICING (в копейках для точности)
BASE_SUBSCRIPTION_PRICE=50000
# ===== ЦЕНЫ (в копейках) =====
BASE_SUBSCRIPTION_PRICE=0
PRICE_14_DAYS=5000
PRICE_30_DAYS=9900
PRICE_60_DAYS=18900
PRICE_90_DAYS=26900
PRICE_180_DAYS=49900
PRICE_360_DAYS=89900
# Цены за периоды
PRICE_14_DAYS=7000
PRICE_30_DAYS=9900
PRICE_60_DAYS=25900
PRICE_90_DAYS=36900
PRICE_180_DAYS=69900
PRICE_360_DAYS=109900
PRICE_TRAFFIC_5GB=2000
PRICE_TRAFFIC_10GB=4000
PRICE_TRAFFIC_25GB=6000
PRICE_TRAFFIC_50GB=10000
PRICE_TRAFFIC_100GB=15000
PRICE_TRAFFIC_250GB=20000
PRICE_TRAFFIC_UNLIMITED=25000
# Выводимые пакеты трафика и их цены в копейках
TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,250:17000:false,500:19000:false,1000:19500:true,0:20000:true"
# Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!)
PRICE_PER_DEVICE=5000
# REFERRAL SYSTEM SETTINGS
REFERRAL_REGISTRATION_REWARD=5000
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
REFERRAL_REGISTRATION_REWARD=10000
REFERRED_USER_REWARD=10000
REFERRAL_COMMISSION_PERCENT=25
# Режим работы кнопки "Подключиться"
# guide - открывает гайд подключения (режим 1)
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
CONNECT_BUTTON_MODE=miniapp_subscription
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
# MINIAPP_CUSTOM_URL=
# AUTO-PAYMENT SETTINGS
# ===== АВТОПРОДЛЕНИЕ =====
AUTOPAY_WARNING_DAYS=3,1
DEFAULT_AUTOPAY_DAYS_BEFORE=3
MIN_BALANCE_FOR_AUTOPAY_KOPEKS=10000
# MONITORING SETTINGS
MONITORING_INTERVAL=60
INACTIVE_USER_DELETE_MONTHS=3
# ===== ПЛАТЕЖНЫЕ СИСТЕМЫ =====
TRIAL_WARNING_HOURS=2
ENABLE_NOTIFICATIONS=true
NOTIFICATION_RETRY_ATTEMPTS=3
MONITORING_LOGS_RETENTION_DAYS=30
# PAYMENT SYSTEMS
# Telegram Stars (работает автоматически)
TELEGRAM_STARS_ENABLED=true
TELEGRAM_STARS_RATE_RUB=1.3
# Tribute (https://tribute.app)
TRIBUTE_ENABLED=false
TRIBUTE_API_KEY=
TRIBUTE_WEBHOOK_SECRET=your_webhook_secret
TRIBUTE_DONATE_LINK=https://t.me/tribute/app?startapp=XXXX
TRIBUTE_DONATE_LINK=
TRIBUTE_WEBHOOK_PATH=/tribute-webhook
TRIBUTE_WEBHOOK_PORT=8081
# === НОВЫЕ НАСТРОЙКИ YOOKASSA ===
# Включение/выключение YooKassa
# YooKassa (https://yookassa.ru)
YOOKASSA_ENABLED=false
# Основные настройки YooKassa (получить в личном кабинете)
YOOKASSA_SHOP_ID=your_shop_id_here
YOOKASSA_SECRET_KEY=your_secret_key_here
# URL для возврата после оплаты (необязательно, по умолчанию t.me/your_bot)
YOOKASSA_RETURN_URL=https://yourdomain.com/payment-success
# Email по умолчанию для чеков (если пользователь не указал свой)
YOOKASSA_SHOP_ID=
YOOKASSA_SECRET_KEY=
YOOKASSA_RETURN_URL=
YOOKASSA_DEFAULT_RECEIPT_EMAIL=receipts@yourdomain.com
# Настройки чеков для налоговой
YOOKASSA_VAT_CODE=1
# Коды НДС:
# 1 - НДС не облагается
# 2 - НДС 0%
# 2 - НДС 0%
# 3 - НДС 10%
# 4 - НДС 20%
# 5 - НДС 10/110
@@ -110,7 +123,7 @@ YOOKASSA_VAT_CODE=1
YOOKASSA_PAYMENT_MODE=full_payment
# Способы расчета:
# full_payment - полная оплата
# partial_payment - частичная оплата
# partial_payment - частичная оплата
# advance - аванс
# full_prepayment - полная предоплата
# partial_prepayment - частичная предоплата
@@ -120,7 +133,7 @@ YOOKASSA_PAYMENT_MODE=full_payment
YOOKASSA_PAYMENT_SUBJECT=service
# Предметы расчета:
# commodity - товар
# excise - подакцизный товар
# excise - подакцизный товар
# job - работа
# service - услуга
# gambling_bet - ставка в азартной игре
@@ -133,21 +146,63 @@ YOOKASSA_PAYMENT_SUBJECT=service
# composite - составной предмет расчета
# another - другое
# Webhook для получения уведомлений от YooKassa
# Webhook настройки
YOOKASSA_WEBHOOK_PATH=/yookassa-webhook
YOOKASSA_WEBHOOK_PORT=8082
YOOKASSA_WEBHOOK_SECRET=ваш_секретный_ключ_для_webhook
YOOKASSA_WEBHOOK_SECRET=your_webhook_secret
WEBHOOK_URL=https://example.com
# ===== НАСТРОЙКИ ОПИСАНИЙ ПЛАТЕЖЕЙ =====
# Эти настройки позволяют изменить описания платежей,
# чтобы избежать блокировок платежных систем
PAYMENT_SERVICE_NAME=Интернет-сервис
PAYMENT_BALANCE_DESCRIPTION=Пополнение баланса
PAYMENT_SUBSCRIPTION_DESCRIPTION=Оплата подписки
PAYMENT_BALANCE_TEMPLATE={service_name} - {description}
PAYMENT_SUBSCRIPTION_TEMPLATE={service_name} - {description}
# ===== ИНТЕРФЕЙС И UX =====
# Режим работы кнопки "Подключиться"
# guide - открывает гайд подключения (режим 1)
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
CONNECT_BUTTON_MODE=guide
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
MINIAPP_CUSTOM_URL=
# ===== МОНИТОРИНГ И УВЕДОМЛЕНИЯ =====
MONITORING_INTERVAL=60
INACTIVE_USER_DELETE_MONTHS=3
# Уведомления
TRIAL_WARNING_HOURS=2
ENABLE_NOTIFICATIONS=true
NOTIFICATION_RETRY_ATTEMPTS=3
MONITORING_LOGS_RETENTION_DAYS=30
NOTIFICATION_CACHE_HOURS=24
# ===== РЕЖИМ ТЕХНИЧЕСКИХ РАБОТ =====
MAINTENANCE_MODE=false
MAINTENANCE_CHECK_INTERVAL=30
MAINTENANCE_AUTO_ENABLE=true
MAINTENANCE_MESSAGE=Ведутся технические работы. Сервис временно недоступен. Попробуйте позже.
# ===== ЛОКАЛИЗАЦИЯ =====
DEFAULT_LANGUAGE=ru
AVAILABLE_LANGUAGES=ru,en
# ===== ЛОГИРОВАНИЕ =====
LOG_LEVEL=INFO
LOG_FILE=logs/bot.log
# ===== РАЗРАБОТКА =====
DEBUG=false
WEBHOOK_URL=
WEBHOOK_PATH=/webhook
# LOCALIZATION
DEFAULT_LANGUAGE=ru
AVAILABLE_LANGUAGES=ru
# LOGGING
LOG_LEVEL=INFO
LOG_FILE=/tmp/bot.log
# DEVELOPMENT
DEBUG=false
# ===== ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ =====
# Конфигурация приложений для гайда подключения
APP_CONFIG_PATH=app-config.json
ENABLE_DEEP_LINKS=true
APP_CONFIG_CACHE_TTL=3600
+3
View File
@@ -4,12 +4,14 @@ on:
branches:
- main
- master
- dev
tags:
- 'v*'
pull_request:
branches:
- main
- master
- dev
env:
REGISTRY: ghcr.io
IMAGE_NAME: fr1ngg/remnawave-bedolaga-telegram-bot
@@ -42,6 +44,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
+30 -2
View File
@@ -1,2 +1,30 @@
/screens
/assets
# Игнорируем все файлы и папки по умолчанию
*
# Исключения: разрешаем только нужные файлы
!.env.example
!Dockerfile
!app-config.json
!main.py
!requirements.txt
# Разрешаем папку app/ и все её содержимое рекурсивно
!app/
!app/**
# Дополнительно разрешаем README и лицензию (опционально)
!README.md
!LICENSE
# Разрешаем .gitignore чтобы он попал в репозиторий
!.gitignore
# Внутри разрешенных папок игнорируем служебные файлы
app/__pycache__/
app/**/__pycache__/
app/**/*.pyc
app/**/*.pyo
app/**/*.pyd
*.pyc
*.pyo
*.pyd
+19 -9
View File
@@ -1,33 +1,43 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
wget \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Create non-root user with specific UID/GID for consistency
RUN groupadd -g 1000 app && \
useradd -u 1000 -g 1000 -m -s /bin/bash app
# Copy application code
COPY . .
# Create logs directory
RUN mkdir -p logs
# Create necessary directories and set permissions
RUN mkdir -p logs data && \
chown -R app:app /app && \
chmod -R 755 /app && \
chmod -R 777 logs
# Switch to non-root user
USER app
# Set environment variables
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
# Create non-root user
RUN useradd --create-home --shell /bin/bash app
RUN chown -R app:app /app
USER app
# Expose webhook ports
EXPOSE 8081
EXPOSE 8082
# Expose port (if needed for webhooks)
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
# Run the application
CMD ["python", "main.py"]
+421 -996
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -25,6 +25,7 @@ from app.handlers.admin import (
statistics as admin_statistics, servers as admin_servers,
maintenance as admin_maintenance
)
from app.handlers.stars_payments import register_stars_handlers
logger = logging.getLogger(__name__)
@@ -52,6 +53,9 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
)
maintenance_service.set_bot(bot)
logger.info("Бот установлен в maintenance_service")
try:
redis_client = redis.from_url(settings.REDIS_URL)
await redis_client.ping()
@@ -67,6 +71,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
dp.callback_query.middleware(LoggingMiddleware())
dp.message.middleware(AuthMiddleware())
dp.callback_query.middleware(AuthMiddleware())
dp.pre_checkout_query.middleware(AuthMiddleware())
dp.message.middleware(MaintenanceMiddleware())
dp.callback_query.middleware(MaintenanceMiddleware())
dp.message.middleware(ThrottlingMiddleware())
@@ -95,6 +100,9 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
common.register_handlers(dp)
register_stars_handlers(dp)
logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей")
try:
await maintenance_service.start_monitoring()
logger.info("Мониторинг техработ запущен")
@@ -107,7 +115,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
async def shutdown_bot():
"""Корректное завершение работы бота"""
try:
await maintenance_service.stop_monitoring()
logger.info("Мониторинг техработ остановлен")
+222 -18
View File
@@ -1,5 +1,5 @@
import os
from typing import List, Optional, Union
from typing import List, Optional, Union, Dict
from pydantic_settings import BaseSettings
from pydantic import field_validator, Field
from pathlib import Path
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
REMNAWAVE_API_URL: str
REMNAWAVE_API_KEY: str
REMNAWAVE_SECRET_KEY: Optional[str] = None
TRIAL_DURATION_DAYS: int = 3
TRIAL_TRAFFIC_LIMIT_GB: int = 10
@@ -24,6 +25,7 @@ class Settings(BaseSettings):
DEFAULT_DEVICE_LIMIT: int = 1
TRIAL_SQUAD_UUID: str
DEFAULT_TRAFFIC_RESET_STRATEGY: str = "MONTH"
MAX_DEVICES_LIMIT: int = 20
TRIAL_WARNING_HOURS: int = 2
ENABLE_NOTIFICATIONS: bool = True
@@ -33,7 +35,9 @@ class Settings(BaseSettings):
NOTIFICATION_CACHE_HOURS: int = 24
BASE_SUBSCRIPTION_PRICE: int = 50000
AVAILABLE_SUBSCRIPTION_PERIODS: str = "14,30,60,90,180,360"
AVAILABLE_RENEWAL_PERIODS: str = "30,90,180"
PRICE_14_DAYS: int = 50000
PRICE_30_DAYS: int = 99000
PRICE_60_DAYS: int = 189000
@@ -41,13 +45,17 @@ class Settings(BaseSettings):
PRICE_180_DAYS: int = 499000
PRICE_360_DAYS: int = 899000
PRICE_TRAFFIC_5GB: int = 10000
PRICE_TRAFFIC_10GB: int = 19000
PRICE_TRAFFIC_25GB: int = 45000
PRICE_TRAFFIC_50GB: int = 85000
PRICE_TRAFFIC_100GB: int = 159000
PRICE_TRAFFIC_250GB: int = 369000
PRICE_TRAFFIC_UNLIMITED: int = 0
PRICE_TRAFFIC_5GB: int = 2000
PRICE_TRAFFIC_10GB: int = 3500
PRICE_TRAFFIC_25GB: int = 7000
PRICE_TRAFFIC_50GB: int = 11000
PRICE_TRAFFIC_100GB: int = 15000
PRICE_TRAFFIC_250GB: int = 17000
PRICE_TRAFFIC_500GB: int = 19000
PRICE_TRAFFIC_1000GB: int = 19500
PRICE_TRAFFIC_UNLIMITED: int = 20000
TRAFFIC_PACKAGES_CONFIG: str = ""
PRICE_PER_DEVICE: int = 5000
@@ -72,6 +80,7 @@ class Settings(BaseSettings):
MAINTENANCE_MESSAGE: str = "🔧 Ведутся технические работы. Сервис временно недоступен. Попробуйте позже."
TELEGRAM_STARS_ENABLED: bool = True
TELEGRAM_STARS_RATE_RUB: float = 1.3
TRIBUTE_ENABLED: bool = False
TRIBUTE_API_KEY: Optional[str] = None
@@ -91,6 +100,12 @@ class Settings(BaseSettings):
YOOKASSA_WEBHOOK_PATH: str = "/yookassa-webhook"
YOOKASSA_WEBHOOK_PORT: int = 8082
YOOKASSA_WEBHOOK_SECRET: Optional[str] = None
PAYMENT_BALANCE_DESCRIPTION: str = "Пополнение баланса"
PAYMENT_SUBSCRIPTION_DESCRIPTION: str = "Оплата подписки"
PAYMENT_SERVICE_NAME: str = "Интернет-сервис"
PAYMENT_BALANCE_TEMPLATE: str = "{service_name} - {description}"
PAYMENT_SUBSCRIPTION_TEMPLATE: str = "{service_name} - {description}"
CONNECT_BUTTON_MODE: str = "guide"
MINIAPP_CUSTOM_URL: str = ""
@@ -216,6 +231,193 @@ class Settings(BaseSettings):
def is_maintenance_auto_enable(self) -> bool:
return self.MAINTENANCE_AUTO_ENABLE
def get_available_subscription_periods(self) -> List[int]:
try:
periods_str = self.AVAILABLE_SUBSCRIPTION_PERIODS
if not periods_str.strip():
return [30, 90, 180]
periods = []
for period_str in periods_str.split(','):
period_str = period_str.strip()
if period_str:
period = int(period_str)
if period in PERIOD_PRICES:
periods.append(period)
return periods if periods else [30, 90, 180]
except (ValueError, AttributeError):
return [30, 90, 180]
def get_available_renewal_periods(self) -> List[int]:
try:
periods_str = self.AVAILABLE_RENEWAL_PERIODS
if not periods_str.strip():
return [30, 90, 180]
periods = []
for period_str in periods_str.split(','):
period_str = period_str.strip()
if period_str:
period = int(period_str)
if period in PERIOD_PRICES:
periods.append(period)
return periods if periods else [30, 90, 180]
except (ValueError, AttributeError):
return [30, 90, 180]
def get_balance_payment_description(self, amount_kopeks: int) -> str:
return self.PAYMENT_BALANCE_TEMPLATE.format(
service_name=self.PAYMENT_SERVICE_NAME,
description=f"{self.PAYMENT_BALANCE_DESCRIPTION} на {self.format_price(amount_kopeks)}"
)
def get_subscription_payment_description(self, period_days: int, amount_kopeks: int) -> str:
return self.PAYMENT_SUBSCRIPTION_TEMPLATE.format(
service_name=self.PAYMENT_SERVICE_NAME,
description=f"{self.PAYMENT_SUBSCRIPTION_DESCRIPTION} на {period_days} дней"
)
def get_custom_payment_description(self, description: str) -> str:
return self.PAYMENT_BALANCE_TEMPLATE.format(
service_name=self.PAYMENT_SERVICE_NAME,
description=description
)
def get_stars_rate(self) -> float:
return self.TELEGRAM_STARS_RATE_RUB
def stars_to_rubles(self, stars: int) -> float:
return stars * self.get_stars_rate()
def rubles_to_stars(self, rubles: float) -> int:
return max(1, int(rubles / self.get_stars_rate()))
def get_traffic_packages(self) -> List[Dict]:
import logging
logger = logging.getLogger(__name__)
try:
packages = []
config_str = self.TRAFFIC_PACKAGES_CONFIG.strip()
logger.info(f"CONFIG STRING: '{config_str}'")
if not config_str:
logger.info("CONFIG EMPTY, USING FALLBACK")
return self._get_fallback_traffic_packages()
logger.info("PARSING CONFIG...")
for package_config in config_str.split(','):
package_config = package_config.strip()
if not package_config:
continue
parts = package_config.split(':')
if len(parts) != 3:
continue
try:
gb = int(parts[0])
price = int(parts[1])
enabled = parts[2].lower() == 'true'
packages.append({
"gb": gb,
"price": price,
"enabled": enabled
})
except ValueError:
continue
logger.info(f"PARSED {len(packages)} packages from config")
return packages if packages else self._get_fallback_traffic_packages()
except Exception as e:
logger.info(f"ERROR PARSING CONFIG: {e}")
return self._get_fallback_traffic_packages()
def _get_fallback_traffic_packages(self) -> List[Dict]:
try:
if self.TRAFFIC_PACKAGES_CONFIG.strip():
packages = []
for package_config in self.TRAFFIC_PACKAGES_CONFIG.split(','):
package_config = package_config.strip()
if not package_config:
continue
parts = package_config.split(':')
if len(parts) != 3:
continue
try:
gb = int(parts[0])
price = int(parts[1])
enabled = parts[2].lower() == 'true'
packages.append({
"gb": gb,
"price": price,
"enabled": enabled
})
except ValueError:
continue
if packages:
return packages
except Exception as e:
pass
return [
{"gb": 5, "price": self.PRICE_TRAFFIC_5GB, "enabled": True},
{"gb": 10, "price": self.PRICE_TRAFFIC_10GB, "enabled": True},
{"gb": 25, "price": self.PRICE_TRAFFIC_25GB, "enabled": True},
{"gb": 50, "price": self.PRICE_TRAFFIC_50GB, "enabled": True},
{"gb": 100, "price": self.PRICE_TRAFFIC_100GB, "enabled": True},
{"gb": 250, "price": self.PRICE_TRAFFIC_250GB, "enabled": True},
{"gb": 500, "price": self.PRICE_TRAFFIC_500GB, "enabled": True},
{"gb": 1000, "price": self.PRICE_TRAFFIC_1000GB, "enabled": True},
{"gb": 0, "price": self.PRICE_TRAFFIC_UNLIMITED, "enabled": True},
]
def get_traffic_price(self, gb: int) -> int:
packages = self.get_traffic_packages()
for package in packages:
if package["gb"] == gb and package["enabled"]:
return package["price"]
enabled_packages = [pkg for pkg in packages if pkg["enabled"]]
if not enabled_packages:
return 0
unlimited_package = next((pkg for pkg in enabled_packages if pkg["gb"] == 0), None)
finite_packages = [pkg for pkg in enabled_packages if pkg["gb"] > 0]
if finite_packages:
max_package = max(finite_packages, key=lambda x: x["gb"])
if gb > max_package["gb"]:
if unlimited_package:
return unlimited_package["price"]
else:
return max_package["price"]
suitable_packages = [pkg for pkg in finite_packages if pkg["gb"] >= gb]
if suitable_packages:
nearest_package = min(suitable_packages, key=lambda x: x["gb"])
return nearest_package["price"]
if unlimited_package:
return unlimited_package["price"]
return 0
model_config = {
"env_file": ".env",
@@ -235,12 +437,14 @@ PERIOD_PRICES = {
360: settings.PRICE_360_DAYS,
}
TRAFFIC_PRICES = {
5: settings.PRICE_TRAFFIC_5GB,
10: settings.PRICE_TRAFFIC_10GB,
25: settings.PRICE_TRAFFIC_25GB,
50: settings.PRICE_TRAFFIC_50GB,
100: settings.PRICE_TRAFFIC_100GB,
250: settings.PRICE_TRAFFIC_250GB,
0: settings.PRICE_TRAFFIC_UNLIMITED,
}
def get_traffic_prices() -> Dict[int, int]:
packages = settings.get_traffic_packages()
return {package["gb"]: package["price"] for package in packages}
TRAFFIC_PRICES = get_traffic_prices()
def refresh_traffic_prices():
global TRAFFIC_PRICES
TRAFFIC_PRICES = get_traffic_prices()
refresh_traffic_prices()
+19 -5
View File
@@ -5,7 +5,7 @@ from sqlalchemy import select, and_, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database.models import PromoCode, PromoCodeUse, PromoCodeType
from app.database.models import PromoCode, PromoCodeUse, PromoCodeType, User
logger = logging.getLogger(__name__)
@@ -187,8 +187,13 @@ async def update_promocode(
async def delete_promocode(db: AsyncSession, promocode: PromoCode) -> bool:
try:
from sqlalchemy import delete as sql_delete
await db.execute(
sql_delete(PromoCodeUse).where(PromoCodeUse.promocode_id == promocode.id)
)
await db.delete(promocode)
await db.commit()
@@ -222,15 +227,24 @@ async def get_promocode_statistics(db: AsyncSession, promocode_id: int) -> dict:
today_uses = today_uses_result.scalar()
recent_uses_result = await db.execute(
select(PromoCodeUse)
select(PromoCodeUse, User)
.join(User, PromoCodeUse.user_id == User.id)
.where(PromoCodeUse.promocode_id == promocode_id)
.order_by(PromoCodeUse.used_at.desc())
.limit(10)
)
recent_uses = recent_uses_result.scalars().all()
recent_uses_data = recent_uses_result.all()
recent_uses = []
for use, user in recent_uses_data:
use.user_username = user.username
use.user_full_name = user.full_name
use.user_telegram_id = user.telegram_id
recent_uses.append(use)
return {
"total_uses": total_uses,
"today_uses": today_uses,
"recent_uses": recent_uses
}
}
+1 -1
View File
@@ -512,7 +512,7 @@ async def get_subscription_renewal_cost(
if subscription.traffic_limit_gb > 0:
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_cost = base_price + servers_cost + traffic_cost + devices_cost
+34 -11
View File
@@ -8,6 +8,7 @@ import aiohttp
import logging
from dataclasses import dataclass
from enum import Enum
from urllib.parse import urlparse, urljoin
logger = logging.getLogger(__name__)
@@ -33,7 +34,7 @@ class RemnaWaveUser:
username: str
status: UserStatus
used_traffic_bytes: int
lifetime_used_traffic_bytes: int # Новое поле
lifetime_used_traffic_bytes: int
traffic_limit_bytes: int
traffic_limit_strategy: TrafficLimitStrategy
expire_at: datetime
@@ -92,10 +93,12 @@ class RemnaWaveAPIError(Exception):
class RemnaWaveAPI:
def __init__(self, base_url: str, api_key: str):
def __init__(self, base_url: str, api_key: str, secret_key: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.secret_key = secret_key
self.session: Optional[aiohttp.ClientSession] = None
self.authenticated = False
def _detect_connection_type(self) -> str:
parsed = urlparse(self.base_url)
@@ -124,18 +127,29 @@ class RemnaWaveAPI:
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Forwarded-Proto': 'https',
'X-Forwarded-For': '127.0.0.1',
'X-Real-IP': '127.0.0.1'
}
cookies = None
if self.secret_key:
if ':' in self.secret_key:
key_name, key_value = self.secret_key.split(':', 1)
cookies = {key_name: key_value}
logger.debug(f"🍪 Используем куки: {key_name}=***")
else:
cookies = {self.secret_key: self.secret_key}
logger.debug(f"🍪 Используем куки: {self.secret_key}=***")
connector_kwargs = {}
if conn_type == "local":
logger.debug("🏠 Использую локальные заголовки proxy")
headers.update({
'X-Forwarded-For': '127.0.0.1',
'X-Forwarded-Proto': 'https',
'X-Forwarded-Host': 'localhost',
'X-Real-IP': '127.0.0.1',
'Host': 'localhost'
})
@@ -152,11 +166,18 @@ class RemnaWaveAPI:
connector = aiohttp.TCPConnector(**connector_kwargs)
self.session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
headers=headers,
connector=connector
)
session_kwargs = {
'timeout': aiohttp.ClientTimeout(total=30),
'headers': headers,
'connector': connector
}
if cookies:
session_kwargs['cookies'] = cookies
self.session = aiohttp.ClientSession(**session_kwargs)
self.authenticated = True
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
@@ -194,6 +215,8 @@ class RemnaWaveAPI:
if response.status >= 400:
error_message = response_data.get('message', f'HTTP {response.status}')
logger.error(f"API Error {response.status}: {error_message}")
logger.error(f"Response: {response_text[:500]}")
raise RemnaWaveAPIError(
error_message,
response.status,
+22 -10
View File
@@ -13,6 +13,14 @@ class TelegramStarsService:
def __init__(self, bot: Bot):
self.bot = bot
@staticmethod
def calculate_stars_from_rubles(rubles: float) -> int:
return settings.rubles_to_stars(rubles)
@staticmethod
def calculate_rubles_from_stars(stars: int) -> float:
return settings.stars_to_rubles(stars)
async def create_invoice(
self,
chat_id: int,
@@ -23,7 +31,8 @@ class TelegramStarsService:
start_parameter: Optional[str] = None
) -> Optional[str]:
try:
stars_amount = max(1, amount_kopeks // 100)
amount_rubles = amount_kopeks / 100
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
invoice_link = await self.bot.create_invoice_link(
title=title,
@@ -35,7 +44,10 @@ class TelegramStarsService:
start_parameter=start_parameter
)
logger.info(f"Создан Stars invoice на {stars_amount} звезд для {chat_id}")
logger.info(
f"Создан Stars invoice на {stars_amount} звезд (~{amount_rubles:.2f}₽) "
f"для {chat_id}, курс: {settings.get_stars_rate():.2f}₽/⭐"
)
return invoice_link
except Exception as e:
@@ -52,7 +64,8 @@ class TelegramStarsService:
keyboard: Optional[InlineKeyboardMarkup] = None
) -> Optional[Dict[str, Any]]:
try:
stars_amount = max(1, amount_kopeks // 100)
amount_rubles = amount_kopeks / 100
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
message = await self.bot.send_invoice(
chat_id=chat_id,
@@ -65,10 +78,14 @@ class TelegramStarsService:
reply_markup=keyboard
)
logger.info(f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд")
logger.info(
f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд "
f"(~{amount_rubles:.2f}₽), курс: {settings.get_stars_rate():.2f}₽/⭐"
)
return {
"message_id": message.message_id,
"stars_amount": stars_amount,
"rubles_amount": amount_rubles,
"payload": payload
}
@@ -88,13 +105,8 @@ class TelegramStarsService:
ok=ok,
error_message=error_message
)
logger.info(f"Ответ на pre_checkout_query: ok={ok}")
return True
except Exception as e:
logger.error(f"Ошибка ответа на pre_checkout_query: {e}")
return False
def calculate_stars_amount(self, rubles: float) -> int:
return max(1, int(rubles))
def calculate_rubles_from_stars(self, stars: int) -> float:
return float(stars)
+75 -21
View File
@@ -28,7 +28,6 @@ class TributeService:
return None
try:
payment_url = f"{self.donate_link}&user_id={user_id}"
logger.info(f"Создана ссылка Tribute для пользователя {user_id}")
@@ -41,7 +40,7 @@ class TributeService:
def verify_webhook_signature(self, payload: str, signature: str) -> bool:
if not self.webhook_secret:
logger.warning("Webhook secret не настроен")
logger.warning("Webhook secret не настроен, пропускаем проверку")
return True
try:
@@ -51,31 +50,54 @@ class TributeService:
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
is_valid = hmac.compare_digest(signature, expected_signature)
if is_valid:
logger.info("✅ Подпись Tribute webhook проверена успешно")
else:
logger.error("❌ Неверная подпись Tribute webhook")
return is_valid
except Exception as e:
logger.error(f"Ошибка проверки подписи webhook: {e}")
return False
async def process_webhook(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
async def process_webhook(self, payload_or_data) -> Optional[Dict[str, Any]]:
try:
logger.info(f"🔄 Начинаем обработку Tribute webhook")
payment_id = payload.get("id") or payload.get("payment_id")
status = payload.get("status")
amount_kopeks = payload.get("amount", 0)
telegram_user_id = payload.get("telegram_user_id") or payload.get("user_id")
if isinstance(payload_or_data, str):
try:
webhook_data = json.loads(payload_or_data)
logger.info(f"📊 Распарсенные данные: {webhook_data}")
except json.JSONDecodeError as e:
logger.error(f"❌ Ошибка парсинга JSON: {e}")
return None
else:
webhook_data = payload_or_data
if not payment_id and "payload" in payload:
data = payload["payload"]
payment_id = None
status = None
amount_kopeks = 0
telegram_user_id = None
payment_id = webhook_data.get("id") or webhook_data.get("payment_id")
status = webhook_data.get("status")
amount_kopeks = webhook_data.get("amount", 0)
telegram_user_id = webhook_data.get("telegram_user_id") or webhook_data.get("user_id")
if not payment_id and "payload" in webhook_data:
data = webhook_data["payload"]
payment_id = data.get("id") or data.get("payment_id")
status = data.get("status")
amount_kopeks = data.get("amount", 0)
telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
if not payment_id and "name" in payload:
event_name = payload.get("name")
data = payload.get("payload", {})
if not payment_id and "name" in webhook_data:
event_name = webhook_data.get("name")
data = webhook_data.get("payload", {})
payment_id = str(data.get("donation_request_id"))
amount_kopeks = data.get("amount", 0)
telegram_user_id = data.get("telegram_user_id")
@@ -87,22 +109,54 @@ class TributeService:
else:
status = "unknown"
logger.info(f"Обработка Tribute webhook: payment_id={payment_id}, status={status}, amount_kopeks={amount_kopeks}, user_id={telegram_user_id}")
logger.info(f"📝 Извлеченные данные: payment_id={payment_id}, status={status}, amount_kopeks={amount_kopeks}, user_id={telegram_user_id}")
if not telegram_user_id:
logger.error("Не найден telegram_user_id в webhook данных")
logger.error("Не найден telegram_user_id в webhook данных")
logger.error(f"🔍 Полные данные для отладки: {json.dumps(webhook_data, ensure_ascii=False, indent=2)}")
return None
return {
try:
telegram_user_id = int(telegram_user_id)
except (ValueError, TypeError):
logger.error(f"❌ Некорректный telegram_user_id: {telegram_user_id}")
return None
result = {
"event_type": "payment",
"payment_id": payment_id or f"tribute_{telegram_user_id}_{amount_kopeks}",
"user_id": int(telegram_user_id),
"amount_kopeks": amount_kopeks,
"user_id": telegram_user_id,
"amount_kopeks": int(amount_kopeks) if amount_kopeks else 0,
"status": status or "paid",
"external_id": f"donation_{payment_id}"
"external_id": f"donation_{payment_id or 'unknown'}",
"payment_system": "tribute"
}
logger.info(f"✅ Tribute webhook обработан успешно: {result}")
return result
except Exception as e:
logger.error(f"Ошибка обработки Tribute webhook: {e}")
logger.error(f"Webhook payload: {json.dumps(payload, ensure_ascii=False)}")
logger.error(f"Ошибка обработки Tribute webhook: {e}", exc_info=True)
logger.error(f"🔍 Webhook data для отладки: {json.dumps(webhook_data, ensure_ascii=False, indent=2)}")
return None
async def get_payment_status(self, payment_id: str) -> Optional[Dict[str, Any]]:
try:
logger.info(f"Запрос статуса платежа {payment_id}")
return {"status": "unknown", "payment_id": payment_id}
except Exception as e:
logger.error(f"Ошибка получения статуса платежа: {e}")
return None
async def refund_payment(
self,
payment_id: str,
amount_kopeks: Optional[int] = None,
reason: str = "Возврат по запросу"
) -> Optional[Dict[str, Any]]:
try:
logger.info(f"Создание возврата для платежа {payment_id}")
return {"refund_id": f"refund_{payment_id}", "status": "pending"}
except Exception as e:
logger.error(f"Ошибка создания возврата: {e}")
return None
+69 -13
View File
@@ -1,4 +1,5 @@
import logging
import json
from typing import Optional
from aiohttp import web
@@ -26,9 +27,11 @@ class WebhookServer:
self.app.router.add_post(settings.TRIBUTE_WEBHOOK_PATH, self._tribute_webhook_handler)
self.app.router.add_get('/health', self._health_check)
self.app.router.add_options(settings.TRIBUTE_WEBHOOK_PATH, self._options_handler)
logger.info(f"Webhook сервер настроен:")
logger.info(f" - Tribute webhook: {settings.TRIBUTE_WEBHOOK_PATH}")
logger.info(f" - Health check: /health")
logger.info(f" - Tribute webhook: POST {settings.TRIBUTE_WEBHOOK_PATH}")
logger.info(f" - Health check: GET /health")
return self.app
@@ -49,11 +52,11 @@ class WebhookServer:
await self.site.start()
logger.info(f"Webhook сервер запущен на порту {settings.TRIBUTE_WEBHOOK_PORT}")
logger.info(f"🎯 Tribute webhook URL: http://your-server:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
logger.info(f"Tribute webhook сервер запущен на порту {settings.TRIBUTE_WEBHOOK_PORT}")
logger.info(f"🎯 Tribute webhook URL: http://0.0.0.0:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
except Exception as e:
logger.error(f"❌ Ошибка запуска webhook сервера: {e}")
logger.error(f"❌ Ошибка запуска Tribute webhook сервера: {e}")
raise
async def stop(self):
@@ -61,31 +64,82 @@ class WebhookServer:
try:
if self.site:
await self.site.stop()
logger.info("Webhook сайт остановлен")
logger.info("Tribute webhook сайт остановлен")
if self.runner:
await self.runner.cleanup()
logger.info("Webhook runner очищен")
logger.info("Tribute webhook runner очищен")
except Exception as e:
logger.error(f"Ошибка остановки webhook сервера: {e}")
logger.error(f"Ошибка остановки Tribute webhook сервера: {e}")
async def _options_handler(self, request: web.Request) -> web.Response:
return web.Response(
status=200,
headers={
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-Tribute-Signature',
}
)
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
try:
logger.info(f"📥 Получен Tribute webhook: {request.method} {request.path}")
logger.info(f"📋 Headers: {dict(request.headers)}")
raw_body = await request.read()
if not raw_body:
logger.warning("⚠️ Получен пустой webhook от Tribute")
return web.json_response(
{"status": "error", "reason": "empty_body"},
status=400
)
payload = raw_body.decode('utf-8')
logger.info(f"📄 Payload: {payload}")
try:
webhook_data = json.loads(payload)
logger.info(f"📊 Распарсенные данные: {webhook_data}")
except json.JSONDecodeError as e:
logger.error(f"❌ Ошибка парсинга JSON: {e}")
return web.json_response(
{"status": "error", "reason": "invalid_json"},
status=400
)
signature = request.headers.get('X-Tribute-Signature')
logger.info(f"🔐 Signature: {signature}")
if signature and settings.TRIBUTE_WEBHOOK_SECRET:
from app.external.tribute import TributeService as TributeAPI
tribute_api = TributeAPI()
if not tribute_api.verify_webhook_signature(payload, signature):
logger.error("❌ Неверная подпись Tribute webhook")
return web.json_response(
{"status": "error", "reason": "invalid_signature"},
status=401
)
result = await self.tribute_service.process_webhook(payload, signature)
return web.json_response(result, status=200)
if result:
logger.info(f"✅ Tribute webhook обработан успешно: {result}")
return web.json_response({"status": "ok", "result": result}, status=200)
else:
logger.error("❌ Ошибка обработки Tribute webhook")
return web.json_response(
{"status": "error", "reason": "processing_failed"},
status=400
)
except Exception as e:
logger.error(f"Ошибка обработки Tribute webhook: {e}")
logger.error(f"❌ Критическая ошибка обработки Tribute webhook: {e}", exc_info=True)
return web.json_response(
{"status": "error", "reason": "internal_error"},
{"status": "error", "reason": "internal_error", "message": str(e)},
status=500
)
@@ -94,5 +148,7 @@ class WebhookServer:
return web.json_response({
"status": "ok",
"service": "tribute-webhooks",
"tribute_enabled": settings.TRIBUTE_ENABLED
})
"tribute_enabled": settings.TRIBUTE_ENABLED,
"port": settings.TRIBUTE_WEBHOOK_PORT,
"path": settings.TRIBUTE_WEBHOOK_PATH
})
+150 -59
View File
@@ -1,7 +1,9 @@
import asyncio
import logging
import json
import hashlib
import hmac
import base64
from typing import Optional, Dict, Any
from aiohttp import web
from sqlalchemy.ext.asyncio import AsyncSession
@@ -17,13 +19,69 @@ class YooKassaWebhookHandler:
@staticmethod
def verify_webhook_signature(body: str, signature: str, secret: str) -> bool:
expected_signature = hmac.new(
secret.encode('utf-8'),
body.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
try:
signature_parts = signature.strip().split(' ')
if len(signature_parts) < 4:
logger.error(f"Неверный формат подписи YooKassa: {signature}")
return False
version = signature_parts[0]
payment_id = signature_parts[1]
timestamp = signature_parts[2]
received_signature = signature_parts[3]
if version != "v1":
logger.error(f"Неподдерживаемая версия подписи: {version}")
return False
logger.info(f"Проверка подписи v1 для платежа {payment_id}, timestamp: {timestamp}")
expected_signature_1 = hmac.new(
secret.encode('utf-8'),
body.encode('utf-8'),
hashlib.sha256
).digest()
expected_signature_1_b64 = base64.b64encode(expected_signature_1).decode('utf-8')
signed_payload_2 = f"{payment_id}.{timestamp}.{body}"
expected_signature_2 = hmac.new(
secret.encode('utf-8'),
signed_payload_2.encode('utf-8'),
hashlib.sha256
).digest()
expected_signature_2_b64 = base64.b64encode(expected_signature_2).decode('utf-8')
signed_payload_3 = f"{timestamp}.{body}"
expected_signature_3 = hmac.new(
secret.encode('utf-8'),
signed_payload_3.encode('utf-8'),
hashlib.sha256
).digest()
expected_signature_3_b64 = base64.b64encode(expected_signature_3).decode('utf-8')
logger.debug(f"Получена подпись: {received_signature}")
logger.debug(f"Ожидаемая подпись (вариант 1): {expected_signature_1_b64}")
logger.debug(f"Ожидаемая подпись (вариант 2): {expected_signature_2_b64}")
logger.debug(f"Ожидаемая подпись (вариант 3): {expected_signature_3_b64}")
is_valid = (
hmac.compare_digest(received_signature, expected_signature_1_b64) or
hmac.compare_digest(received_signature, expected_signature_2_b64) or
hmac.compare_digest(received_signature, expected_signature_3_b64)
)
if is_valid:
logger.info("✅ Подпись YooKassa webhook проверена успешно")
else:
logger.warning("⚠️ Подпись YooKassa webhook не совпадает ни с одним вариантом")
return is_valid
except Exception as e:
logger.error(f"Ошибка проверки подписи YooKassa: {e}")
return False
def __init__(self, payment_service: PaymentService):
self.payment_service = payment_service
@@ -31,91 +89,116 @@ class YooKassaWebhookHandler:
async def handle_webhook(self, request: web.Request) -> web.Response:
try:
logger.info(f"📥 Получен YooKassa webhook: {request.method} {request.path}")
logger.info(f"📋 Headers: {dict(request.headers)}")
body = await request.text()
if not body:
logger.warning("Получен пустой webhook от YooKassa")
logger.warning("⚠️ Получен пустой webhook от YooKassa")
return web.Response(status=400, text="Empty body")
if hasattr(settings, 'YOOKASSA_WEBHOOK_SECRET') and settings.YOOKASSA_WEBHOOK_SECRET:
signature = request.headers.get('X-YooKassa-Signature')
if not signature:
logger.warning("Webhook без подписи")
return web.Response(status=400, text="Missing signature")
logger.info(f"📄 Body: {body}")
signature = request.headers.get('Signature') or request.headers.get('X-YooKassa-Signature')
if settings.YOOKASSA_WEBHOOK_SECRET and signature:
logger.info(f"🔐 Получена подпись: {signature}")
if not YooKassaWebhookHandler.verify_webhook_signature(body, signature, settings.YOOKASSA_WEBHOOK_SECRET):
logger.error("Неверная подпись webhook")
return web.Response(status=400, text="Invalid signature")
logger.warning("❌ Подпись не совпала, но продолжаем обработку (режим отладки)")
else:
logger.info("✅ Подпись webhook проверена успешно")
elif settings.YOOKASSA_WEBHOOK_SECRET and not signature:
logger.warning("⚠️ Webhook без подписи, но секрет настроен")
elif signature and not settings.YOOKASSA_WEBHOOK_SECRET:
logger.info("ℹ️ Подпись получена, но проверка отключена (YOOKASSA_WEBHOOK_SECRET не настроен)")
else:
logger.info("ℹ️ Проверка подписи отключена")
try:
webhook_data = json.loads(body)
except json.JSONDecodeError as e:
logger.error(f"Ошибка парсинга JSON webhook YooKassa: {e}")
logger.error(f"Ошибка парсинга JSON webhook YooKassa: {e}")
return web.Response(status=400, text="Invalid JSON")
logger.info(f"Получен webhook YooKassa: {webhook_data.get('event', 'unknown_event')}")
logger.debug(f"Полные данные webhook: {webhook_data}")
logger.info(f"📊 Обработка webhook YooKassa: {webhook_data.get('event', 'unknown_event')}")
logger.debug(f"🔍 Полные данные webhook: {webhook_data}")
event_type = webhook_data.get("event")
if not event_type:
logger.warning("Webhook YooKassa без типа события")
logger.warning("⚠️ Webhook YooKassa без типа события")
return web.Response(status=400, text="No event type")
if event_type not in ["payment.succeeded", "payment.waiting_for_capture"]:
logger.info(f"Игнорируем событие YooKassa: {event_type}")
logger.info(f"Игнорируем событие YooKassa: {event_type}")
return web.Response(status=200, text="OK")
async with get_db() as db:
success = await self.payment_service.process_yookassa_webhook(db, webhook_data)
if success:
logger.info(f"Успешно обработан webhook YooKassa: {event_type}")
return web.Response(status=200, text="OK")
else:
logger.error(f"Ошибка обработки webhook YooKassa: {event_type}")
return web.Response(status=500, text="Processing error")
async for db in get_db():
try:
success = await self.payment_service.process_yookassa_webhook(db, webhook_data)
if success:
logger.info(f"✅ Успешно обработан webhook YooKassa: {event_type}")
return web.Response(status=200, text="OK")
else:
logger.error(f"❌ Ошибка обработки webhook YooKassa: {event_type}")
return web.Response(status=500, text="Processing error")
finally:
await db.close()
except Exception as e:
logger.error(f"Критическая ошибка обработки webhook YooKassa: {e}", exc_info=True)
logger.error(f"Критическая ошибка обработки webhook YooKassa: {e}", exc_info=True)
return web.Response(status=500, text="Internal server error")
def setup_routes(self, app: web.Application) -> None:
webhook_path = settings.YOOKASSA_WEBHOOK_PATH
app.router.add_post(webhook_path, self.handle_webhook)
app.router.add_get(webhook_path, self._get_handler)
app.router.add_options(webhook_path, self._options_handler)
logger.info(f"Настроен webhook YooKassa на пути: {webhook_path}")
logger.info(f"Настроен YooKassa webhook на пути: POST {webhook_path}")
async def _get_handler(self, request: web.Request) -> web.Response:
return web.json_response({
"status": "ok",
"message": "YooKassa webhook endpoint is working",
"method": "GET",
"path": request.path,
"note": "Use POST method for actual webhooks"
})
async def _options_handler(self, request: web.Request) -> web.Response:
return web.Response(
status=200,
headers={
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-YooKassa-Signature',
}
)
def create_yookassa_webhook_app(payment_service: PaymentService) -> web.Application:
app = web.Application()
async def logging_middleware(request, handler):
start_time = request.loop.time()
try:
response = await handler(request)
process_time = request.loop.time() - start_time
logger.info(f"YooKassa webhook {request.method} {request.path_qs} "
f"-> {response.status} ({process_time:.3f}s)")
return response
except Exception as e:
process_time = request.loop.time() - start_time
logger.error(f"YooKassa webhook {request.method} {request.path_qs} "
f"-> ERROR ({process_time:.3f}s): {e}")
raise
app.middlewares.append(logging_middleware)
webhook_handler = YooKassaWebhookHandler(payment_service)
webhook_handler.setup_routes(app)
async def health_check(request):
return web.json_response({"status": "ok", "service": "yookassa_webhook"})
return web.json_response({
"status": "ok",
"service": "yookassa_webhook",
"port": settings.YOOKASSA_WEBHOOK_PORT,
"path": settings.YOOKASSA_WEBHOOK_PATH,
"enabled": settings.is_yookassa_enabled()
})
app.router.add_get("/health", health_check)
@@ -125,12 +208,10 @@ def create_yookassa_webhook_app(payment_service: PaymentService) -> web.Applicat
async def start_yookassa_webhook_server(payment_service: PaymentService) -> None:
if not settings.is_yookassa_enabled():
logger.info("YooKassa отключен, webhook сервер не запускается")
logger.info("YooKassa отключена, webhook сервер не запускается")
return
try:
from aiohttp import web
app = create_yookassa_webhook_app(payment_service)
runner = web.AppRunner(app)
@@ -138,15 +219,25 @@ async def start_yookassa_webhook_server(payment_service: PaymentService) -> None
site = web.TCPSite(
runner,
host='0.0.0.0',
host='0.0.0.0',
port=settings.YOOKASSA_WEBHOOK_PORT
)
await site.start()
logger.info(f"YooKassa webhook сервер запущен на порту {settings.YOOKASSA_WEBHOOK_PORT}")
logger.info(f"Webhook URL: http://localhost:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
logger.info(f"YooKassa webhook сервер запущен на порту {settings.YOOKASSA_WEBHOOK_PORT}")
logger.info(f"🎯 YooKassa webhook URL: http://0.0.0.0:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
try:
while True:
await asyncio.sleep(1)
except asyncio.CancelledError:
logger.info("🛑 YooKassa webhook сервер получил сигнал остановки")
finally:
await site.stop()
await runner.cleanup()
logger.info("✅ YooKassa webhook сервер остановлен")
except Exception as e:
logger.error(f"Ошибка запуска YooKassa webhook сервера: {e}", exc_info=True)
logger.error(f"Ошибка запуска YooKassa webhook сервера: {e}", exc_info=True)
raise
+20 -11
View File
@@ -45,7 +45,8 @@ async def show_messages_menu(
await callback.message.edit_text(
text,
reply_markup=get_admin_messages_keyboard(db_user.language)
reply_markup=get_admin_messages_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
@@ -60,7 +61,8 @@ async def show_broadcast_targets(
await callback.message.edit_text(
"🎯 <b>Выбор целевой аудитории</b>\n\n"
"Выберите категорию пользователей для рассылки:",
reply_markup=get_broadcast_target_keyboard(db_user.language)
reply_markup=get_broadcast_target_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
@@ -118,7 +120,8 @@ async def show_messages_history(
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
parse_mode="HTML"
)
await callback.answer()
@@ -135,7 +138,7 @@ async def show_custom_broadcast(
stats = await get_users_statistics(db)
text = f"""
🔍 <b>Рассылка по критериям</b>
📝 <b>Рассылка по критериям</b>
📊 <b>Доступные фильтры:</b>
@@ -158,7 +161,8 @@ async def show_custom_broadcast(
await callback.message.edit_text(
text,
reply_markup=get_custom_criteria_keyboard(db_user.language)
reply_markup=get_custom_criteria_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
@@ -196,7 +200,8 @@ async def select_custom_criteria(
f"<i>Поддерживается HTML разметка</i>",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="admin_messages")]
])
]),
parse_mode="HTML"
)
await state.set_state(AdminStates.waiting_for_broadcast_message)
@@ -233,7 +238,8 @@ async def select_broadcast_target(
f"<i>Поддерживается HTML разметка</i>",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="admin_messages")]
])
]),
parse_mode="HTML"
)
await state.set_state(AdminStates.waiting_for_broadcast_message)
@@ -284,7 +290,8 @@ async def process_broadcast_message(
await message.answer(
preview_text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
parse_mode="HTML"
)
await state.set_state(AdminStates.confirming_broadcast)
@@ -304,7 +311,8 @@ async def confirm_broadcast(
await callback.message.edit_text(
"📨 Начинаю рассылку...\n\n"
"⏳ Это может занять несколько минут.",
reply_markup=None
reply_markup=None,
parse_mode="HTML"
)
if target.startswith('custom_'):
@@ -367,7 +375,8 @@ async def confirm_broadcast(
result_text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="📨 К рассылкам", callback_data="admin_messages")]
])
]),
parse_mode="HTML"
)
await state.clear()
@@ -548,4 +557,4 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_custom_broadcast, F.data == "admin_msg_custom")
dp.callback_query.register(select_custom_criteria, F.data.startswith("criteria_"))
dp.message.register(process_broadcast_message, AdminStates.waiting_for_broadcast_message)
dp.message.register(process_broadcast_message, AdminStates.waiting_for_broadcast_message)
+547 -110
View File
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.states import AdminStates
from app.database.models import User, PromoCodeType
from app.database.models import PromoCode, PromoCodeUse, PromoCodeType, User
from app.keyboards.admin import (
get_admin_promocodes_keyboard, get_promocode_type_keyboard,
get_admin_pagination_keyboard, get_confirmation_keyboard
@@ -77,6 +77,7 @@ async def show_promocodes_list(
return
text = f"🎫 <b>Список промокодов</b> (стр. {page}/{total_pages})\n\n"
keyboard = []
for promo in promocodes:
status_emoji = "" if promo.is_active else ""
@@ -93,9 +94,14 @@ async def show_promocodes_list(
if promo.valid_until:
text += f"⏰ До: {format_datetime(promo.valid_until)}\n"
text += f"🔧 Управление: /promo_{promo.id}\n\n"
keyboard = []
keyboard.append([
types.InlineKeyboardButton(
text=f"🎫 {promo.code}",
callback_data=f"promo_manage_{promo.id}"
)
])
text += "\n"
if total_pages > 1:
pagination_row = get_admin_pagination_keyboard(
@@ -182,128 +188,75 @@ async def show_promocode_management(
)
await callback.answer()
@admin_required
@error_handler
async def toggle_promocode_status(
async def show_promocode_edit_menu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
new_status = not promo.is_active
await update_promocode(db, promo, is_active=new_status)
status_text = "активирован" if new_status else "деактивирован"
await callback.answer(f"✅ Промокод {status_text}", show_alert=True)
await show_promocode_management(callback, db_user, db)
@admin_required
@error_handler
async def confirm_delete_promocode(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
text = f"""
<b>Подтверждение удаления</b>
<b>Редактирование промокода</b> <code>{promo.code}</code>
Вы действительно хотите удалить промокод <code>{promo.code}</code>?
<b>Внимание:</b> Это действие нельзя отменить!
💰 <b>Текущие параметры:</b>
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
if promo.type == PromoCodeType.BALANCE.value:
text += f"• Бонус: {settings.format_price(promo.balance_bonus_kopeks)}\n"
elif promo.type in [PromoCodeType.SUBSCRIPTION_DAYS.value, PromoCodeType.TRIAL_SUBSCRIPTION.value]:
text += f"• Дней: {promo.subscription_days}\n"
text += f"• Использований: {promo.current_uses}/{promo.max_uses}\n"
if promo.valid_until:
text += f"• До: {format_datetime(promo.valid_until)}\n"
else:
text += f"• Срок: бессрочно\n"
text += f"\nВыберите параметр для изменения:"
keyboard = [
[
types.InlineKeyboardButton(
text=" Да, удалить",
callback_data=f"promo_delete_confirm_{promo.id}"
),
text="📅 Дата окончания",
callback_data=f"promo_edit_date_{promo.id}"
)
],
[
types.InlineKeyboardButton(
text="❌ Отмена",
callback_data=f"promo_manage_{promo.id}"
text="📊 Количество использований",
callback_data=f"promo_edit_uses_{promo.id}"
)
]
])
]
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
@admin_required
@error_handler
async def delete_promocode_confirmed(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
if promo.type == PromoCodeType.BALANCE.value:
keyboard.insert(1, [
types.InlineKeyboardButton(
text="💰 Сумма бонуса",
callback_data=f"promo_edit_amount_{promo.id}"
)
])
elif promo.type in [PromoCodeType.SUBSCRIPTION_DAYS.value, PromoCodeType.TRIAL_SUBSCRIPTION.value]:
keyboard.insert(1, [
types.InlineKeyboardButton(
text="📅 Количество дней",
callback_data=f"promo_edit_days_{promo.id}"
)
])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
code = promo.code
success = await delete_promocode(db, promo)
if success:
await callback.answer(f"✅ Промокод {code} удален", show_alert=True)
await show_promocodes_list(callback, db_user, db)
else:
await callback.answer("❌ Ошибка удаления промокода", show_alert=True)
@admin_required
@error_handler
async def show_promocode_stats(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
stats = await get_promocode_statistics(db, promo_id)
text = f"""
📊 <b>Статистика промокода</b> <code>{promo.code}</code>
📈 <b>Общая статистика:</b>
- Всего использований: {stats['total_uses']}
- Использований сегодня: {stats['today_uses']}
- Осталось использований: {promo.max_uses - promo.current_uses}
📅 <b>Последние использования:</b>
"""
for use in stats['recent_uses'][:5]:
use_date = format_datetime(use.used_at)
text += f"- {use_date} (ID: {use.user_id})\n"
if not stats['recent_uses']:
text += "- Пока не было использований\n"
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
keyboard.extend([
[
types.InlineKeyboardButton(
text="⬅️ Назад",
@@ -312,7 +265,160 @@ async def show_promocode_stats(
]
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def start_edit_promocode_date(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
await state.update_data(
editing_promo_id=promo_id,
edit_action="date"
)
text = f"""
📅 <b>Изменение даты окончания промокода</b>
Введите количество дней до окончания (от текущего момента):
• Введите <b>0</b> для бессрочного промокода
• Введите положительное число для установки срока
<i>Например: 30 (промокод будет действовать 30 дней)</i>
ID промокода: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await state.set_state(AdminStates.setting_promocode_expiry)
await callback.answer()
@admin_required
@error_handler
async def start_edit_promocode_amount(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
await state.update_data(
editing_promo_id=promo_id,
edit_action="amount"
)
text = f"""
💰 <b>Изменение суммы бонуса промокода</b>
Введите новую сумму в рублях:
<i>Например: 500</i>
ID промокода: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await state.set_state(AdminStates.setting_promocode_value)
await callback.answer()
@admin_required
@error_handler
async def start_edit_promocode_days(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
# ИСПРАВЛЕНИЕ: берем последний элемент как ID
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
await state.update_data(
editing_promo_id=promo_id,
edit_action="days"
)
text = f"""
📅 <b>Изменение количества дней подписки</b>
Введите новое количество дней:
<i>Например: 30</i>
ID промокода: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await state.set_state(AdminStates.setting_promocode_value)
await callback.answer()
@admin_required
@error_handler
async def start_edit_promocode_uses(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
await state.update_data(
editing_promo_id=promo_id,
edit_action="uses"
)
text = f"""
📊 <b>Изменение максимального количества использований</b>
Введите новое количество использований:
• Введите <b>0</b> для безлимитных использований
• Введите положительное число для ограничения
<i>Например: 100</i>
ID промокода: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await state.set_state(AdminStates.setting_promocode_uses)
await callback.answer()
@@ -375,7 +481,6 @@ async def process_promocode_code(
await message.answer("❌ Код должен содержать только латинские буквы и цифры (3-20 символов)")
return
from app.database.crud.promocode import get_promocode_by_code
existing = await get_promocode_by_code(db, code)
if existing:
await message.answer("❌ Промокод с таким кодом уже существует")
@@ -411,12 +516,18 @@ async def process_promocode_code(
async def process_promocode_value(
message: types.Message,
db_user: User,
state: FSMContext
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
if data.get('editing_promo_id'):
await handle_edit_value(message, db_user, state, db)
return
try:
value = int(message.text.strip())
data = await state.get_data()
promo_type = data.get('promocode_type')
if promo_type == "balance" and (value < 1 or value > 10000):
@@ -437,13 +548,72 @@ async def process_promocode_value(
await message.answer("❌ Введите корректное число")
async def handle_edit_value(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
promo_id = data.get('editing_promo_id')
edit_action = data.get('edit_action')
promo = await db.get(PromoCode, promo_id)
if not promo:
await message.answer("❌ Промокод не найден")
await state.clear()
return
try:
value = int(message.text.strip())
if edit_action == "amount":
if value < 1 or value > 10000:
await message.answer("❌ Сумма должна быть от 1 до 10,000 рублей")
return
await update_promocode(db, promo, balance_bonus_kopeks=value * 100)
await message.answer(
f"✅ Сумма бонуса изменена на {value}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
])
)
elif edit_action == "days":
if value < 1 or value > 3650:
await message.answer("❌ Количество дней должно быть от 1 до 3650")
return
await update_promocode(db, promo, subscription_days=value)
await message.answer(
f"✅ Количество дней изменено на {value}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
])
)
await state.clear()
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: {edit_action} = {value}")
except ValueError:
await message.answer("❌ Введите корректное число")
@admin_required
@error_handler
async def process_promocode_uses(
message: types.Message,
db_user: User,
state: FSMContext
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
if data.get('editing_promo_id'):
await handle_edit_uses(message, db_user, state, db)
return
try:
max_uses = int(message.text.strip())
@@ -465,6 +635,54 @@ async def process_promocode_uses(
await message.answer("❌ Введите корректное число")
async def handle_edit_uses(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
promo_id = data.get('editing_promo_id')
promo = await db.get(PromoCode, promo_id)
if not promo:
await message.answer("❌ Промокод не найден")
await state.clear()
return
try:
max_uses = int(message.text.strip())
if max_uses < 0 or max_uses > 100000:
await message.answer("❌ Количество использований должно быть от 0 до 100,000")
return
if max_uses == 0:
max_uses = 999999
if max_uses < promo.current_uses:
await message.answer(
f"❌ Новый лимит ({max_uses}) не может быть меньше текущих использований ({promo.current_uses})"
)
return
await update_promocode(db, promo, max_uses=max_uses)
uses_text = "безлимитное" if max_uses == 999999 else str(max_uses)
await message.answer(
f"✅ Максимальное количество использований изменено на {uses_text}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
])
)
await state.clear()
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: max_uses = {max_uses}")
except ValueError:
await message.answer("❌ Введите корректное число")
@admin_required
@error_handler
async def process_promocode_expiry(
@@ -473,6 +691,12 @@ async def process_promocode_expiry(
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
if data.get('editing_promo_id'):
await handle_edit_expiry(message, db_user, state, db)
return
try:
expiry_days = int(message.text.strip())
@@ -480,7 +704,6 @@ async def process_promocode_expiry(
await message.answer("❌ Срок действия должен быть от 0 до 3650 дней")
return
data = await state.get_data()
code = data.get('promocode_code')
promo_type = data.get('promocode_type')
value = data.get('promocode_value', 0)
@@ -544,6 +767,209 @@ async def process_promocode_expiry(
await message.answer("❌ Введите корректное число дней")
async def handle_edit_expiry(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
promo_id = data.get('editing_promo_id')
promo = await db.get(PromoCode, promo_id)
if not promo:
await message.answer("❌ Промокод не найден")
await state.clear()
return
try:
expiry_days = int(message.text.strip())
if expiry_days < 0 or expiry_days > 3650:
await message.answer("❌ Срок действия должен быть от 0 до 3650 дней")
return
valid_until = None
if expiry_days > 0:
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
await update_promocode(db, promo, valid_until=valid_until)
if valid_until:
expiry_text = f"до {format_datetime(valid_until)}"
else:
expiry_text = "бессрочно"
await message.answer(
f"✅ Срок действия промокода изменен: {expiry_text}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
])
)
await state.clear()
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: expiry = {expiry_days} дней")
except ValueError:
await message.answer("❌ Введите корректное число дней")
@admin_required
@error_handler
async def toggle_promocode_status(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
new_status = not promo.is_active
await update_promocode(db, promo, is_active=new_status)
status_text = "активирован" if new_status else "деактивирован"
await callback.answer(f"✅ Промокод {status_text}", show_alert=True)
await show_promocode_management(callback, db_user, db)
@admin_required
@error_handler
async def confirm_delete_promocode(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
text = f"""
⚠️ <b>Подтверждение удаления</b>
Вы действительно хотите удалить промокод <code>{promo.code}</code>?
📊 <b>Информация о промокоде:</b>
• Использований: {promo.current_uses}/{promo.max_uses}
• Статус: {'Активен' if promo.is_active else 'Неактивен'}
<b>⚠️ Внимание:</b> Это действие нельзя отменить!
ID: {promo_id}
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"promo_delete_confirm_{promo.id}"
),
types.InlineKeyboardButton(
text="❌ Отмена",
callback_data=f"promo_manage_{promo.id}"
)
]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
@admin_required
@error_handler
async def delete_promocode_confirmed(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
try:
promo_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
return
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
code = promo.code
success = await delete_promocode(db, promo)
if success:
await callback.answer(f"✅ Промокод {code} удален", show_alert=True)
await show_promocodes_list(callback, db_user, db)
else:
await callback.answer("❌ Ошибка удаления промокода", show_alert=True)
@admin_required
@error_handler
async def show_promocode_stats(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
promo_id = int(callback.data.split('_')[-1])
promo = await db.get(PromoCode, promo_id)
if not promo:
await callback.answer("❌ Промокод не найден", show_alert=True)
return
stats = await get_promocode_statistics(db, promo_id)
text = f"""
📊 <b>Статистика промокода</b> <code>{promo.code}</code>
📈 <b>Общая статистика:</b>
- Всего использований: {stats['total_uses']}
- Использований сегодня: {stats['today_uses']}
- Осталось использований: {promo.max_uses - promo.current_uses}
📅 <b>Последние использования:</b>
"""
if stats['recent_uses']:
for use in stats['recent_uses'][:5]:
use_date = format_datetime(use.used_at)
if hasattr(use, 'user_username') and use.user_username:
user_display = f"@{use.user_username}"
elif hasattr(use, 'user_full_name') and use.user_full_name:
user_display = use.user_full_name
elif hasattr(use, 'user_telegram_id'):
user_display = f"ID{use.user_telegram_id}"
else:
user_display = f"ID{use.user_id}"
text += f"- {use_date} | {user_display}\n"
else:
text += "- Пока не было использований\n"
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[
types.InlineKeyboardButton(
text="⬅️ Назад",
callback_data=f"promo_manage_{promo.id}"
)
]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_promocodes_menu, F.data == "admin_promocodes")
dp.callback_query.register(show_promocodes_list, F.data == "admin_promo_list")
@@ -552,11 +978,22 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_promocode_management, F.data.startswith("promo_manage_"))
dp.callback_query.register(toggle_promocode_status, F.data.startswith("promo_toggle_"))
dp.callback_query.register(confirm_delete_promocode, F.data.startswith("promo_delete_"))
dp.callback_query.register(delete_promocode_confirmed, F.data.startswith("promo_delete_confirm_"))
dp.callback_query.register(show_promocode_stats, F.data.startswith("promo_stats_"))
dp.callback_query.register(start_edit_promocode_date, F.data.startswith("promo_edit_date_"))
dp.callback_query.register(start_edit_promocode_amount, F.data.startswith("promo_edit_amount_"))
dp.callback_query.register(start_edit_promocode_days, F.data.startswith("promo_edit_days_"))
dp.callback_query.register(start_edit_promocode_uses, F.data.startswith("promo_edit_uses_"))
dp.callback_query.register(
show_promocode_edit_menu,
F.data.regexp(r"^promo_edit_\d+$")
)
dp.callback_query.register(delete_promocode_confirmed, F.data.startswith("promo_delete_confirm_"))
dp.callback_query.register(confirm_delete_promocode, F.data.startswith("promo_delete_"))
dp.message.register(process_promocode_code, AdminStates.creating_promocode)
dp.message.register(process_promocode_value, AdminStates.setting_promocode_value)
dp.message.register(process_promocode_uses, AdminStates.setting_promocode_uses)
dp.message.register(process_promocode_expiry, AdminStates.setting_promocode_expiry)
dp.message.register(process_promocode_expiry, AdminStates.setting_promocode_expiry)
+106 -56
View File
@@ -1322,53 +1322,22 @@ async def show_sync_options(
text = """
🔄 <b>Синхронизация с RemnaWave</b>
Выберите тип синхронизации:
🔄 <b>Синхронизировать всех</b>
Полная синхронизация всех пользователей
• Создание новых пользователей из панели
• Обновление данных существующих
• Удаление неактуальных подписок
🔄 <b>Полная синхронизация выполняет:</b>
• Создание новых пользователей из панели в боте
• Обновление данных существующих пользователей
Деактивация подписок пользователей, отсутствующих в панели
• Сохранение балансов пользователей
• ⏱️ Время выполнения: 2-5 минут
🆕 <b>Только новых</b>
• Создание пользователей из панели, которых нет в боте
• Быстрое добавление при массовой регистрации
• ⏱️ Время выполнения: 30 секунд - 2 минуты
📈 <b>Обновить данные</b>
• Обновление информации о трафике и подписках
• Синхронизация статуса и лимитов
• Обновление подключенных сквадов
• ⏱️ Время выполнения: 1-3 минуты
🔍 <b>Валидация подписок</b>
• Проверка и исправление проблем в данных
• Восстановление отсутствующих полей
• Исправление некорректных статусов
🧹 <b>Мягкая очистка</b>
• Деактивация подписок отсутствующих в панели
• Сохранение транзакций и истории
🗑️ <b>ПРИНУДИТЕЛЬНАЯ ОЧИСТКА</b>
• ⚠️ ОПАСНО: Полное удаление данных пользователей
• Удаление транзакций, балансов, рефералов
• Только при серьезных проблемах синхронизации
⚠️ <b>Важно:</b>
• Во время синхронизации не выполняйте другие операции
• При полной синхронизации подписки пользователей, отсутствующих в панели, будут деактивированы
• Рекомендуется делать полную синхронизацию ежедневно
• Баланс пользователей НЕ удаляется
"""
keyboard = [
[types.InlineKeyboardButton(text="🔄 Синхронизировать всех", callback_data="sync_all_users")],
[types.InlineKeyboardButton(text="🆕 Только новых", callback_data="sync_new_users")],
[types.InlineKeyboardButton(text="📈 Обновить данные", callback_data="sync_update_data")],
[types.InlineKeyboardButton(text="🔍 Валидация подписок", callback_data="sync_validate")],
[types.InlineKeyboardButton(text="🧹 Мягкая очистка", callback_data="sync_cleanup")],
[types.InlineKeyboardButton(text="🗑️ ПРИНУДИТЕЛЬНАЯ ОЧИСТКА", callback_data="confirm_force_cleanup")],
[types.InlineKeyboardButton(text="🔄 Запустить полную синхронизацию", callback_data="sync_all_users")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
]
@@ -1378,6 +1347,104 @@ async def show_sync_options(
)
await callback.answer()
@admin_required
@error_handler
async def sync_all_users(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
"""Выполняет полную синхронизацию всех пользователей"""
progress_text = """
🔄 <b>Выполняется полная синхронизация...</b>
📋 Этапы:
• Загрузка ВСЕХ пользователей из панели RemnaWave
• Создание новых пользователей в боте
• Обновление существующих пользователей
• Деактивация подписок отсутствующих пользователей
• Сохранение балансов
⏳ Пожалуйста, подождите...
"""
await callback.message.edit_text(progress_text, reply_markup=None)
remnawave_service = RemnaWaveService()
stats = await remnawave_service.sync_users_from_panel(db, "all")
total_operations = stats['created'] + stats['updated'] + stats.get('deleted', 0)
if stats['errors'] == 0:
status_emoji = ""
status_text = "успешно завершена"
elif stats['errors'] < total_operations:
status_emoji = "⚠️"
status_text = "завершена с предупреждениями"
else:
status_emoji = ""
status_text = "завершена с ошибками"
text = f"""
{status_emoji} <b>Полная синхронизация {status_text}</b>
📊 <b>Результат:</b>
• 🆕 Создано: {stats['created']}
• 🔄 Обновлено: {stats['updated']}
• 🗑️ Деактивировано: {stats.get('deleted', 0)}
• ❌ Ошибок: {stats['errors']}
"""
if stats.get('deleted', 0) > 0:
text += f"""
🗑️ <b>Деактивированные подписки:</b>
Деактивированы подписки пользователей, которые
отсутствуют в панели RemnaWave.
💰 Балансы пользователей сохранены.
"""
if stats['errors'] > 0:
text += f"""
⚠️ <b>Внимание:</b>
Некоторые операции завершились с ошибками.
Проверьте логи для получения подробной информации.
"""
text += f"""
💡 <b>Рекомендации:</b>
• Полная синхронизация выполнена
• Рекомендуется запускать раз в день
• Все пользователи из панели синхронизированы
"""
keyboard = []
if stats['errors'] > 0:
keyboard.append([
types.InlineKeyboardButton(
text="🔄 Повторить синхронизацию",
callback_data="sync_all_users"
)
])
keyboard.extend([
[
types.InlineKeyboardButton(text="📊 Статистика системы", callback_data="admin_rw_system"),
types.InlineKeyboardButton(text="🌐 Ноды", callback_data="admin_rw_nodes")
],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_sync_recommendations(
@@ -1844,39 +1911,22 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(manage_node, F.data.startswith("node_restart_"))
dp.callback_query.register(restart_all_nodes, F.data == "admin_restart_all_nodes")
dp.callback_query.register(show_sync_options, F.data == "admin_rw_sync")
dp.callback_query.register(sync_users, F.data.startswith("sync_"))
dp.callback_query.register(show_sync_recommendations, F.data == "sync_recommendations")
dp.callback_query.register(validate_subscriptions, F.data == "sync_validate")
dp.callback_query.register(cleanup_subscriptions, F.data == "sync_cleanup")
dp.callback_query.register(confirm_force_cleanup, F.data == "confirm_force_cleanup")
dp.callback_query.register(force_cleanup_all_orphaned_users, F.data == "force_cleanup_orphaned")
dp.callback_query.register(sync_all_users, F.data == "sync_all_users")
dp.callback_query.register(show_squads_management, F.data == "admin_rw_squads")
dp.callback_query.register(show_squad_details, F.data.startswith("admin_squad_manage_"))
dp.callback_query.register(manage_squad_action, F.data.startswith("squad_add_users_"))
dp.callback_query.register(manage_squad_action, F.data.startswith("squad_remove_users_"))
dp.callback_query.register(manage_squad_action, F.data.startswith("squad_delete_"))
dp.callback_query.register(show_squad_edit_menu, F.data.startswith("squad_edit_") & ~F.data.startswith("squad_edit_inbounds_"))
dp.callback_query.register(show_squad_inbounds_selection, F.data.startswith("squad_edit_inbounds_"))
dp.callback_query.register(show_squad_rename_form, F.data.startswith("squad_rename_"))
dp.callback_query.register(cancel_squad_rename, F.data.startswith("cancel_rename_"))
dp.callback_query.register(toggle_squad_inbound, F.data.startswith("sqd_tgl_"))
dp.callback_query.register(save_squad_inbounds, F.data.startswith("sqd_save_"))
dp.callback_query.register(show_squad_edit_menu_short, F.data.startswith("sqd_edit_"))
dp.callback_query.register(start_squad_creation, F.data == "admin_squad_create")
dp.callback_query.register(cancel_squad_creation, F.data == "cancel_squad_create")
dp.callback_query.register(toggle_create_inbound, F.data.startswith("create_tgl_"))
dp.callback_query.register(finish_squad_creation, F.data == "create_squad_finish")
dp.message.register(
+9 -10
View File
@@ -306,13 +306,13 @@ async def show_pricing_settings(
"""
keyboard = [
[
types.InlineKeyboardButton(text="📅 Периоды", callback_data="admin_edit_period_prices"),
types.InlineKeyboardButton(text="📈 Трафик", callback_data="admin_edit_traffic_prices")
],
[
types.InlineKeyboardButton(text="📱 Устройства", callback_data="admin_edit_device_price")
],
# [
# types.InlineKeyboardButton(text="📅 Периоды", callback_data="admin_edit_period_prices"),
# types.InlineKeyboardButton(text="📈 Трафик", callback_data="admin_edit_traffic_prices")
# ],
# [
# types.InlineKeyboardButton(text="📱 Устройства", callback_data="admin_edit_device_price")
# ],
[
types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")
]
@@ -405,8 +405,7 @@ async def show_countries_management(
keyboard = [
[
types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_countries"),
types.InlineKeyboardButton(text="⚙️ API настройки", callback_data="admin_rw_api")
types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_countries")
],
[
types.InlineKeyboardButton(text="📊 Статистика нод", callback_data="admin_rw_nodes"),
@@ -495,4 +494,4 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
handle_subscriptions_pagination,
F.data.startswith("admin_subs_list_page_")
)
)
+693
View File
@@ -16,6 +16,8 @@ from app.localization.texts import get_texts
from app.services.user_service import UserService
from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime, format_time_ago
from app.services.remnawave_service import RemnaWaveService
from app.database.crud.server_squad import get_all_server_squads, get_server_squad_by_uuid, get_server_squad_by_id
logger = logging.getLogger(__name__)
@@ -1300,6 +1302,642 @@ async def process_subscription_grant_text(
await state.clear()
@admin_required
@error_handler
async def show_user_servers_management(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
user_id = int(callback.data.split('_')[-1])
user_service = UserService()
profile = await user_service.get_user_profile(db, user_id)
if not profile:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
user = profile["user"]
subscription = profile["subscription"]
text = f"🌍 <b>Управление серверами пользователя</b>\n\n"
text += f"👤 {user.full_name} (ID: <code>{user.telegram_id}</code>)\n\n"
if subscription:
current_squads = subscription.connected_squads or []
if current_squads:
text += f"<b>Текущие серверы ({len(current_squads)}):</b>\n"
for squad_uuid in current_squads:
try:
server = await get_server_squad_by_uuid(db, squad_uuid)
if server:
text += f"{server.display_name}\n"
else:
text += f"{squad_uuid[:8]}... (неизвестный)\n"
except Exception as e:
logger.error(f"Ошибка получения сервера {squad_uuid}: {e}")
text += f"{squad_uuid[:8]}... (ошибка загрузки)\n"
else:
text += "<b>Серверы:</b> Не подключены\n"
text += f"\n<b>Устройства:</b> {subscription.device_limit}\n"
traffic_display = f"{subscription.traffic_used_gb:.1f}/"
if subscription.traffic_limit_gb == 0:
traffic_display += "∞ ГБ"
else:
traffic_display += f"{subscription.traffic_limit_gb} ГБ"
text += f"<b>Трафик:</b> {traffic_display}\n"
else:
text += "❌ <b>Подписка отсутствует</b>"
keyboard = [
[
types.InlineKeyboardButton(text="🌍 Сменить сервер", callback_data=f"admin_user_change_server_{user_id}"),
types.InlineKeyboardButton(text="📱 Устройства", callback_data=f"admin_user_devices_{user_id}")
],
[
types.InlineKeyboardButton(text="📊 Трафик", callback_data=f"admin_user_traffic_{user_id}"),
types.InlineKeyboardButton(text="🔄 Сбросить устройства", callback_data=f"admin_user_reset_devices_{user_id}")
],
[
types.InlineKeyboardButton(text="⬅️ К пользователю", callback_data=f"admin_user_manage_{user_id}")
]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_server_selection(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
user_id = int(callback.data.split('_')[-1])
await _show_servers_for_user(callback, user_id, db)
await callback.answer()
async def _show_servers_for_user(
callback: types.CallbackQuery,
user_id: int,
db: AsyncSession
):
try:
user = await get_user_by_id(db, user_id)
current_squads = []
if user and user.subscription:
current_squads = user.subscription.connected_squads or []
all_servers, _ = await get_all_server_squads(db, available_only=False)
servers_to_show = []
for server in all_servers:
if server.is_available or server.squad_uuid in current_squads:
servers_to_show.append(server)
if not servers_to_show:
await callback.message.edit_text(
"❌ Доступные серверы не найдены",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_user_servers_{user_id}")]
])
)
return
text = f"🌍 <b>Управление серверами</b>\n\n"
text += f"Нажмите на сервер чтобы добавить/убрать:\n"
text += f"✅ - выбранный сервер\n"
text += f"⚪ - доступный сервер\n"
text += f"🔒 - неактивный (только для уже назначенных)\n\n"
keyboard = []
selected_servers = [s for s in servers_to_show if s.squad_uuid in current_squads]
available_servers = [s for s in servers_to_show if s.squad_uuid not in current_squads and s.is_available]
inactive_servers = [s for s in servers_to_show if s.squad_uuid not in current_squads and not s.is_available]
sorted_servers = selected_servers + available_servers + inactive_servers
for server in sorted_servers[:20]:
is_selected = server.squad_uuid in current_squads
if is_selected:
emoji = ""
elif server.is_available:
emoji = ""
else:
emoji = "🔒"
display_name = server.display_name
if not server.is_available and not is_selected:
display_name += " (неактивный)"
keyboard.append([
types.InlineKeyboardButton(
text=f"{emoji} {display_name}",
callback_data=f"admin_user_toggle_server_{user_id}_{server.id}"
)
])
if len(servers_to_show) > 20:
text += f"\n📝 Показано первых 20 из {len(servers_to_show)} серверов"
keyboard.append([
types.InlineKeyboardButton(text="✅ Готово", callback_data=f"admin_user_servers_{user_id}"),
types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_user_servers_{user_id}")
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
except Exception as e:
logger.error(f"Ошибка показа серверов: {e}")
@admin_required
@error_handler
async def toggle_user_server(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
parts = callback.data.split('_')
user_id = int(parts[4])
server_id = int(parts[5])
try:
user = await get_user_by_id(db, user_id)
if not user or not user.subscription:
await callback.answer("❌ Пользователь или подписка не найдены", show_alert=True)
return
server = await get_server_squad_by_id(db, server_id)
if not server:
await callback.answer("❌ Сервер не найден", show_alert=True)
return
subscription = user.subscription
current_squads = list(subscription.connected_squads or [])
if server.squad_uuid in current_squads:
current_squads.remove(server.squad_uuid)
action_text = "удален"
else:
current_squads.append(server.squad_uuid)
action_text = "добавлен"
subscription.connected_squads = current_squads
subscription.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(subscription)
if user.remnawave_uuid:
try:
remnawave_service = RemnaWaveService()
async with remnawave_service.api as api:
await api.update_user(
uuid=user.remnawave_uuid,
active_internal_squads=current_squads
)
logger.info(f"✅ Обновлены серверы в RemnaWave для пользователя {user.telegram_id}")
except Exception as rw_error:
logger.error(f"❌ Ошибка обновления RemnaWave: {rw_error}")
logger.info(f"Админ {db_user.id}: сервер {server.display_name} {action_text} для пользователя {user_id}")
await refresh_server_selection_screen(callback, user_id, db_user, db)
except Exception as e:
logger.error(f"Ошибка переключения сервера: {e}")
await callback.answer("❌ Ошибка изменения сервера", show_alert=True)
async def refresh_server_selection_screen(
callback: types.CallbackQuery,
user_id: int,
db_user: User,
db: AsyncSession
):
try:
user = await get_user_by_id(db, user_id)
current_squads = []
if user and user.subscription:
current_squads = user.subscription.connected_squads or []
servers, _ = await get_all_server_squads(db, available_only=True)
if not servers:
await callback.message.edit_text(
"❌ Доступные серверы не найдены",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_user_servers_{user_id}")]
])
)
return
text = f"🌍 <b>Управление серверами</b>\n\n"
text += f"Нажмите на сервер чтобы добавить/убрать:\n\n"
keyboard = []
for server in servers[:15]:
is_selected = server.squad_uuid in current_squads
emoji = "" if is_selected else ""
keyboard.append([
types.InlineKeyboardButton(
text=f"{emoji} {server.display_name}",
callback_data=f"admin_user_toggle_server_{user_id}_{server.id}"
)
])
if len(servers) > 15:
text += f"\n📝 Показано первых 15 из {len(servers)} серверов"
keyboard.append([
types.InlineKeyboardButton(text="✅ Готово", callback_data=f"admin_user_servers_{user_id}"),
types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_user_servers_{user_id}")
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
except Exception as e:
logger.error(f"Ошибка обновления экрана серверов: {e}")
@admin_required
@error_handler
async def start_devices_edit(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
user_id = int(callback.data.split('_')[-1])
await state.update_data(editing_devices_user_id=user_id)
await callback.message.edit_text(
"📱 <b>Изменение количества устройств</b>\n\n"
"Введите новое количество устройств (от 1 до 10):\n"
"• Текущее значение будет заменено\n"
"• Примеры: 1, 2, 5, 10\n\n"
"Или нажмите /cancel для отмены",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[
types.InlineKeyboardButton(text="1", callback_data=f"admin_user_devices_set_{user_id}_1"),
types.InlineKeyboardButton(text="2", callback_data=f"admin_user_devices_set_{user_id}_2"),
types.InlineKeyboardButton(text="3", callback_data=f"admin_user_devices_set_{user_id}_3")
],
[
types.InlineKeyboardButton(text="5", callback_data=f"admin_user_devices_set_{user_id}_5"),
types.InlineKeyboardButton(text="10", callback_data=f"admin_user_devices_set_{user_id}_10")
],
[
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_servers_{user_id}")
]
])
)
await state.set_state(AdminStates.editing_user_devices)
await callback.answer()
@admin_required
@error_handler
async def set_user_devices_button(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
parts = callback.data.split('_')
user_id = int(parts[-2])
devices = int(parts[-1])
success = await _update_user_devices(db, user_id, devices, db_user.id)
if success:
await callback.message.edit_text(
f"✅ Количество устройств изменено на: {devices}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
])
)
else:
await callback.message.edit_text(
"❌ Ошибка изменения количества устройств",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
])
)
await callback.answer()
@admin_required
@error_handler
async def process_devices_edit_text(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
user_id = data.get("editing_devices_user_id")
if not user_id:
await message.answer("❌ Ошибка: пользователь не найден")
await state.clear()
return
try:
devices = int(message.text.strip())
if devices <= 0 or devices > 10:
await message.answer("❌ Количество устройств должно быть от 1 до 10")
return
success = await _update_user_devices(db, user_id, devices, db_user.id)
if success:
await message.answer(
f"✅ Количество устройств изменено на: {devices}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
])
)
else:
await message.answer("❌ Ошибка изменения количества устройств")
except ValueError:
await message.answer("❌ Введите корректное число устройств")
return
await state.clear()
@admin_required
@error_handler
async def start_traffic_edit(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
user_id = int(callback.data.split('_')[-1])
await state.update_data(editing_traffic_user_id=user_id)
await callback.message.edit_text(
"📊 <b>Изменение лимита трафика</b>\n\n"
"Введите новый лимит трафика в ГБ:\n"
"• 0 - безлимитный трафик\n"
"• Примеры: 50, 100, 500, 1000\n"
"• Максимум: 10000 ГБ\n\n"
"Или нажмите /cancel для отмены",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[
types.InlineKeyboardButton(text="50 ГБ", callback_data=f"admin_user_traffic_set_{user_id}_50"),
types.InlineKeyboardButton(text="100 ГБ", callback_data=f"admin_user_traffic_set_{user_id}_100")
],
[
types.InlineKeyboardButton(text="500 ГБ", callback_data=f"admin_user_traffic_set_{user_id}_500"),
types.InlineKeyboardButton(text="1000 ГБ", callback_data=f"admin_user_traffic_set_{user_id}_1000")
],
[
types.InlineKeyboardButton(text="♾️ Безлимит", callback_data=f"admin_user_traffic_set_{user_id}_0")
],
[
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_servers_{user_id}")
]
])
)
await state.set_state(AdminStates.editing_user_traffic)
await callback.answer()
@admin_required
@error_handler
async def set_user_traffic_button(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
parts = callback.data.split('_')
user_id = int(parts[-2])
traffic_gb = int(parts[-1])
success = await _update_user_traffic(db, user_id, traffic_gb, db_user.id)
if success:
traffic_text = "♾️ безлимитный" if traffic_gb == 0 else f"{traffic_gb} ГБ"
await callback.message.edit_text(
f"✅ Лимит трафика изменен на: {traffic_text}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
])
)
else:
await callback.message.edit_text(
"❌ Ошибка изменения лимита трафика",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
])
)
await callback.answer()
@admin_required
@error_handler
async def process_traffic_edit_text(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession
):
data = await state.get_data()
user_id = data.get("editing_traffic_user_id")
if not user_id:
await message.answer("❌ Ошибка: пользователь не найден")
await state.clear()
return
try:
traffic_gb = int(message.text.strip())
if traffic_gb < 0 or traffic_gb > 10000:
await message.answer("❌ Лимит трафика должен быть от 0 до 10000 ГБ (0 = безлимит)")
return
success = await _update_user_traffic(db, user_id, traffic_gb, db_user.id)
if success:
traffic_text = "♾️ безлимитный" if traffic_gb == 0 else f"{traffic_gb} ГБ"
await message.answer(
f"✅ Лимит трафика изменен на: {traffic_text}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
])
)
else:
await message.answer("❌ Ошибка изменения лимита трафика")
except ValueError:
await message.answer("❌ Введите корректное число ГБ")
return
await state.clear()
@admin_required
@error_handler
async def confirm_reset_devices(
callback: types.CallbackQuery,
db_user: User
):
user_id = int(callback.data.split('_')[-1])
await callback.message.edit_text(
"🔄 <b>Сброс устройств пользователя</b>\n\n"
"⚠️ <b>ВНИМАНИЕ!</b>\n"
"Вы уверены, что хотите сбросить все HWID устройства этого пользователя?\n\n"
"Это действие:\n"
"• Удалит все привязанные устройства\n"
"• Пользователь сможет заново подключить устройства\n"
"• Действие необратимо!\n\n"
"Продолжить?",
reply_markup=get_confirmation_keyboard(
f"admin_user_reset_devices_confirm_{user_id}",
f"admin_user_servers_{user_id}",
db_user.language
)
)
await callback.answer()
@admin_required
@error_handler
async def reset_user_devices(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
user_id = int(callback.data.split('_')[-1])
try:
user = await get_user_by_id(db, user_id)
if not user or not user.remnawave_uuid:
await callback.answer("❌ Пользователь не найден или не связан с RemnaWave", show_alert=True)
return
remnawave_service = RemnaWaveService()
async with remnawave_service.api as api:
success = await api.reset_user_devices(user.remnawave_uuid)
if success:
await callback.message.edit_text(
"✅ Устройства пользователя успешно сброшены",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
])
)
logger.info(f"Админ {db_user.id} сбросил устройства пользователя {user_id}")
else:
await callback.message.edit_text(
"❌ Ошибка сброса устройств",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🌍 Управление серверами", callback_data=f"admin_user_servers_{user_id}")]
])
)
except Exception as e:
logger.error(f"Ошибка сброса устройств: {e}")
await callback.answer("❌ Ошибка сброса устройств", show_alert=True)
async def _update_user_devices(db: AsyncSession, user_id: int, devices: int, admin_id: int) -> bool:
try:
user = await get_user_by_id(db, user_id)
if not user or not user.subscription:
logger.error(f"Пользователь {user_id} или подписка не найдены")
return False
subscription = user.subscription
old_devices = subscription.device_limit
subscription.device_limit = devices
subscription.updated_at = datetime.utcnow()
await db.commit()
if user.remnawave_uuid:
try:
remnawave_service = RemnaWaveService()
async with remnawave_service.api as api:
await api.update_user(
uuid=user.remnawave_uuid,
hwid_device_limit=devices
)
logger.info(f"✅ Обновлен лимит устройств в RemnaWave для пользователя {user.telegram_id}")
except Exception as rw_error:
logger.error(f"❌ Ошибка обновления лимита устройств в RemnaWave: {rw_error}")
logger.info(f"Админ {admin_id} изменил лимит устройств пользователя {user_id}: {old_devices} -> {devices}")
return True
except Exception as e:
logger.error(f"Ошибка обновления лимита устройств: {e}")
await db.rollback()
return False
async def _update_user_traffic(db: AsyncSession, user_id: int, traffic_gb: int, admin_id: int) -> bool:
try:
user = await get_user_by_id(db, user_id)
if not user or not user.subscription:
logger.error(f"Пользователь {user_id} или подписка не найдены")
return False
subscription = user.subscription
old_traffic = subscription.traffic_limit_gb
subscription.traffic_limit_gb = traffic_gb
subscription.updated_at = datetime.utcnow()
await db.commit()
if user.remnawave_uuid:
try:
from app.external.remnawave_api import TrafficLimitStrategy
remnawave_service = RemnaWaveService()
async with remnawave_service.api as api:
await api.update_user(
uuid=user.remnawave_uuid,
traffic_limit_bytes=traffic_gb * (1024**3) if traffic_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH
)
logger.info(f"✅ Обновлен лимит трафика в RemnaWave для пользователя {user.telegram_id}")
except Exception as rw_error:
logger.error(f"❌ Ошибка обновления лимита трафика в RemnaWave: {rw_error}")
traffic_text_old = "безлимитный" if old_traffic == 0 else f"{old_traffic} ГБ"
traffic_text_new = "безлимитный" if traffic_gb == 0 else f"{traffic_gb} ГБ"
logger.info(f"Админ {admin_id} изменил лимит трафика пользователя {user_id}: {traffic_text_old} -> {traffic_text_new}")
return True
except Exception as e:
logger.error(f"Ошибка обновления лимита трафика: {e}")
await db.rollback()
return False
async def _extend_subscription_by_days(db: AsyncSession, user_id: int, days: int, admin_id: int) -> bool:
try:
@@ -1648,3 +2286,58 @@ def register_handlers(dp: Dispatcher):
process_subscription_grant_text,
AdminStates.granting_subscription
)
dp.callback_query.register(
show_user_servers_management,
F.data.startswith("admin_user_servers_")
)
dp.callback_query.register(
show_server_selection,
F.data.startswith("admin_user_change_server_")
)
dp.callback_query.register(
toggle_user_server,
F.data.startswith("admin_user_toggle_server_") & ~F.data.endswith("_add") & ~F.data.endswith("_remove")
)
dp.callback_query.register(
start_devices_edit,
F.data.startswith("admin_user_devices_") & ~F.data.contains("set")
)
dp.callback_query.register(
set_user_devices_button,
F.data.startswith("admin_user_devices_set_")
)
dp.message.register(
process_devices_edit_text,
AdminStates.editing_user_devices
)
dp.callback_query.register(
start_traffic_edit,
F.data.startswith("admin_user_traffic_") & ~F.data.contains("set")
)
dp.callback_query.register(
set_user_traffic_button,
F.data.startswith("admin_user_traffic_set_")
)
dp.message.register(
process_traffic_edit_text,
AdminStates.editing_user_traffic
)
dp.callback_query.register(
confirm_reset_devices,
F.data.startswith("admin_user_reset_devices_") & ~F.data.contains("confirm")
)
dp.callback_query.register(
reset_user_devices,
F.data.startswith("admin_user_reset_devices_confirm_")
)
+13 -6
View File
@@ -356,10 +356,15 @@ async def process_stars_payment_amount(
texts = get_texts(db_user.language)
if not settings.TELEGRAM_STARS_ENABLED:
await message.answer(" Оплата Stars временно недоступна")
await message.answer(" Оплата Stars временно недоступна")
return
try:
from app.external.telegram_stars import TelegramStarsService
amount_rubles = amount_kopeks / 100
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
payment_service = PaymentService(message.bot)
invoice_link = await payment_service.create_stars_invoice(
amount_kopeks=amount_kopeks,
@@ -374,7 +379,9 @@ async def process_stars_payment_amount(
await message.answer(
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
f"Сумма: {texts.format_price(amount_kopeks)}\n\n"
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
f"К оплате: {stars_amount} звезд\n"
f"📊 Курс: {settings.get_stars_rate():.2f}₽ за звезду\n\n"
f"Нажмите кнопку ниже для оплаты:",
reply_markup=keyboard,
parse_mode="HTML"
@@ -384,7 +391,7 @@ async def process_stars_payment_amount(
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
await message.answer(" Ошибка создания платежа")
await message.answer(" Ошибка создания платежа")
@error_handler
@@ -412,7 +419,7 @@ async def process_yookassa_payment_amount(
db=db,
user_id=db_user.id,
amount_kopeks=amount_kopeks,
description=f"Пополнение баланса VPN на {settings.format_price(amount_kopeks)}",
description=settings.get_balance_payment_description(amount_kopeks),
receipt_email=None,
receipt_phone=None,
metadata={
@@ -500,14 +507,14 @@ async def check_yookassa_payment_status(
emoji = status_emoji.get(payment.status, "")
status = status_text.get(payment.status, "Неизвестно")
message_text = (f"💳 <b>Статус платежа</b>\n\n"
message_text = (f"💳 Статус платежа:\n\n"
f"🆔 ID: {payment.yookassa_payment_id[:8]}...\n"
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n"
f"📊 Статус: {emoji} {status}\n"
f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n")
if payment.is_succeeded:
message_text += "\n✅ Платеж успешно завершен!\nСредства зачислены на баланс."
message_text += "\n✅ Платеж успешно завершен!\n\nСредства зачислены на баланс."
elif payment.is_pending:
message_text += "\n⏳ Платеж ожидает оплаты. Нажмите кнопку 'Оплатить' выше."
elif payment.is_failed:
+3 -4
View File
@@ -19,7 +19,6 @@ async def show_main_menu(
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
from datetime import datetime
@@ -34,7 +33,6 @@ async def show_main_menu(
menu_text = texts.MAIN_MENU.format(
user_name=db_user.full_name,
balance=texts.format_price(db_user.balance_kopeks),
subscription_status=_get_subscription_status(db_user, texts)
)
@@ -45,7 +43,8 @@ async def show_main_menu(
is_admin=settings.is_admin(db_user.telegram_id),
has_had_paid_subscription=db_user.has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=db_user.balance_kopeks
)
)
await callback.answer()
@@ -109,7 +108,7 @@ async def handle_back_to_menu(
def _get_subscription_status(user: User, texts) -> str:
if not user.subscription:
return " Отсутствует"
return " Отсутствует"
subscription = user.subscription
current_time = datetime.utcnow()
+2 -27
View File
@@ -85,13 +85,7 @@ async def create_invite_message(
[
types.InlineKeyboardButton(
text="📤 Поделиться",
switch_inline_query=invite_text
)
],
[
types.InlineKeyboardButton(
text="📋 Скопировать ссылку",
callback_data="copy_referral_link"
switch_inline_query=invite_text
)
],
[
@@ -104,7 +98,7 @@ async def create_invite_message(
await callback.message.edit_text(
f"📝 <b>Приглашение создано!</b>\n\n"
f"Используйте кнопку ниже для отправки приглашения или скопируйте текст:\n\n"
f"Нажмите кнопку «📤 Поделиться» чтобы отправить приглашение в любой чат, или скопируйте текст ниже:\n\n"
f"<code>{invite_text}</code>",
reply_markup=keyboard,
parse_mode="HTML"
@@ -112,20 +106,6 @@ async def create_invite_message(
await callback.answer()
async def copy_referral_link(
callback: types.CallbackQuery,
db_user: User
):
bot_username = (await callback.bot.get_me()).username
referral_link = f"https://t.me/{bot_username}?start={db_user.referral_code}"
await callback.answer(
f"Ссылка скопирована: {referral_link}",
show_alert=True
)
def register_handlers(dp: Dispatcher):
dp.callback_query.register(
@@ -137,8 +117,3 @@ def register_handlers(dp: Dispatcher):
create_invite_message,
F.data == "referral_create_invite"
)
dp.callback_query.register(
copy_referral_link,
F.data == "copy_referral_link"
)
+132
View File
@@ -0,0 +1,132 @@
import logging
from aiogram import Dispatcher, types, F
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import User
from app.services.payment_service import PaymentService
from app.external.telegram_stars import TelegramStarsService
from app.database.crud.user import get_user_by_telegram_id
logger = logging.getLogger(__name__)
async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
try:
logger.info(f"📋 Pre-checkout query от {query.from_user.id}: {query.total_amount} XTR, payload: {query.invoice_payload}")
if not query.invoice_payload or not query.invoice_payload.startswith("balance_"):
logger.warning(f"Невалидный payload: {query.invoice_payload}")
await query.answer(
ok=False,
error_message="Ошибка валидации платежа. Попробуйте еще раз."
)
return
try:
from app.database.database import get_db
async for db in get_db():
user = await get_user_by_telegram_id(db, query.from_user.id)
if not user:
logger.warning(f"Пользователь {query.from_user.id} не найден в БД")
await query.answer(
ok=False,
error_message="Пользователь не найден. Обратитесь в поддержку."
)
return
break
except Exception as db_error:
logger.error(f"Ошибка подключения к БД в pre_checkout_query: {db_error}")
await query.answer(
ok=False,
error_message="Техническая ошибка. Попробуйте позже."
)
return
await query.answer(ok=True)
logger.info(f"✅ Pre-checkout одобрен для пользователя {query.from_user.id}")
except Exception as e:
logger.error(f"Ошибка в pre_checkout_query: {e}", exc_info=True)
await query.answer(
ok=False,
error_message="Техническая ошибка. Попробуйте позже."
)
async def handle_successful_payment(
message: types.Message,
db: AsyncSession,
**kwargs
):
try:
payment = message.successful_payment
user_id = message.from_user.id
logger.info(
f"💳 Успешный Stars платеж от {user_id}: "
f"{payment.total_amount} XTR, "
f"payload: {payment.invoice_payload}, "
f"charge_id: {payment.telegram_payment_charge_id}"
)
user = await get_user_by_telegram_id(db, user_id)
if not user:
logger.error(f"Пользователь {user_id} не найден при обработке Stars платежа")
await message.answer(
"❌ Ошибка: пользователь не найден. Обратитесь в поддержку."
)
return
payment_service = PaymentService(message.bot)
success = await payment_service.process_stars_payment(
db=db,
user_id=user.id,
stars_amount=payment.total_amount,
payload=payment.invoice_payload,
telegram_payment_charge_id=payment.telegram_payment_charge_id
)
if success:
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount)
await message.answer(
f"🎉 <b>Платеж успешно обработан!</b>\n\n"
f"⭐ Потрачено звезд: {payment.total_amount}\n"
f"💰 Зачислено на баланс: {rubles_amount:.2f}\n"
f"🆔 ID транзакции: {payment.telegram_payment_charge_id[:8]}...\n\n"
f"Спасибо за пополнение! 🚀",
parse_mode="HTML"
)
logger.info(
f"✅ Stars платеж успешно обработан: "
f"пользователь {user.id}, {payment.total_amount} звезд → {rubles_amount:.2f}"
)
else:
logger.error(f"Ошибка обработки Stars платежа для пользователя {user.id}")
await message.answer(
"❌ Произошла ошибка при зачислении средств. "
"Обратитесь в поддержку, платеж будет проверен вручную."
)
except Exception as e:
logger.error(f"Ошибка в successful_payment: {e}", exc_info=True)
await message.answer(
"❌ Техническая ошибка при обработке платежа. "
"Обратитесь в поддержку для решения проблемы."
)
def register_stars_handlers(dp: Dispatcher):
dp.pre_checkout_query.register(
handle_pre_checkout_query,
F.currency == "XTR"
)
dp.message.register(
handle_successful_payment,
F.successful_payment
)
logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей")
+242 -25
View File
@@ -1,4 +1,5 @@
import logging
from datetime import datetime
from aiogram import Dispatcher, types, F
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
@@ -20,6 +21,62 @@ from app.utils.user_utils import generate_unique_referral_code
logger = logging.getLogger(__name__)
async def handle_potential_referral_code(
message: types.Message,
state: FSMContext,
db: AsyncSession
):
current_state = await state.get_state()
logger.info(f"🔍 REFERRAL CHECK: Проверка сообщения '{message.text}' в состоянии {current_state}")
if current_state not in [
RegistrationStates.waiting_for_rules_accept.state,
RegistrationStates.waiting_for_referral_code.state,
None
]:
return False
user = await get_user_by_telegram_id(db, message.from_user.id)
if user and user.status == UserStatus.ACTIVE.value:
return False
potential_code = message.text.strip()
if len(potential_code) < 4 or len(potential_code) > 20:
return False
referrer = await get_user_by_referral_code(db, potential_code)
if not referrer:
await message.answer(
"❌ Неверный реферальный код.\n\n"
"💡 Если у вас есть реферальный код, убедитесь что он введен правильно.\n"
"⏭️ Для продолжения регистрации без реферального кода используйте команду /start"
)
return True
data = await state.get_data() or {}
data['referral_code'] = potential_code
data['referrer_id'] = referrer.id
await state.set_data(data)
await message.answer("✅ Реферальный код принят!")
logger.info(f"✅ Реферальный код {potential_code} применен для пользователя {message.from_user.id}")
if current_state != RegistrationStates.waiting_for_referral_code.state:
language = data.get('language', 'ru')
texts = get_texts(language)
await message.answer(
texts.RULES_TEXT,
reply_markup=get_rules_keyboard(language)
)
await state.set_state(RegistrationStates.waiting_for_rules_accept)
logger.info("📋 Правила отправлены после ввода реферального кода")
else:
await complete_registration(message, state, db)
return True
async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession, db_user=None):
logger.info(f"🚀 START: Обработка /start от {message.from_user.id}")
@@ -36,6 +93,37 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
if user and user.status != UserStatus.DELETED.value:
logger.info(f"✅ Активный пользователь найден: {user.telegram_id}")
profile_updated = False
if user.username != message.from_user.username:
old_username = user.username
user.username = message.from_user.username
logger.info(f"📝 Username обновлен: '{old_username}''{user.username}'")
profile_updated = True
if user.first_name != message.from_user.first_name:
old_first_name = user.first_name
user.first_name = message.from_user.first_name
logger.info(f"📝 Имя обновлено: '{old_first_name}''{user.first_name}'")
profile_updated = True
if user.last_name != message.from_user.last_name:
old_last_name = user.last_name
user.last_name = message.from_user.last_name
logger.info(f"📝 Фамилия обновлена: '{old_last_name}''{user.last_name}'")
profile_updated = True
user.last_activity = datetime.utcnow()
if profile_updated:
user.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(user)
logger.info(f"💾 Профиль пользователя {user.telegram_id} обновлен")
else:
await db.commit()
texts = get_texts(user.language)
if referral_code and not user.referred_by_id:
@@ -50,7 +138,6 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
await message.answer(
texts.MAIN_MENU.format(
user_name=user.full_name,
balance=texts.format_price(user.balance_kopeks),
subscription_status=_get_subscription_status(user, texts)
),
reply_markup=get_main_menu_keyboard(
@@ -58,7 +145,8 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
is_admin=settings.is_admin(user.telegram_id),
has_had_paid_subscription=user.has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=user.balance_kopeks
)
)
await state.clear()
@@ -107,6 +195,13 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
user.remnawave_uuid = None
user.has_had_paid_subscription = False
user.referred_by_id = None
user.username = message.from_user.username
user.first_name = message.from_user.first_name
user.last_name = message.from_user.last_name
user.updated_at = datetime.utcnow()
user.last_activity = datetime.utcnow()
from app.utils.user_utils import generate_unique_referral_code
user.referral_code = await generate_unique_referral_code(db, user.telegram_id)
@@ -285,6 +380,7 @@ async def process_referral_code_skip(
await complete_registration_from_callback(callback, state, db)
async def complete_registration_from_callback(
callback: types.CallbackQuery,
state: FSMContext,
@@ -299,6 +395,10 @@ async def complete_registration_from_callback(
logger.warning(f"⚠️ Пользователь {callback.from_user.id} уже активен! Показываем главное меню.")
texts = get_texts(existing_user.language)
data = await state.get_data()
if data.get('referral_code') and not existing_user.referred_by_id:
await callback.message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.")
has_active_subscription = existing_user.subscription is not None
subscription_is_active = False
@@ -306,21 +406,24 @@ async def complete_registration_from_callback(
subscription_is_active = existing_user.subscription.is_active
user_name = existing_user.full_name
user_telegram_id = existing_user.telegram_id
user_language = existing_user.language
has_had_paid_subscription = existing_user.has_had_paid_subscription
balance_kopeks = existing_user.balance_kopeks
try:
await callback.message.answer(
texts.MAIN_MENU.format(
user_name=user_name,
balance=texts.format_price(balance_kopeks),
subscription_status=_get_subscription_status(existing_user, texts)
),
reply_markup=get_main_menu_keyboard(
language=existing_user.language,
is_admin=settings.is_admin(existing_user.telegram_id),
has_had_paid_subscription=existing_user.has_had_paid_subscription,
language=user_language,
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
except Exception as e:
@@ -362,7 +465,7 @@ async def complete_registration_from_callback(
user = existing_user
logger.info(f"✅ Пользователь {callback.from_user.id} восстановлен")
else:
elif not existing_user:
logger.info(f"🆕 Создаем нового пользователя {callback.from_user.id}")
referral_code = await generate_unique_referral_code(db, callback.from_user.id)
@@ -377,6 +480,20 @@ async def complete_registration_from_callback(
referred_by_id=referrer_id,
referral_code=referral_code
)
else:
logger.info(f"🔄 Обновляем существующего пользователя {callback.from_user.id}")
existing_user.status = UserStatus.ACTIVE.value
existing_user.language = language
if referrer_id and not existing_user.referred_by_id:
existing_user.referred_by_id = referrer_id
from datetime import datetime
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
await db.commit()
await db.refresh(existing_user)
user = existing_user
if referrer_id:
try:
@@ -390,7 +507,6 @@ async def complete_registration_from_callback(
has_active_subscription = False
subscription_is_active = False
user_name = user.full_name
balance_kopeks = user.balance_kopeks
user_telegram_id = user.telegram_id
@@ -401,7 +517,6 @@ async def complete_registration_from_callback(
await callback.message.answer(
texts.MAIN_MENU.format(
user_name=user_name,
balance=texts.format_price(balance_kopeks),
subscription_status=_get_subscription_status_simple(texts)
),
reply_markup=get_main_menu_keyboard(
@@ -409,7 +524,8 @@ async def complete_registration_from_callback(
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
logger.info(f"✅ Главное меню отправлено для пользователя {user_telegram_id}")
@@ -419,24 +535,25 @@ async def complete_registration_from_callback(
balance_rubles = balance_kopeks / 100
await callback.message.answer(
f"Добро пожаловать, {user_name}!\n"
f"Баланс: {balance_rubles}\n"
f"Баланс: {balance_rubles:.2f}\n"
f"Подписка: Нет активной подписки",
reply_markup=get_main_menu_keyboard(
language=user_language,
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
logger.info(f"✅ Fallback главное меню отправлено для пользователя {user_telegram_id}")
except Exception as fallback_error:
logger.error(f" Критическая ошибка при отправке fallback меню: {fallback_error}")
logger.error(f" Критическая ошибка при отправке fallback меню: {fallback_error}")
try:
await callback.message.answer(f"Добро пожаловать, {user_name}! Регистрация завершена.")
logger.info(f"✅ Простое приветствие отправлено для пользователя {user_telegram_id}")
except Exception as final_error:
logger.error(f" Критическая ошибка при отправке простого сообщения: {final_error}")
logger.error(f" Критическая ошибка при отправке простого сообщения: {final_error}")
logger.info(f"✅ Регистрация завершена для пользователя: {user_telegram_id}")
@@ -451,6 +568,48 @@ async def complete_registration(
existing_user = await get_user_by_telegram_id(db, message.from_user.id)
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
logger.warning(f"⚠️ Пользователь {message.from_user.id} уже активен! Показываем главное меню.")
texts = get_texts(existing_user.language)
data = await state.get_data()
if data.get('referral_code') and not existing_user.referred_by_id:
await message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.")
has_active_subscription = existing_user.subscription is not None
subscription_is_active = False
if existing_user.subscription:
subscription_is_active = existing_user.subscription.is_active
user_name = existing_user.full_name
user_telegram_id = existing_user.telegram_id
user_language = existing_user.language
has_had_paid_subscription = existing_user.has_had_paid_subscription
balance_kopeks = existing_user.balance_kopeks
try:
await message.answer(
texts.MAIN_MENU.format(
user_name=user_name,
subscription_status=_get_subscription_status(existing_user, texts)
),
reply_markup=get_main_menu_keyboard(
language=user_language,
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
except Exception as e:
logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}")
await message.answer(f"Добро пожаловать, {user_name}!")
await state.clear()
return
data = await state.get_data()
language = data.get('language', 'ru')
texts = get_texts(language)
@@ -483,7 +642,7 @@ async def complete_registration(
user = existing_user
logger.info(f"✅ Пользователь {message.from_user.id} восстановлен")
else:
elif not existing_user:
logger.info(f"🆕 Создаем нового пользователя {message.from_user.id}")
referral_code = await generate_unique_referral_code(db, message.from_user.id)
@@ -498,6 +657,20 @@ async def complete_registration(
referred_by_id=referrer_id,
referral_code=referral_code
)
else:
logger.info(f"🔄 Обновляем существующего пользователя {message.from_user.id}")
existing_user.status = UserStatus.ACTIVE.value
existing_user.language = language
if referrer_id and not existing_user.referred_by_id:
existing_user.referred_by_id = referrer_id
from datetime import datetime
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
await db.commit()
await db.refresh(existing_user)
user = existing_user
if referrer_id:
try:
@@ -511,7 +684,6 @@ async def complete_registration(
has_active_subscription = False
subscription_is_active = False
user_name = user.full_name
balance_kopeks = user.balance_kopeks
user_telegram_id = user.telegram_id
@@ -522,7 +694,6 @@ async def complete_registration(
await message.answer(
texts.MAIN_MENU.format(
user_name=user_name,
balance=texts.format_price(balance_kopeks),
subscription_status=_get_subscription_status_simple(texts)
),
reply_markup=get_main_menu_keyboard(
@@ -530,7 +701,8 @@ async def complete_registration(
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
logger.info(f"✅ Главное меню отправлено для пользователя {user_telegram_id}")
@@ -540,19 +712,20 @@ async def complete_registration(
balance_rubles = balance_kopeks / 100
await message.answer(
f"Добро пожаловать, {user_name}!\n"
f"Баланс: {balance_rubles}\n"
f"Баланс: {balance_rubles:.2f}\n"
f"Подписка: Нет активной подписки",
reply_markup=get_main_menu_keyboard(
language=user_language,
is_admin=settings.is_admin(user_telegram_id),
has_had_paid_subscription=has_had_paid_subscription,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active
subscription_is_active=subscription_is_active,
balance_kopeks=balance_kopeks
)
)
logger.info(f"✅ Fallback главное меню отправлено для пользователя {user_telegram_id}")
except Exception as fallback_error:
logger.error(f" Критическая ошибка при отправке fallback меню: {fallback_error}")
logger.error(f" Критическая ошибка при отправке fallback меню: {fallback_error}")
try:
await message.answer(f"Добро пожаловать, {user_name}! Регистрация завершена.")
logger.info(f"✅ Простое приветствие отправлено для пользователя {user_telegram_id}")
@@ -563,9 +736,44 @@ async def complete_registration(
def _get_subscription_status(user, texts):
if user.subscription and user.subscription.is_active:
return getattr(texts, 'SUBSCRIPTION_ACTIVE', 'Активна')
return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки')
if not user or not hasattr(user, 'subscription'):
return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки')
if not user.subscription:
return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки')
subscription = user.subscription
from datetime import datetime
current_time = datetime.utcnow()
if hasattr(subscription, 'end_date') and subscription.end_date <= current_time:
return f"🔴 Истекла\n📅 {subscription.end_date.strftime('%d.%m.%Y')}"
if hasattr(subscription, 'end_date'):
days_left = (subscription.end_date - current_time).days
else:
days_left = 0
is_trial = getattr(subscription, 'is_trial', False)
if is_trial:
if days_left > 1:
return f"🎁 Тестовая подписка\n📅 до {subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)"
elif days_left == 1:
return "🎁 Тестовая подписка\n⚠️ истекает завтра!"
else:
return "🎁 Тестовая подписка\n⚠️ истекает сегодня!"
else:
if days_left > 7:
return f"💎 Активна\n📅 до {subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)"
elif days_left > 1:
return f"💎 Активна\n⚠️ истекает через {days_left} дн."
elif days_left == 1:
return "💎 Активна\n⚠️ истекает завтра!"
else:
return "💎 Активна\n⚠️ истекает сегодня!"
def _get_subscription_status_simple(texts):
@@ -614,4 +822,13 @@ def register_handlers(dp: Dispatcher):
)
logger.info("✅ Зарегистрирован process_referral_code_input")
dp.message.register(
handle_potential_referral_code,
StateFilter(
RegistrationStates.waiting_for_rules_accept,
RegistrationStates.waiting_for_referral_code
)
)
logger.info("✅ Зарегистрирован handle_potential_referral_code")
logger.info("🔧 === КОНЕЦ регистрации обработчиков start.py ===")
+411 -82
View File
@@ -8,7 +8,7 @@ import json
import os
from typing import Dict, List, Any, Tuple, Optional
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
from app.config import settings, PERIOD_PRICES, get_traffic_prices
from app.states import SubscriptionStates
from app.database.crud.subscription import (
get_subscription_by_user_id, create_trial_subscription,
@@ -43,6 +43,7 @@ from app.services.referral_service import process_referral_purchase
logger = logging.getLogger(__name__)
TRAFFIC_PRICES = get_traffic_prices()
async def show_subscription_info(
callback: types.CallbackQuery,
@@ -80,7 +81,7 @@ async def show_subscription_info(
if subscription.is_trial:
actual_status = "trial_active"
status_display = "Тестовая"
status_emoji = "🎁"
status_emoji = "🎯"
else:
actual_status = "paid_active"
status_display = "Активна"
@@ -116,26 +117,61 @@ async def show_subscription_info(
subscription_type = "Триал" if subscription.is_trial else "Платная"
if subscription.traffic_limit_gb == 0:
traffic_used_display = f"∞ (безлимит) / {subscription.traffic_used_gb:.1f} ГБ"
traffic_used_display = f"∞ (безлимит) | Использовано: {subscription.traffic_used_gb:.1f} ГБ"
else:
traffic_used_display = f"{subscription.traffic_used_gb:.1f} / {subscription.traffic_limit_gb} ГБ"
devices_used = await get_current_devices_count(db_user)
devices_used_str = ""
devices_list = []
devices_count = 0
try:
if db_user.remnawave_uuid:
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
async with service.api as api:
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
if response and 'response' in response:
devices_info = response['response']
devices_count = devices_info.get('total', 0)
devices_list = devices_info.get('devices', [])
devices_used_str = str(devices_count)
logger.info(f"Найдено {devices_count} устройств для пользователя {db_user.telegram_id}")
else:
logger.warning(f"Не удалось получить информацию об устройствах для {db_user.telegram_id}")
except Exception as e:
logger.error(f"Ошибка получения устройств для отображения: {e}")
devices_used_str = await get_current_devices_count(db_user)
servers_names = await get_servers_display_names(subscription.connected_squads)
servers_display = servers_names if servers_names else "Нет серверов"
message = f"""👤 {db_user.full_name}
💰 Баланс: {settings.format_price(db_user.balance_kopeks)}
📱 Подписка: {status_emoji} {status_display}{warning_text}
📱 Информация о подписке
🎭 Тип: {subscription_type}
📅 Действует до: {subscription.end_date.strftime("%d.%m.%Y %H:%M")}
Осталось: {time_left_text}
📈 Трафик: {traffic_used_display}
🌍 Серверы: {len(subscription.connected_squads)} стран
📱 Устройства: {devices_used} / {subscription.device_limit}"""
🌍 Серверы: {servers_display}
📱 Устройства: {devices_used_str} / {subscription.device_limit}"""
if devices_list and len(devices_list) > 0:
message += f"\n\n<blockquote>📱 <b>Подключенные устройства:</b>\n"
for device in devices_list[:5]:
platform = device.get('platform', 'Unknown')
device_model = device.get('deviceModel', 'Unknown')
device_info = f"{platform} - {device_model}"
if len(device_info) > 35:
device_info = device_info[:32] + "..."
message += f"{device_info}\n"
message += "</blockquote>"
if hasattr(subscription, 'subscription_url') and subscription.subscription_url:
if actual_status in ['trial_active', 'paid_active']:
@@ -154,6 +190,64 @@ async def show_subscription_info(
)
await callback.answer()
async def get_current_devices_detailed(db_user: User) -> dict:
try:
if not db_user.remnawave_uuid:
return {"count": 0, "devices": []}
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
async with service.api as api:
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
if response and 'response' in response:
devices_info = response['response']
total_devices = devices_info.get('total', 0)
devices_list = devices_info.get('devices', [])
return {
"count": total_devices,
"devices": devices_list[:5]
}
else:
return {"count": 0, "devices": []}
except Exception as e:
logger.error(f"Ошибка получения детальной информации об устройствах: {e}")
return {"count": 0, "devices": []}
async def get_servers_display_names(squad_uuids: List[str]) -> str:
"""
Получает отображаемые названия серверов по их UUID
"""
if not squad_uuids:
return "Нет серверов"
try:
countries = await _get_available_countries()
server_names = []
for uuid in squad_uuids:
for country in countries:
if country['uuid'] == uuid:
server_names.append(country['name'])
break
if not server_names:
return f"{len(squad_uuids)} стран"
if len(server_names) > 6:
displayed = ", ".join(server_names[:6])
remaining = len(server_names) - 6
return f"{displayed} и ещё {remaining}"
else:
return ", ".join(server_names)
except Exception as e:
logger.error(f"Ошибка получения названий серверов: {e}")
return f"{len(squad_uuids)} стран"
async def get_current_devices_count(db_user: User) -> str:
try:
if not db_user.remnawave_uuid:
@@ -181,7 +275,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
if subscription.is_trial:
return 0
from app.config import TRAFFIC_PRICES, PERIOD_PRICES, settings
from app.config import settings
from app.services.subscription_service import SubscriptionService
subscription_service = SubscriptionService()
@@ -197,8 +291,8 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
subscription.connected_squads, db
)
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
traffic_cost = settings.get_traffic_price(subscription.traffic_limit_gb)
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
@@ -212,7 +306,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
return total_cost
except Exception as e:
logger.error(f"⚠ Ошибка расчета стоимости подписки: {e}")
logger.error(f" Ошибка расчета стоимости подписки: {e}")
return 0
@@ -328,7 +422,7 @@ async def start_subscription_purchase(
initial_data = {
'period_days': None,
'countries': [],
'devices': 1,
'devices': settings.DEFAULT_DEVICE_LIMIT,
'total_price': 0
}
@@ -349,6 +443,9 @@ async def handle_add_countries(
db: AsyncSession,
state: FSMContext
):
if not await _should_show_countries_management():
await callback.answer("ℹ️ Управление серверами недоступно - доступен только один сервер", show_alert=True)
return
texts = get_texts(db_user.language)
subscription = db_user.subscription
@@ -366,7 +463,7 @@ async def handle_add_countries(
current_countries_names.append(country['name'])
text = "🌍 <b>Управление странами подписки</b>\n\n"
text += f"📍 <b>Текущие страны ({len(current_countries)}):</b>\n"
text += f"📋 <b>Текущие страны ({len(current_countries)}):</b>\n"
if current_countries_names:
text += "\n".join(f"{name}" for name in current_countries_names)
else:
@@ -409,11 +506,11 @@ async def get_countries_price_by_uuids_fallback(country_uuids: List[str], db: As
total_price += price
prices_list.append(price)
else:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
except Exception:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
@@ -421,7 +518,7 @@ async def get_countries_price_by_uuids_fallback(country_uuids: List[str], db: As
except Exception as e:
logger.error(f"Ошибка fallback функции: {e}")
default_prices = [1000] * len(country_uuids)
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
async def handle_manage_country(
@@ -667,36 +764,57 @@ async def handle_extend_subscription(
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
subscription = db_user.subscription
if not subscription or subscription.is_trial:
await callback.answer(" Продление доступно только для платных подписок", show_alert=True)
await callback.answer(" Продление доступно только для платных подписок", show_alert=True)
return
if subscription.days_left > 3:
await callback.answer(" Продление доступно за 3 дня до окончания подписки", show_alert=True)
await callback.answer(" Продление доступно за 3 дня до окончания подписки", show_alert=True)
return
subscription_service = SubscriptionService()
available_periods = settings.get_available_renewal_periods()
renewal_prices = {}
for days in [30, 90, 180]:
price = await subscription_service.calculate_renewal_price(subscription, days, db)
renewal_prices[days] = price
for days in available_periods:
try:
price = await subscription_service.calculate_renewal_price(subscription, days, db)
renewal_prices[days] = price
except Exception as e:
logger.error(f"Ошибка расчета цены для периода {days}: {e}")
continue
if not renewal_prices:
await callback.answer("⌛ Нет доступных периодов для продления", show_alert=True)
return
prices_text = ""
period_display = {
14: "14 дней",
30: "30 дней",
60: "60 дней",
90: "90 дней",
180: "180 дней",
360: "360 дней"
}
for days in available_periods:
if days in renewal_prices and days in period_display:
prices_text += f"📅 {period_display[days]} - {texts.format_price(renewal_prices[days])}\n"
await callback.message.edit_text(
f"<b>Продление подписки</b>\n\n"
f"⏰ Продление подписки\n\n"
f"Осталось дней: {subscription.days_left}\n\n"
f"<b>Ваша текущая конфигурация:</b>\n"
f"🌍 Серверов: {len(subscription.connected_squads)}\n"
f"📊 Трафик: {texts.format_traffic(subscription.traffic_limit_gb)}\n"
f"📱 Устройств: {subscription.device_limit}\n\n"
f"<b>Выберите период продления:</b>\n"
f"📅 30 дней - {texts.format_price(renewal_prices[30])}\n"
f"📅 90 дней - {texts.format_price(renewal_prices[90])}\n"
f"📅 180 дней - {texts.format_price(renewal_prices[180])}\n\n"
f"{prices_text.rstrip()}\n\n"
f"💡 <i>Цена включает все ваши текущие серверы и настройки</i>",
reply_markup=get_extend_subscription_keyboard_with_prices(db_user.language, renewal_prices),
parse_mode="HTML"
@@ -761,10 +879,14 @@ async def confirm_add_traffic(
texts = get_texts(db_user.language)
subscription = db_user.subscription
price = TRAFFIC_PRICES[traffic_gb]
price = settings.get_traffic_price(traffic_gb)
if price == 0 and traffic_gb != 0:
await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True)
return
if db_user.balance_kopeks < price:
await callback.answer(" Недостаточно средств на балансе", show_alert=True)
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
return
try:
@@ -774,7 +896,7 @@ async def confirm_add_traffic(
)
if not success:
await callback.answer(" Ошибка списания средств", show_alert=True)
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
if traffic_gb == 0:
@@ -829,6 +951,11 @@ async def confirm_add_traffic(
await callback.answer()
def update_traffic_prices():
from app.config import refresh_traffic_prices
refresh_traffic_prices()
logger.info("🔄 TRAFFIC_PRICES обновлены из конфигурации")
async def confirm_add_devices(
callback: types.CallbackQuery,
@@ -840,10 +967,20 @@ async def confirm_add_devices(
texts = get_texts(db_user.language)
subscription = db_user.subscription
new_total_devices = subscription.device_limit + devices_count
if settings.MAX_DEVICES_LIMIT > 0 and new_total_devices > settings.MAX_DEVICES_LIMIT:
await callback.answer(
f"⚠️ Превышен максимальный лимит устройств ({settings.MAX_DEVICES_LIMIT}). "
f"У вас: {subscription.device_limit}, добавляете: {devices_count}",
show_alert=True
)
return
price = devices_count * settings.PRICE_PER_DEVICE
if db_user.balance_kopeks < price:
await callback.answer(" Недостаточно средств на балансе", show_alert=True)
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
return
try:
@@ -853,7 +990,7 @@ async def confirm_add_devices(
)
if not success:
await callback.answer(" Ошибка списания средств", show_alert=True)
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
await add_subscription_devices(db, subscription, devices_count)
@@ -1122,27 +1259,96 @@ async def select_period(
data['total_price'] = PERIOD_PRICES[period_days]
if settings.is_traffic_fixed():
fixed_traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
fixed_traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
data['total_price'] += fixed_traffic_price
data['traffic_gb'] = settings.get_fixed_traffic_limit()
await state.set_data(data)
if settings.is_traffic_selectable():
available_packages = [pkg for pkg in settings.get_traffic_packages() if pkg['enabled']]
if not available_packages:
await callback.answer("⚠️ Пакеты трафика не настроены", show_alert=True)
return
await callback.message.edit_text(
texts.SELECT_TRAFFIC,
reply_markup=get_traffic_packages_keyboard(db_user.language)
)
await state.set_state(SubscriptionStates.selecting_traffic)
else:
countries = await _get_available_countries()
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
if await _should_show_countries_management():
countries = await _get_available_countries()
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
else:
countries = await _get_available_countries()
available_countries = [c for c in countries if c.get('is_available', True)]
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
await state.set_data(data)
await callback.message.edit_text(
texts.SELECT_DEVICES,
reply_markup=get_devices_keyboard(1, db_user.language)
)
await state.set_state(SubscriptionStates.selecting_devices)
await callback.answer()
async def refresh_traffic_config():
try:
from app.config import refresh_traffic_prices
refresh_traffic_prices()
packages = settings.get_traffic_packages()
enabled_count = sum(1 for pkg in packages if pkg['enabled'])
logger.info(f"🔄 Конфигурация трафика обновлена: {enabled_count} активных пакетов")
for pkg in packages:
if pkg['enabled']:
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
logger.info(f" 📦 {gb_text}: {pkg['price']/100}")
return True
except Exception as e:
logger.error(f"⚠️ Ошибка обновления конфигурации трафика: {e}")
return False
async def get_traffic_packages_info() -> str:
try:
packages = settings.get_traffic_packages()
info_lines = ["📦 Настроенные пакеты трафика:"]
enabled_packages = [pkg for pkg in packages if pkg['enabled']]
disabled_packages = [pkg for pkg in packages if not pkg['enabled']]
if enabled_packages:
info_lines.append("\n✅ Активные:")
for pkg in enabled_packages:
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
info_lines.append(f"{gb_text}: {pkg['price']/100}")
if disabled_packages:
info_lines.append("\n❌ Отключенные:")
for pkg in disabled_packages:
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
info_lines.append(f"{gb_text}: {pkg['price']/100}")
info_lines.append(f"\n📊 Всего пакетов: {len(packages)}")
info_lines.append(f"🟢 Активных: {len(enabled_packages)}")
info_lines.append(f"🔴 Отключенных: {len(disabled_packages)}")
return "\n".join(info_lines)
except Exception as e:
return f"⚠️ Ошибка получения информации: {e}"
async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSession):
devices_used = await get_current_devices_count(db_user)
@@ -1216,23 +1422,36 @@ async def select_traffic(
state: FSMContext,
db_user: User
):
traffic_gb = int(callback.data.split('_')[1])
texts = get_texts(db_user.language)
data = await state.get_data()
data['traffic_gb'] = traffic_gb
data['total_price'] += TRAFFIC_PRICES[traffic_gb]
traffic_price = settings.get_traffic_price(traffic_gb)
data['total_price'] += traffic_price
await state.set_data(data)
countries = await _get_available_countries()
if await _should_show_countries_management():
countries = await _get_available_countries()
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
else:
countries = await _get_available_countries()
available_countries = [c for c in countries if c.get('is_available', True)]
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
await state.set_data(data)
await callback.message.edit_text(
texts.SELECT_DEVICES,
reply_markup=get_devices_keyboard(1, db_user.language)
)
await state.set_state(SubscriptionStates.selecting_devices)
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
await callback.answer()
@@ -1242,7 +1461,6 @@ async def select_country(
db_user: User,
db: AsyncSession
):
country_uuid = callback.data.split('_')[1]
data = await state.get_data()
@@ -1254,7 +1472,7 @@ async def select_country(
countries = await _get_available_countries()
base_price = PERIOD_PRICES[data['period_days']] + TRAFFIC_PRICES[data['traffic_gb']]
base_price = PERIOD_PRICES[data['period_days']] + settings.get_traffic_price(data['traffic_gb'])
try:
subscription_service = SubscriptionService()
@@ -1314,7 +1532,7 @@ async def select_devices(
base_price = (
PERIOD_PRICES[data['period_days']] +
TRAFFIC_PRICES[data['traffic_gb']]
settings.get_traffic_price(data['traffic_gb'])
)
countries = await _get_available_countries()
@@ -1323,7 +1541,7 @@ async def select_devices(
if c['uuid'] in data['countries']
)
devices_price = (devices - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
data['devices'] = devices
data['total_price'] = base_price + countries_price + devices_price
@@ -1341,7 +1559,7 @@ async def devices_continue(
db: AsyncSession
):
if not callback.data == "devices_continue":
await callback.answer(" Некорректный запрос", show_alert=True)
await callback.answer("⚠️ Некорректный запрос", show_alert=True)
return
data = await state.get_data()
@@ -1364,21 +1582,23 @@ async def devices_continue(
base_price = PERIOD_PRICES[data['period_days']]
if settings.is_traffic_fixed():
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
final_traffic_gb = settings.get_fixed_traffic_limit()
else:
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
traffic_price = settings.get_traffic_price(data['traffic_gb'])
final_traffic_gb = data['traffic_gb']
devices_price = (data['devices'] - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_price = base_price + traffic_price + countries_price + devices_price
data['total_price'] = total_price
await state.set_data(data)
if settings.is_traffic_fixed():
if data['traffic_gb'] == 0:
if final_traffic_gb == 0:
traffic_display = "Безлимитный"
else:
traffic_display = f"{data['traffic_gb']} ГБ"
traffic_display = f"{final_traffic_gb} ГБ"
else:
if data['traffic_gb'] == 0:
traffic_display = "Безлимитный"
@@ -1428,13 +1648,13 @@ async def confirm_purchase(
countries_price += country['price_kopeks']
server_prices.append(country['price_kopeks'])
devices_price = (data['devices'] - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
if settings.is_traffic_fixed():
traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
final_traffic_gb = settings.get_fixed_traffic_limit()
else:
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
traffic_price = settings.get_traffic_price(data['traffic_gb'])
final_traffic_gb = data['traffic_gb']
final_price = base_price + traffic_price + countries_price + devices_price
@@ -1522,7 +1742,7 @@ async def confirm_purchase(
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
if not remnawave_user:
logger.error(f" Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
logger.error(f"⚠️ Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
logger.info(f"🔄 Fallback: принудительное создание нового RemnaWave пользователя")
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
@@ -1588,6 +1808,91 @@ async def confirm_purchase(
await state.clear()
await callback.answer()
async def add_traffic(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
if settings.is_traffic_fixed():
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
return
traffic_gb = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
subscription = db_user.subscription
price = settings.get_traffic_price(traffic_gb)
if price == 0 and traffic_gb != 0:
await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True)
return
if db_user.balance_kopeks < price:
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
return
try:
success = await subtract_user_balance(
db, db_user, price,
f"Добавление {traffic_gb} ГБ трафика"
)
if not success:
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
if traffic_gb == 0:
subscription.traffic_limit_gb = 0
else:
await add_subscription_traffic(db, subscription, traffic_gb)
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
await create_transaction(
db=db,
user_id=db_user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price,
description=f"Добавление {traffic_gb} ГБ трафика"
)
try:
await process_referral_purchase(
db=db,
user_id=db_user.id,
purchase_amount_kopeks=price,
transaction_id=None
)
except Exception as e:
logger.error(f"Ошибка обработки реферальной покупки: {e}")
await db.refresh(db_user)
await db.refresh(subscription)
success_text = f"✅ Трафик успешно добавлен!\n\n"
if traffic_gb == 0:
success_text += "🎉 Теперь у вас безлимитный трафик!"
else:
success_text += f"📈 Добавлено: {traffic_gb} ГБ\n"
success_text += f"Новый лимит: {texts.format_traffic(subscription.traffic_limit_gb)}"
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language)
)
logger.info(f"✅ Пользователь {db_user.telegram_id} добавил {traffic_gb} ГБ трафика")
except Exception as e:
logger.error(f"Ошибка добавления трафика: {e}")
await callback.message.edit_text(
texts.ERROR,
reply_markup=get_back_keyboard(db_user.language)
)
await callback.answer()
async def create_paid_subscription_with_traffic_mode(
db: AsyncSession,
user_id: int,
@@ -1620,6 +1925,15 @@ async def create_paid_subscription_with_traffic_mode(
return subscription
def validate_traffic_price(gb: int) -> bool:
from app.config import settings
price = settings.get_traffic_price(gb)
if gb == 0:
return True
return price > 0
async def handle_subscription_settings(
callback: types.CallbackQuery,
@@ -1646,9 +1960,11 @@ async def handle_subscription_settings(
Выберите что хотите изменить:
"""
show_countries = await _should_show_countries_management()
await callback.message.edit_text(
settings_text,
reply_markup=get_subscription_settings_keyboard(db_user.language),
reply_markup=get_subscription_settings_keyboard(db_user.language, show_countries),
parse_mode="HTML"
)
await callback.answer()
@@ -1837,7 +2153,7 @@ async def _get_available_countries():
countries.append({
"uuid": squad["uuid"],
"name": squad_name,
"price_kopeks": 1000,
"price_kopeks": 0,
"is_available": True
})
@@ -1968,6 +2284,15 @@ async def handle_add_country_to_subscription(
await callback.answer()
async def _should_show_countries_management() -> bool:
try:
countries = await _get_available_countries()
available_countries = [c for c in countries if c.get('is_available', True)]
return len(available_countries) > 1
except Exception as e:
logger.error(f"Ошибка проверки доступных серверов: {e}")
return True
async def confirm_add_countries_to_subscription(
callback: types.CallbackQuery,
@@ -2186,7 +2511,7 @@ async def handle_connect_subscription(
subscription = db_user.subscription
if not subscription or not subscription.subscription_url:
await callback.answer(" У вас нет активной подписки или ссылка еще генерируется", show_alert=True)
await callback.answer(" У вас нет активной подписки или ссылка еще генерируется", show_alert=True)
return
connect_mode = settings.CONNECT_BUTTON_MODE
@@ -2195,13 +2520,10 @@ async def handle_connect_subscription(
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="🔗 Открыть подписку",
text="🔗 Открыть подписку в мини-приложении",
web_app=types.WebAppInfo(url=subscription.subscription_url)
)
],
[
InlineKeyboardButton(text="📋 Показать ссылку", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
@@ -2209,9 +2531,9 @@ async def handle_connect_subscription(
await callback.message.edit_text(
f"""
🔗 <b>Подключить подписку</b>
📱 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:
🚀 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:
""",
reply_markup=keyboard,
parse_mode="HTML"
@@ -2219,7 +2541,7 @@ async def handle_connect_subscription(
elif connect_mode == "miniapp_custom":
if not settings.MINIAPP_CUSTOM_URL:
await callback.answer(" Кастомная ссылка для мини-приложения не настроена", show_alert=True)
await callback.answer(" Кастомная ссылка для мини-приложения не настроена", show_alert=True)
return
keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -2229,9 +2551,6 @@ async def handle_connect_subscription(
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL)
)
],
[
InlineKeyboardButton(text="📋 Показать ссылку подписки", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
@@ -2242,9 +2561,6 @@ async def handle_connect_subscription(
🚀 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть приложение:
📋 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
""",
reply_markup=keyboard,
parse_mode="HTML"
@@ -2407,6 +2723,16 @@ async def handle_specific_app_guide(
)
await callback.answer()
async def handle_no_traffic_packages(
callback: types.CallbackQuery,
db_user: User
):
await callback.answer(
"⚠️ В данный момент нет доступных пакетов трафика. "
"Обратитесь в техподдержку для получения информации.",
show_alert=True
)
async def handle_open_subscription_link(
callback: types.CallbackQuery,
@@ -2518,6 +2844,7 @@ def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMa
def register_handlers(dp: Dispatcher):
update_traffic_prices()
dp.callback_query.register(
show_subscription_info,
@@ -2539,7 +2866,6 @@ def register_handlers(dp: Dispatcher):
F.data.in_(["menu_buy", "subscription_upgrade"])
)
dp.callback_query.register(
handle_add_countries,
F.data == "subscription_add_countries"
@@ -2595,7 +2921,6 @@ def register_handlers(dp: Dispatcher):
F.data == "confirm_reset_devices"
)
dp.callback_query.register(
select_period,
F.data.startswith("period_"),
@@ -2626,7 +2951,6 @@ def register_handlers(dp: Dispatcher):
SubscriptionStates.confirming_purchase
)
dp.callback_query.register(
handle_autopay_menu,
F.data == "subscription_autopay"
@@ -2708,3 +3032,8 @@ def register_handlers(dp: Dispatcher):
handle_subscription_settings,
F.data == "subscription_settings"
)
dp.callback_query.register(
handle_no_traffic_packages,
F.data == "no_traffic_packages"
)
-171
View File
@@ -1,171 +0,0 @@
import logging
from datetime import datetime, timedelta
from typing import List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest
from app.database.models import User, Subscription
from app.database.crud.user import get_user_by_id, subtract_user_balance
from app.database.crud.subscription import get_expiring_subscriptions, extend_subscription
from app.database.crud.transaction import create_transaction
from app.database.models import TransactionType
from app.keyboards.inline import get_autopay_notification_keyboard, get_subscription_expiring_keyboard
from app.localization.texts import get_texts
from app.services.subscription_service import SubscriptionService
logger = logging.getLogger(__name__)
async def send_subscription_expiring_notification(
bot: Bot,
db: AsyncSession,
subscription: Subscription,
days_left: int
) -> bool:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user:
return False
texts = get_texts(user.language)
if subscription.is_trial:
text = texts.TRIAL_ENDING_SOON.format(
price=texts.format_price(30000)
)
else:
autopay_status = texts.AUTOPAY_ENABLED_TEXT if subscription.autopay_enabled else texts.AUTOPAY_DISABLED_TEXT
if subscription.autopay_enabled:
action_text = f"💰 Убедитесь, что на балансе достаточно средств: {texts.format_price(user.balance_kopeks)}"
else:
action_text = "💡 Включите автоплатеж или продлите подписку вручную"
text = texts.SUBSCRIPTION_EXPIRING_PAID.format(
days=days_left,
end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M"),
autopay_status=autopay_status,
action_text=action_text
)
keyboard = get_subscription_expiring_keyboard(subscription.id, user.language)
await bot.send_message(
chat_id=user.telegram_id,
text=text,
reply_markup=keyboard,
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление об истечении подписки пользователю {user.telegram_id}")
return True
except TelegramBadRequest as e:
logger.warning(f"⚠️ Не удалось отправить уведомление пользователю {user.telegram_id}: {e}")
return False
except Exception as e:
logger.error(f"❌ Ошибка отправки уведомления об истечении подписки: {e}")
return False
async def send_autopay_failed_notification(
bot: Bot,
db: AsyncSession,
subscription: Subscription,
required_amount: int
) -> bool:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user:
return False
texts = get_texts(user.language)
text = texts.AUTOPAY_FAILED.format(
balance=texts.format_price(user.balance_kopeks),
required=texts.format_price(required_amount)
)
keyboard = get_autopay_notification_keyboard(subscription.id, user.language)
await bot.send_message(
chat_id=user.telegram_id,
text=text,
reply_markup=keyboard,
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление о неудачном автоплатеже пользователю {user.telegram_id}")
return True
except TelegramBadRequest as e:
logger.warning(f"⚠️ Не удалось отправить уведомление пользователю {user.telegram_id}: {e}")
return False
except Exception as e:
logger.error(f"❌ Ошибка отправки уведомления о неудачном автоплатеже: {e}")
return False
async def process_autopayment(
bot: Bot,
db: AsyncSession,
subscription: Subscription
) -> bool:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user:
logger.error(f"Пользователь {subscription.user_id} не найден для автоплатежа")
return False
subscription_service = SubscriptionService()
renewal_cost = await subscription_service.calculate_renewal_price(
subscription, 30, db
)
if user.balance_kopeks < renewal_cost:
logger.warning(f"Недостаточно средств для автоплатежа у пользователя {user.telegram_id}")
await send_autopay_failed_notification(bot, db, subscription, renewal_cost)
return False
success = await subtract_user_balance(
db, user, renewal_cost,
f"Автопродление подписки на 30 дней"
)
if not success:
logger.error(f"Ошибка списания средств для автоплатежа у пользователя {user.telegram_id}")
await send_autopay_failed_notification(bot, db, subscription, renewal_cost)
return False
await extend_subscription(db, subscription, 30)
await subscription_service.update_remnawave_user(db, subscription)
await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=renewal_cost,
description="Автопродление подписки на 30 дней"
)
texts = get_texts(user.language)
success_text = texts.AUTOPAY_SUCCESS.format(
days=30,
amount=texts.format_price(renewal_cost),
new_end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M")
)
await bot.send_message(
chat_id=user.telegram_id,
text=success_text,
parse_mode="HTML"
)
logger.info(f"✅ Автоплатеж успешно выполнен для пользователя {user.telegram_id}")
return True
except Exception as e:
logger.error(f"❌ Ошибка обработки автоплатежа: {e}")
return False
+12 -2
View File
@@ -180,7 +180,10 @@ def get_user_management_keyboard(user_id: int, user_status: str, language: str =
InlineKeyboardButton(text="📱 Подписка", callback_data=f"admin_user_subscription_{user_id}")
],
[
InlineKeyboardButton(text="📊 Статистика", callback_data=f"admin_user_statistics_{user_id}"),
InlineKeyboardButton(text="⚙️ Настройка", callback_data=f"admin_user_servers_{user_id}"),
InlineKeyboardButton(text="📊 Статистика", callback_data=f"admin_user_statistics_{user_id}")
],
[
InlineKeyboardButton(text="📋 Транзакции", callback_data=f"admin_user_transactions_{user_id}")
]
]
@@ -578,7 +581,6 @@ def get_admin_pagination_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_maintenance_keyboard(language: str = "ru", is_active: bool = False, monitoring_active: bool = False) -> InlineKeyboardMarkup:
"""Клавиатура для управления техработами"""
if language == "en":
toggle_text = "🔴 Disable maintenance" if is_active else "🔧 Enable maintenance"
@@ -599,3 +601,11 @@ def get_maintenance_keyboard(language: str = "ru", is_active: bool = False, moni
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_sync_simplified_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
keyboard = [
[InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_all_users")],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
+191 -93
View File
@@ -1,4 +1,5 @@
from typing import List, Optional
from aiogram import types
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
@@ -20,20 +21,29 @@ def get_main_menu_keyboard(
is_admin: bool = False,
has_had_paid_subscription: bool = False,
has_active_subscription: bool = False,
subscription_is_active: bool = False
subscription_is_active: bool = False,
balance_kopeks: int = 0
) -> InlineKeyboardMarkup:
texts = get_texts(language)
if settings.DEBUG:
print(f"DEBUG KEYBOARD: language={language}, is_admin={is_admin}, has_had_paid={has_had_paid_subscription}, has_active={has_active_subscription}, sub_active={subscription_is_active}")
print(f"DEBUG KEYBOARD: language={language}, is_admin={is_admin}, has_had_paid={has_had_paid_subscription}, has_active={has_active_subscription}, sub_active={subscription_is_active}, balance={balance_kopeks}")
if hasattr(texts, 'BALANCE_BUTTON') and balance_kopeks > 0:
balance_button_text = texts.BALANCE_BUTTON.format(balance=texts.format_price(balance_kopeks))
else:
balance_button_text = f"💰 Баланс: {texts.format_price(balance_kopeks)}"
keyboard = [
[
InlineKeyboardButton(text=texts.MENU_BALANCE, callback_data="menu_balance"),
InlineKeyboardButton(text=texts.MENU_SUBSCRIPTION, callback_data="menu_subscription")
InlineKeyboardButton(text=balance_button_text, callback_data="menu_balance")
]
]
if has_active_subscription and subscription_is_active:
keyboard.append([
InlineKeyboardButton(text=texts.MENU_SUBSCRIPTION, callback_data="menu_subscription")
])
show_trial = not has_had_paid_subscription and not has_active_subscription
@@ -107,15 +117,28 @@ def get_subscription_keyboard(
connect_mode = settings.CONNECT_BUTTON_MODE
if connect_mode == "miniapp_subscription":
button_text = "🚀 Открыть в мини-приложении"
keyboard.append([
InlineKeyboardButton(
text="🚀 Открыть в мини-приложении",
web_app=types.WebAppInfo(url=subscription.subscription_url)
)
])
elif connect_mode == "miniapp_custom":
button_text = "🚀 Открыть приложение"
if settings.MINIAPP_CUSTOM_URL:
keyboard.append([
InlineKeyboardButton(
text="🚀 Открыть приложение",
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL)
)
])
else:
keyboard.append([
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
])
else:
button_text = "🔗 Подключиться"
keyboard.append([
InlineKeyboardButton(text=button_text, callback_data="subscription_connect")
])
keyboard.append([
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
])
if not is_trial and subscription and subscription.days_left <= 3:
keyboard.append([
@@ -142,27 +165,31 @@ def get_subscription_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_subscription_settings_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_subscription_settings_keyboard(language: str = "ru", show_countries_management: bool = True) -> InlineKeyboardMarkup:
from app.config import settings
texts = get_texts(language)
keyboard = [
[
keyboard = []
if show_countries_management:
keyboard.append([
InlineKeyboardButton(text="🌍 Добавить страны", callback_data="subscription_add_countries")
],
])
keyboard.extend([
[
InlineKeyboardButton(text="📱 Добавить устройства", callback_data="subscription_add_devices")
],
[
InlineKeyboardButton(text="🔄 Сбросить устройства", callback_data="subscription_reset_devices")
]
]
])
if settings.is_traffic_selectable():
keyboard.insert(1, [
keyboard.insert(-2, [
InlineKeyboardButton(text="📈 Добавить трафик", callback_data="subscription_add_traffic")
])
keyboard.insert(2, [
keyboard.insert(-2, [
InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="subscription_reset_traffic")
])
@@ -187,19 +214,25 @@ def get_subscription_period_keyboard(language: str = "ru") -> InlineKeyboardMark
texts = get_texts(language)
keyboard = []
periods = [
(14, texts.PERIOD_14_DAYS),
(30, texts.PERIOD_30_DAYS),
(60, texts.PERIOD_60_DAYS),
(90, texts.PERIOD_90_DAYS),
(180, texts.PERIOD_180_DAYS),
(360, texts.PERIOD_360_DAYS)
]
available_periods = settings.get_available_subscription_periods()
for days, text in periods:
keyboard.append([
InlineKeyboardButton(text=text, callback_data=f"period_{days}")
])
period_texts = {
14: texts.PERIOD_14_DAYS,
30: texts.PERIOD_30_DAYS,
60: texts.PERIOD_60_DAYS,
90: texts.PERIOD_90_DAYS,
180: texts.PERIOD_180_DAYS,
360: texts.PERIOD_360_DAYS
}
for days in available_periods:
if days in period_texts:
keyboard.append([
InlineKeyboardButton(
text=period_texts[days],
callback_data=f"period_{days}"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
@@ -209,28 +242,57 @@ def get_subscription_period_keyboard(language: str = "ru") -> InlineKeyboardMark
def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
import logging
logger = logging.getLogger(__name__)
from app.config import settings
if settings.is_traffic_fixed():
return get_back_keyboard(language)
logger.info(f"🔍 RAW CONFIG: '{settings.TRAFFIC_PACKAGES_CONFIG}'")
all_packages = settings.get_traffic_packages()
logger.info(f"🔍 ALL PACKAGES: {all_packages}")
enabled_packages = [pkg for pkg in all_packages if pkg['enabled']]
disabled_packages = [pkg for pkg in all_packages if not pkg['enabled']]
logger.info(f"🔍 ENABLED: {len(enabled_packages)} packages")
logger.info(f"🔍 DISABLED: {len(disabled_packages)} packages")
for pkg in disabled_packages:
logger.info(f"🔍 DISABLED PACKAGE: {pkg['gb']}GB = {pkg['price']} kopeks, enabled={pkg['enabled']}")
texts = get_texts(language)
keyboard = []
packages = [
(5, texts.TRAFFIC_5GB),
(10, texts.TRAFFIC_10GB),
(25, texts.TRAFFIC_25GB),
(50, texts.TRAFFIC_50GB),
(100, texts.TRAFFIC_100GB),
(250, texts.TRAFFIC_250GB),
(0, texts.TRAFFIC_UNLIMITED)
]
traffic_packages = settings.get_traffic_packages()
for gb, text in packages:
for package in traffic_packages:
gb = package["gb"]
price = package["price"]
enabled = package["enabled"]
if not enabled:
continue
if gb == 0:
text = f"♾️ Безлимит - {settings.format_price(package['price'])}"
else:
text = f"📊 {gb} ГБ - {settings.format_price(package['price'])}"
keyboard.append([
InlineKeyboardButton(text=text, callback_data=f"traffic_{gb}")
])
if not keyboard:
keyboard.append([
InlineKeyboardButton(
text="⚠️ Пакеты трафика не настроены",
callback_data="no_traffic_packages"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="subscription_config_back")
@@ -238,7 +300,6 @@ def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_countries_keyboard(countries: List[dict], selected: List[str], language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -281,17 +342,31 @@ def get_devices_keyboard(current: int, language: str = "ru") -> InlineKeyboardMa
texts = get_texts(language)
keyboard = []
for devices in range(1, 6):
price = (devices - 1) * settings.PRICE_PER_DEVICE
price_text = f" (+{texts.format_price(price)})" if price > 0 else ""
start_devices = settings.DEFAULT_DEVICE_LIMIT
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 50
end_devices = min(max_devices + 1, start_devices + 10)
buttons = []
for devices in range(start_devices, end_devices):
price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
price_text = f" (+{texts.format_price(price)})" if price > 0 else " (вкл.)"
emoji = "" if devices == current else ""
keyboard.append([
button_text = f"{emoji} {devices}{price_text}"
buttons.append(
InlineKeyboardButton(
text=f"{emoji} {devices} устройство{_get_device_suffix(devices)}{price_text}",
text=button_text,
callback_data=f"devices_{devices}"
)
])
)
for i in range(0, len(buttons), 2):
if i + 1 < len(buttons):
keyboard.append([buttons[i], buttons[i + 1]])
else:
keyboard.append([buttons[i]])
keyboard.extend([
[InlineKeyboardButton(text="✅ Продолжить", callback_data="devices_continue")],
@@ -300,6 +375,13 @@ def get_devices_keyboard(current: int, language: str = "ru") -> InlineKeyboardMa
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def _get_device_declension(count: int) -> str:
if count % 10 == 1 and count % 100 != 11:
return "устройство"
elif count % 10 in [2, 3, 4] and count % 100 not in [12, 13, 14]:
return "устройства"
else:
return "устройств"
def get_subscription_confirm_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
@@ -494,16 +576,6 @@ def get_pagination_keyboard(
return keyboard
def _get_device_suffix(count: int) -> str:
if count == 1:
return ""
elif 2 <= count <= 4:
return "а"
else:
return ""
def get_confirmation_keyboard(
confirm_data: str,
cancel_data: str = "cancel",
@@ -593,44 +665,68 @@ def get_add_traffic_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
packages = [
(5, f"📊 +5 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_5GB)}"),
(10, f"📊 +10 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_10GB)}"),
(25, f"📊 +25 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_25GB)}"),
(50, f"📊 +50 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_50GB)}"),
(100, f"📊 +100 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_100GB)}"),
(0, f"📊 Безлимит - {settings.format_price(settings.PRICE_TRAFFIC_UNLIMITED)}")
]
traffic_packages = settings.get_traffic_packages()
for gb, text in packages:
for package in traffic_packages:
gb = package["gb"]
price = package["price"]
enabled = package["enabled"]
if not enabled:
continue
if gb == 0:
text = f"📊 Безлимит - {settings.format_price(package['price'])}"
else:
text = f"📊 +{gb} ГБ - {settings.format_price(package['price'])}"
keyboard.append([
InlineKeyboardButton(text=text, callback_data=f"add_traffic_{gb}")
])
if not keyboard:
keyboard.append([
InlineKeyboardButton(
text="⚠️ Пакеты трафика не настроены",
callback_data="no_traffic_packages"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_add_devices_keyboard(current_devices: int, language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
max_devices = 10
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 100
for add_count in range(1, min(6, max_devices - current_devices + 1)):
max_add = min(5, max_devices - current_devices)
for add_count in range(1, max_add + 1):
price = add_count * settings.PRICE_PER_DEVICE
total_devices = current_devices + add_count
add_device_word = _get_device_declension(add_count)
keyboard.append([
InlineKeyboardButton(
text=f"📱 +{add_count} устройство{_get_device_suffix(add_count)} (итого: {total_devices}) - {settings.format_price(price)}",
text=f"📱 +{add_count} {add_device_word} (итого: {total_devices}) - {settings.format_price(price)}",
callback_data=f"add_devices_{add_count}"
)
])
if max_add == 0:
keyboard.append([
InlineKeyboardButton(
text="⚠️ Достигнут максимум устройств",
callback_data="max_devices_reached"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
])
@@ -869,29 +965,31 @@ def get_specific_app_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_extend_subscription_keyboard_with_prices(language: str, prices: dict) -> InlineKeyboardMarkup:
from app.localization.texts import get_texts
texts = get_texts(language)
keyboard = []
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text=f"📅 30 дней - {texts.format_price(prices[30])}",
callback_data="extend_period_30"
)
],
[
InlineKeyboardButton(
text=f"📅 90 дней - {texts.format_price(prices[90])}",
callback_data="extend_period_90"
)
],
[
InlineKeyboardButton(
text=f"📅 180 дней - {texts.format_price(prices[180])}",
callback_data="extend_period_180"
)
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
available_periods = settings.get_available_renewal_periods()
period_display = {
14: "14 дней",
30: "30 дней",
60: "60 дней",
90: "90 дней",
180: "180 дней",
360: "360 дней"
}
for days in available_periods:
if days in prices and days in period_display:
keyboard.append([
InlineKeyboardButton(
text=f"📅 {period_display[days]} - {texts.format_price(prices[days])}",
callback_data=f"extend_period_{days}"
)
])
keyboard.append([
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
+6 -6
View File
@@ -105,19 +105,17 @@ class RussianTexts(Texts):
REFERRAL_CODE_APPLIED = "🎁 Реферальный код применен! Вы получите бонус после первой покупки."
REFERRAL_CODE_INVALID = "❌ Неверный реферальный код"
REFERRAL_CODE_SKIP = "⏭️ Пропустить"
MAIN_MENU = """👤 <b>{user_name}</b>
💰 <b>Баланс:</b> {balance}
📱 <b>Подписка:</b> {subscription_status}
Выберите действие:
"""
MENU_BALANCE = "💰 Баланс"
MENU_SUBSCRIPTION = "📱 Подписка"
MENU_TRIAL = "🎁 Тестовая подписка"
MENU_TRIAL = "🧪 Тестовая подписка"
MENU_BUY_SUBSCRIPTION = "💎 Купить подписку"
MENU_EXTEND_SUBSCRIPTION = "⏰ Продлить подписку"
MENU_PROMOCODE = "🎫 Промокод"
@@ -126,9 +124,11 @@ class RussianTexts(Texts):
MENU_RULES = "📋 Правила сервиса"
MENU_LANGUAGE = "🌐 Язык"
MENU_ADMIN = "⚙️ Админ-панель"
BALANCE_BUTTON = "💰 Баланс: {balance}"
BALANCE_BUTTON_ZERO = "💰 Баланс: 0.00 ₽"
SUBSCRIPTION_NONE = "❌ Нет активной подписки"
SUBSCRIPTION_TRIAL = "🎁 Тестовая подписка"
SUBSCRIPTION_TRIAL = "🧪 Тестовая подписка"
SUBSCRIPTION_ACTIVE = "✅ Активна"
SUBSCRIPTION_EXPIRED = "⏰ Истекла"
+28 -2
View File
@@ -1,4 +1,5 @@
import logging
from datetime import datetime
from typing import Callable, Dict, Any, Awaitable
from aiogram import BaseMiddleware
from aiogram.types import Message, CallbackQuery, TelegramObject, User as TgUser
@@ -56,7 +57,7 @@ class AuthMiddleware(BaseMiddleware):
)
if is_registration_process:
logger.info(f"📝 Пропускаем пользователя {user.id} в процессе регистрации")
logger.info(f"🔍 Пропускаем пользователя {user.id} в процессе регистрации")
data['db'] = db
data['db_user'] = None
data['is_admin'] = False
@@ -124,8 +125,33 @@ class AuthMiddleware(BaseMiddleware):
logger.info(f"❌ Удаленный пользователь {user.id} попытался использовать бота без /start")
return
from datetime import datetime
profile_updated = False
if db_user.username != user.username:
old_username = db_user.username
db_user.username = user.username
logger.info(f"📝 [Middleware] Username обновлен для {user.id}: '{old_username}''{db_user.username}'")
profile_updated = True
if db_user.first_name != user.first_name:
old_first_name = db_user.first_name
db_user.first_name = user.first_name
logger.info(f"📝 [Middleware] Имя обновлено для {user.id}: '{old_first_name}''{db_user.first_name}'")
profile_updated = True
if db_user.last_name != user.last_name:
old_last_name = db_user.last_name
db_user.last_name = user.last_name
logger.info(f"📝 [Middleware] Фамилия обновлена для {user.id}: '{old_last_name}''{db_user.last_name}'")
profile_updated = True
db_user.last_activity = datetime.utcnow()
if profile_updated:
db_user.updated_at = datetime.utcnow()
logger.info(f"💾 [Middleware] Профиль пользователя {user.id} обновлен в middleware")
await db.commit()
data['db'] = db
+142 -4
View File
@@ -28,8 +28,15 @@ class MaintenanceService:
self._status = MaintenanceStatus(is_active=False)
self._check_task: Optional[asyncio.Task] = None
self._is_checking = False
self._max_consecutive_failures = 3
self._max_consecutive_failures = 3
self._bot = None
self._last_notification_sent = None
def set_bot(self, bot):
"""Устанавливает ссылку на бота для отправки уведомлений"""
self._bot = bot
logger.info("Бот установлен для maintenance_service")
@property
def status(self) -> MaintenanceStatus:
return self._status
@@ -51,6 +58,51 @@ class MaintenanceService:
else:
return settings.get_maintenance_message()
async def _notify_admins(self, message: str, alert_type: str = "info"):
"""Отправка уведомлений администраторам"""
if not self._bot:
logger.warning("Бот не установлен, уведомления не могут быть отправлены")
return
cache_key = f"maintenance_notification_{alert_type}"
if await cache.get(cache_key):
return
admin_ids = settings.get_admin_ids()
if not admin_ids:
logger.warning("Список администраторов пуст")
return
emoji_map = {
"error": "🚨",
"warning": "⚠️",
"success": "",
"info": ""
}
emoji = emoji_map.get(alert_type, "")
formatted_message = f"{emoji} <b>Maintenance Service</b>\n\n{message}"
success_count = 0
for admin_id in admin_ids:
try:
await self._bot.send_message(
chat_id=admin_id,
text=formatted_message,
parse_mode="HTML"
)
success_count += 1
await asyncio.sleep(0.1)
except Exception as e:
logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
if success_count > 0:
logger.info(f"Уведомление отправлено {success_count} администраторам")
await cache.set(cache_key, True, expire=300)
else:
logger.error("Не удалось отправить уведомления ни одному администратору")
async def enable_maintenance(self, reason: Optional[str] = None, auto: bool = False) -> bool:
try:
if self._status.is_active:
@@ -64,6 +116,18 @@ class MaintenanceService:
await self._save_status_to_cache()
notification_msg = f"""
Режим технических работ ВКЛЮЧЕН
📋 <b>Причина:</b> {self._status.reason}
🤖 <b>Автоматически:</b> {'Да' if auto else 'Нет'}
🕐 <b>Время:</b> {self._status.enabled_at.strftime('%d.%m.%Y %H:%M:%S')}
Обычные пользователи временно не смогут использовать бота.
"""
await self._notify_admins(notification_msg, "warning" if auto else "info")
logger.warning(f"🔧 Режим техработ ВКЛЮЧЕН. Причина: {self._status.reason}")
return True
@@ -77,6 +141,11 @@ class MaintenanceService:
logger.info("Режим техработ уже выключен")
return True
was_auto = self._status.auto_enabled
duration = None
if self._status.enabled_at:
duration = datetime.utcnow() - self._status.enabled_at
self._status.is_active = False
self._status.enabled_at = None
self._status.reason = None
@@ -85,6 +154,27 @@ class MaintenanceService:
await self._save_status_to_cache()
duration_str = ""
if duration:
hours = int(duration.total_seconds() // 3600)
minutes = int((duration.total_seconds() % 3600) // 60)
if hours > 0:
duration_str = f"\n⏱️ <b>Длительность:</b> {hours}ч {minutes}мин"
else:
duration_str = f"\n⏱️ <b>Длительность:</b> {minutes}мин"
notification_msg = f"""
Режим технических работ ВЫКЛЮЧЕН
🤖 <b>Автоматически:</b> {'Да' if was_auto else 'Нет'}
🕐 <b>Время:</b> {datetime.utcnow().strftime('%d.%m.%Y %H:%M:%S')}
{duration_str}
Сервис снова доступен для пользователей.
"""
await self._notify_admins(notification_msg, "success")
logger.info("✅ Режим техработ ВЫКЛЮЧЕН")
return True
@@ -102,6 +192,17 @@ class MaintenanceService:
self._check_task = asyncio.create_task(self._monitoring_loop())
logger.info(f"🔄 Запущен мониторинг API RemnaWave (интервал: {settings.get_maintenance_check_interval()}с)")
await self._notify_admins(f"""
Мониторинг технических работ запущен
🔄 <b>Интервал проверки:</b> {settings.get_maintenance_check_interval()} секунд
🤖 <b>Автовключение:</b> {'Включено' if settings.is_maintenance_auto_enable() else 'Отключено'}
🎯 <b>Порог ошибок:</b> {self._max_consecutive_failures}
Система будет следить за доступностью API.
""", "info")
return True
except Exception as e:
@@ -117,7 +218,8 @@ class MaintenanceService:
except asyncio.CancelledError:
pass
logger.info("⏹️ Мониторинг API остановлен")
await self._notify_admins("Мониторинг технических работ остановлен", "info")
logger.info("ℹ️ Мониторинг API остановлен")
return True
except Exception as e:
@@ -138,6 +240,18 @@ class MaintenanceService:
is_connected = await test_api_connection(api)
if is_connected:
# API восстановилось
if not self._status.api_status:
await self._notify_admins(f"""
API RemnaWave восстановлено!
<b>Статус:</b> Доступно
🕐 <b>Время восстановления:</b> {self._status.last_check.strftime('%H:%M:%S')}
🔄 <b>Неудачных попыток было:</b> {self._status.consecutive_failures}
API снова отвечает на запросы.
""", "success")
self._status.api_status = True
self._status.consecutive_failures = 0
@@ -147,9 +261,21 @@ class MaintenanceService:
return True
else:
was_available = self._status.api_status
self._status.api_status = False
self._status.consecutive_failures += 1
if was_available:
await self._notify_admins(f"""
API RemnaWave недоступно!
<b>Статус:</b> Недоступно
🕐 <b>Время обнаружения:</b> {self._status.last_check.strftime('%H:%M:%S')}
🔄 <b>Попытка:</b> {self._status.consecutive_failures}
Началась серия неудачных проверок API.
""", "error")
if (self._status.consecutive_failures >= self._max_consecutive_failures and
not self._status.is_active and
settings.is_maintenance_auto_enable()):
@@ -163,6 +289,17 @@ class MaintenanceService:
except Exception as e:
logger.error(f"Ошибка проверки API: {e}")
if self._status.api_status:
await self._notify_admins(f"""
Ошибка при проверке API RemnaWave
<b>Ошибка:</b> {str(e)}
🕐 <b>Время:</b> {datetime.utcnow().strftime('%H:%M:%S')}
Не удалось выполнить проверку доступности API.
""", "error")
self._status.api_status = False
self._status.consecutive_failures += 1
return False
@@ -216,7 +353,7 @@ class MaintenanceService:
if status_data.get("last_check"):
self._status.last_check = datetime.fromisoformat(status_data["last_check"])
logger.info(f"📥 Состояние техработ загружено из кеша: активен={self._status.is_active}")
logger.info(f"🔥 Состояние техработ загружено из кеша: активен={self._status.is_active}")
except Exception as e:
logger.error(f"Ошибка загрузки состояния из кеша: {e}")
@@ -232,7 +369,8 @@ class MaintenanceService:
"consecutive_failures": self._status.consecutive_failures,
"monitoring_active": self._check_task is not None and not self._check_task.done(),
"auto_enable_configured": settings.is_maintenance_auto_enable(),
"check_interval": settings.get_maintenance_check_interval()
"check_interval": settings.get_maintenance_check_interval(),
"bot_connected": self._bot is not None
}
async def force_api_check(self) -> Dict[str, Any]:
+82 -46
View File
@@ -37,6 +37,7 @@ class MonitoringService:
self.payment_service = PaymentService()
self.bot = bot
self._notified_users: Set[str] = set()
self._last_cleanup = datetime.utcnow()
async def start_monitoring(self):
if self.is_running:
@@ -62,6 +63,8 @@ class MonitoringService:
async def _monitoring_cycle(self):
async for db in get_db():
try:
await self._cleanup_notification_cache()
await self._check_expired_subscriptions(db)
await self._check_expiring_subscriptions(db)
await self._check_trial_expiring_soon(db)
@@ -69,10 +72,6 @@ class MonitoringService:
await self._cleanup_inactive_users(db)
await self._sync_with_remnawave(db)
current_hour = datetime.utcnow().hour
if current_hour == 0:
self._notified_users.clear()
await self._log_monitoring_event(
db, "monitoring_cycle_completed",
"Цикл мониторинга успешно завершен",
@@ -90,6 +89,15 @@ class MonitoringService:
finally:
break
async def _cleanup_notification_cache(self):
current_time = datetime.utcnow()
if (current_time - self._last_cleanup).total_seconds() >= 3600:
old_count = len(self._notified_users)
self._notified_users.clear()
self._last_cleanup = current_time
logger.info(f"🧹 Очищен кеш уведомлений ({old_count} записей)")
async def _check_expired_subscriptions(self, db: AsyncSession):
try:
expired_subscriptions = await get_expired_subscriptions(db)
@@ -168,30 +176,52 @@ class MonitoringService:
async def _check_expiring_subscriptions(self, db: AsyncSession):
try:
warning_days = settings.get_autopay_warning_days()
all_processed_users = set()
for days in warning_days:
expiring_subscriptions = await self._get_expiring_paid_subscriptions(db, days)
sent_count = 0
for subscription in expiring_subscriptions:
user = await get_user_by_id(db, subscription.user_id)
if not user:
continue
notification_key = f"expiring_{user.telegram_id}_{days}d"
if notification_key in self._notified_users:
continue
notification_key = f"expiring_{user.telegram_id}_{days}d_{subscription.id}"
user_key = f"user_{user.telegram_id}_today"
if (notification_key in self._notified_users or
user_key in all_processed_users):
logger.debug(f"🔄 Пропускаем дублирование для пользователя {user.telegram_id} на {days} дней")
continue
should_send = True
for other_days in warning_days:
if other_days < days:
other_subs = await self._get_expiring_paid_subscriptions(db, other_days)
if any(s.user_id == user.id for s in other_subs):
should_send = False
logger.debug(f"🎯 Пропускаем уведомление на {days} дней для пользователя {user.telegram_id}, есть более срочное на {other_days} дней")
break
if not should_send:
continue
if self.bot:
await self._send_subscription_expiring_notification(user, subscription, days)
self._notified_users.add(notification_key)
logger.info(f"⚠️ Пользователю {user.telegram_id} отправлено уведомление об истечении подписки через {days} дней")
success = await self._send_subscription_expiring_notification(user, subscription, days)
if success:
self._notified_users.add(notification_key)
all_processed_users.add(user_key)
sent_count += 1
logger.info(f"✅ Пользователю {user.telegram_id} отправлено уведомление об истечении подписки через {days} дней")
else:
logger.warning(f"❌ Не удалось отправить уведомление пользователю {user.telegram_id}")
if expiring_subscriptions:
if sent_count > 0:
await self._log_monitoring_event(
db, "expiring_notifications_sent",
f"Отправлено {len(expiring_subscriptions)} уведомлений об истечении через {days} дней",
{"days": days, "count": len(expiring_subscriptions)}
f"Отправлено {sent_count} уведомлений об истечении через {days} дней",
{"days": days, "count": sent_count}
)
except Exception as e:
@@ -220,15 +250,15 @@ class MonitoringService:
if not user:
continue
notification_key = f"trial_2h_{user.telegram_id}"
notification_key = f"trial_2h_{user.telegram_id}_{subscription.id}"
if notification_key in self._notified_users:
continue
if self.bot:
await self._send_trial_ending_notification(user, subscription)
self._notified_users.add(notification_key)
logger.info(f"🎁 Пользователю {user.telegram_id} отправлено уведомление об окончании тестовой подписки через 2 часа")
success = await self._send_trial_ending_notification(user, subscription)
if success:
self._notified_users.add(notification_key)
logger.info(f"🎁 Пользователю {user.telegram_id} отправлено уведомление об окончании тестовой подписки через 2 часа")
if trial_expiring:
await self._log_monitoring_event(
@@ -257,9 +287,9 @@ class MonitoringService:
)
)
logger.info(f"🔍 Поиск платных подписок, истекающих в ближайшие {days_before} дней")
logger.info(f"📅 Текущее время: {current_time}")
logger.info(f"📅 Пороговая дата: {threshold_date}")
logger.debug(f"🔍 Поиск платных подписок, истекающих в ближайшие {days_before} дней")
logger.debug(f"📅 Текущее время: {current_time}")
logger.debug(f"📅 Пороговая дата: {threshold_date}")
subscriptions = result.scalars().all()
logger.info(f"📊 Найдено {len(subscriptions)} платных подписок для уведомлений")
@@ -340,15 +370,15 @@ class MonitoringService:
except Exception as e:
logger.error(f"Ошибка обработки автоплатежей: {e}")
async def _send_subscription_expired_notification(self, user: User):
async def _send_subscription_expired_notification(self, user: User) -> bool:
try:
message = """
<b>Подписка истекла</b>
<b>Подписка истекла</b>
Ваша подписка истекла. Для восстановления доступа продлите подписку.
Ваша подписка истекла. Для восстановления доступа продлите подписку.
🔧 Доступ к серверам заблокирован до продления.
"""
🔧 Доступ к серверам заблокирован до продления.
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
@@ -363,11 +393,13 @@ class MonitoringService:
parse_mode="HTML",
reply_markup=keyboard
)
return True
except Exception as e:
logger.error(f"Ошибка отправки уведомления об истечении подписки пользователю {user.telegram_id}: {e}")
return False
async def _send_subscription_expiring_notification(self, user: User, subscription: Subscription, days: int):
async def _send_subscription_expiring_notification(self, user: User, subscription: Subscription, days: int) -> bool:
try:
from app.utils.formatters import format_days_declension
@@ -382,14 +414,14 @@ class MonitoringService:
action_text = "💡 Включите автоплатеж или продлите подписку вручную"
message = f"""
<b>Подписка истекает через {days_text}!</b>
<b>Подписка истекает через {days_text}!</b>
Ваша платная подписка истекает {subscription.end_date.strftime("%d.%m.%Y %H:%M")}.
Ваша платная подписка истекает {subscription.end_date.strftime("%d.%m.%Y %H:%M")}.
💳 <b>Автоплатеж:</b> {autopay_status}
💳 <b>Автоплатеж:</b> {autopay_status}
{action_text}
"""
{action_text}
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
@@ -405,30 +437,32 @@ class MonitoringService:
parse_mode="HTML",
reply_markup=keyboard
)
return True
except Exception as e:
logger.error(f"Ошибка отправки уведомления об истечении подписки пользователю {user.telegram_id}: {e}")
return False
async def _send_trial_ending_notification(self, user: User, subscription: Subscription):
async def _send_trial_ending_notification(self, user: User, subscription: Subscription) -> bool:
try:
texts = get_texts(user.language)
message = f"""
🎁 <b>Тестовая подписка скоро закончится!</b>
🎁 <b>Тестовая подписка скоро закончится!</b>
Ваша тестовая подписка истекает через 2 часа.
Ваша тестовая подписка истекает через 2 часа.
💎 <b>Не хотите остаться без VPN?</b>
Переходите на полную подписку со скидкой!
💎 <b>Не хотите остаться без VPN?</b>
Переходите на полную подписку со скидкой!
🔥 <b>Специальное предложение:</b>
30 дней всего за {settings.format_price(settings.PRICE_30_DAYS)}
Безлимитный трафик
Все серверы доступны
Поддержка до 3 устройств
🔥 <b>Специальное предложение:</b>
30 дней всего за {settings.format_price(settings.PRICE_30_DAYS)}
Безлимитный трафик
Все серверы доступны
Поддержка до 3 устройств
Успейте оформить до окончания тестового периода!
"""
Успейте оформить до окончания тестового периода!
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
@@ -443,9 +477,11 @@ class MonitoringService:
parse_mode="HTML",
reply_markup=keyboard
)
return True
except Exception as e:
logger.error(f"Ошибка отправки уведомления об окончании тестовой подписки пользователю {user.telegram_id}: {e}")
return False
async def _send_autopay_success_notification(self, user: User, amount: int, days: int):
try:
@@ -554,7 +590,7 @@ class MonitoringService:
except Exception as e:
logger.error(f"Ошибка логирования события мониторинга: {e}")
async def get_monitoring_status(self, db: AsyncSession) -> Dict[str, Any]:
try:
from sqlalchemy import select, desc
+95 -24
View File
@@ -9,9 +9,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.services.yookassa_service import YooKassaService
from app.external.telegram_stars import TelegramStarsService
from app.database.crud.yookassa import create_yookassa_payment, link_yookassa_payment_to_transaction
from app.database.crud.transaction import create_transaction
from app.database.crud.user import add_user_balance
from app.database.crud.user import add_user_balance, get_user_by_id
from app.database.models import TransactionType, PaymentMethod
logger = logging.getLogger(__name__)
@@ -22,6 +23,7 @@ class PaymentService:
def __init__(self, bot: Optional[Bot] = None):
self.bot = bot
self.yookassa_service = YooKassaService() if settings.is_yookassa_enabled() else None
self.stars_service = TelegramStarsService(bot) if bot else None
async def create_stars_invoice(
self,
@@ -30,28 +32,90 @@ class PaymentService:
payload: Optional[str] = None
) -> str:
if not self.bot:
if not self.bot or not self.stars_service:
raise ValueError("Bot instance required for Stars payments")
try:
stars_amount = max(1, amount_kopeks // 100)
amount_rubles = amount_kopeks / 100
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
invoice_link = await self.bot.create_invoice_link(
title="Пополнение баланса VPN",
description=description,
description=f"{description} (≈{stars_amount} ⭐)",
payload=payload or f"balance_topup_{amount_kopeks}",
provider_token="",
currency="XTR",
prices=[LabeledPrice(label="Пополнение", amount=stars_amount)]
)
logger.info(f"Создан Stars invoice на {stars_amount} звезд")
logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{amount_rubles:.2f}₽)")
return invoice_link
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
raise
async def process_stars_payment(
self,
db: AsyncSession,
user_id: int,
stars_amount: int,
payload: str,
telegram_payment_charge_id: str
) -> bool:
try:
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(stars_amount)
amount_kopeks = int(rubles_amount * 100)
transaction = await create_transaction(
db=db,
user_id=user_id,
type=TransactionType.DEPOSIT,
amount_kopeks=amount_kopeks,
description=f"Пополнение через Telegram Stars ({stars_amount} ⭐)",
payment_method=PaymentMethod.TELEGRAM_STARS,
external_id=telegram_payment_charge_id,
is_completed=True
)
user = await get_user_by_id(db, user_id)
if user:
await add_user_balance(
db,
user,
amount_kopeks,
f"Пополнение Stars: {rubles_amount:.2f}₽ ({stars_amount} ⭐)"
)
if self.bot:
try:
await self.bot.send_message(
user.telegram_id,
f"✅ <b>Пополнение успешно!</b>\n\n"
f"⭐ Звезд: {stars_amount}\n"
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
f"🏦 Способ: Telegram Stars\n"
f"🆔 Транзакция: {telegram_payment_charge_id[:8]}...\n\n"
f"Баланс пополнен автоматически!",
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {rubles_amount:.2f}")
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
logger.info(
f"✅ Обработан Stars платеж: пользователь {user_id}, "
f"{stars_amount} звезд → {rubles_amount:.2f}"
)
return True
else:
logger.error(f"Пользователь с ID {user_id} не найден при обработке Stars платежа")
return False
except Exception as e:
logger.error(f"Ошибка обработки Stars платежа: {e}", exc_info=True)
return False
async def create_yookassa_payment(
self,
db: AsyncSession,
@@ -186,22 +250,27 @@ class PaymentService:
db, yookassa_payment_id, transaction.id
)
from app.database.crud.user import add_user_balance
await add_user_balance(db, updated_payment.user_id, updated_payment.amount_kopeks)
if self.bot:
try:
await self.bot.send_message(
updated_payment.user.telegram_id,
f"✅ <b>Пополнение успешно!</b>\n\n"
f"💰 Сумма: {settings.format_price(updated_payment.amount_kopeks)}\n"
f"🏦 Способ: Банковская карта\n"
f"🆔 Транзакция: {yookassa_payment_id[:8]}...\n\n"
f"Баланс пополнен автоматически!",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении: {e}")
user = await get_user_by_id(db, updated_payment.user_id)
if user:
await add_user_balance(db, user, updated_payment.amount_kopeks, f"Пополнение YooKassa: {updated_payment.amount_kopeks/100:.2f}")
if self.bot:
try:
await self.bot.send_message(
user.telegram_id,
f"✅ <b>Пополнение успешно!</b>\n\n"
f"💰 Сумма: {settings.format_price(updated_payment.amount_kopeks)}\n"
f"🦐 Способ: Банковская карта\n"
f"🆔 Транзакция: {yookassa_payment_id[:8]}...\n\n"
f"Баланс пополнен автоматически!",
parse_mode="HTML"
)
logger.info(f"Отправлено уведомление пользователю {user.telegram_id} о пополнении на {updated_payment.amount_kopeks/100:.2f}")
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении: {e}")
else:
logger.error(f"Пользователь с ID {updated_payment.user_id} не найден при пополнении баланса")
return False
return True
@@ -233,15 +302,17 @@ class PaymentService:
transaction_id=transaction.id
)
await add_user_balance(db, payment.user_id, payment.amount_kopeks)
user = await get_user_by_id(db, payment.user_id)
if user:
await add_user_balance(db, user, payment.amount_kopeks, f"Пополнение YooKassa: {payment.amount_kopeks/100:.2f}")
logger.info(f"Успешно обработан платеж YooKassa {payment.yookassa_payment_id}: "
f"пользователь {payment.user_id} получил {payment.amount_kopeks/100}")
if self.bot:
if self.bot and user:
try:
await self._send_payment_success_notification(
payment.user.telegram_id,
user.telegram_id,
payment.amount_kopeks
)
except Exception as e:
+21 -9
View File
@@ -20,7 +20,7 @@ async def process_referral_registration(
referrer = await get_user_by_id(db, referrer_id)
if not new_user or not referrer:
logger.error(f"Пользователи не найдены: {new_user_id}, {referrer_id}")
logger.error(f"Пользователи не найдены: new_user_id={new_user_id}, referrer_id={referrer_id}")
return False
if new_user.referred_by_id != referrer_id:
@@ -81,9 +81,13 @@ async def process_referral_purchase(
logger.info(f"🔍 Покупка реферала {user_id}: первая = {is_first_purchase}, сумма = {purchase_amount_kopeks/100}")
if is_first_purchase:
if is_first_purchase and settings.REFERRAL_REGISTRATION_REWARD > 0:
reward_amount = settings.REFERRAL_REGISTRATION_REWARD
if reward_amount > 1000000:
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: reward_amount = {reward_amount} слишком большой! Проверьте настройки REFERRAL_REGISTRATION_REWARD")
reward_amount = 10000
await add_user_balance(
db, referrer, reward_amount,
f"Реферальная награда за первую покупку {user.full_name}"
@@ -100,12 +104,18 @@ async def process_referral_purchase(
logger.info(f"🎉 Первая покупка реферала: {referrer.telegram_id} получил {reward_amount/100}")
commission_amount = int(purchase_amount_kopeks * settings.REFERRAL_COMMISSION_PERCENT / 100)
if not (0 <= settings.REFERRAL_COMMISSION_PERCENT <= 100):
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: REFERRAL_COMMISSION_PERCENT = {settings.REFERRAL_COMMISSION_PERCENT} некорректный! Должен быть от 0 до 100")
commission_percent = 10
else:
commission_percent = settings.REFERRAL_COMMISSION_PERCENT
commission_amount = int(purchase_amount_kopeks * commission_percent / 100)
if commission_amount > 0:
await add_user_balance(
db, referrer, commission_amount,
f"Комиссия {settings.REFERRAL_COMMISSION_PERCENT}% с покупки {user.full_name}"
f"Комиссия {commission_percent}% с покупки {user.full_name}"
)
await create_referral_earning(
@@ -128,6 +138,8 @@ async def process_referral_purchase(
except Exception as e:
logger.error(f"Ошибка обработки покупки реферала: {e}")
import traceback
logger.error(f"Полный traceback: {traceback.format_exc()}")
return False
@@ -141,7 +153,7 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
invited_count_result = await db.execute(
select(func.count(User.id)).where(User.referred_by_id == user_id)
)
invited_count = invited_count_result.scalar()
invited_count = invited_count_result.scalar() or 0
paid_referrals_result = await db.execute(
select(func.count(User.id)).where(
@@ -149,13 +161,13 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
User.has_had_paid_subscription == True
)
)
paid_referrals_count = paid_referrals_result.scalar()
paid_referrals_count = paid_referrals_result.scalar() or 0
total_earned = await get_referral_earnings_sum(db, user_id)
total_earned = await get_referral_earnings_sum(db, user_id) or 0
from datetime import datetime, timedelta
month_ago = datetime.utcnow() - timedelta(days=30)
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago)
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago) or 0
return {
"invited_count": invited_count,
@@ -171,4 +183,4 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
"paid_referrals_count": 0,
"total_earned_kopeks": 0,
"month_earned_kopeks": 0
}
}
+189 -199
View File
@@ -2,6 +2,8 @@ import logging
from typing import Dict, List, Any, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete
from datetime import datetime, timedelta
import re
from app.config import settings
from app.external.remnawave_api import (
@@ -23,8 +25,37 @@ class RemnaWaveService:
def __init__(self):
self.api = RemnaWaveAPI(
base_url=settings.REMNAWAVE_API_URL,
api_key=settings.REMNAWAVE_API_KEY
api_key=settings.REMNAWAVE_API_KEY,
secret_key=settings.REMNAWAVE_SECRET_KEY
)
def _parse_remnawave_date(self, date_str: str) -> datetime:
if not date_str:
return datetime.utcnow() + timedelta(days=30)
try:
cleaned_date = date_str.strip()
if cleaned_date.endswith('Z'):
cleaned_date = cleaned_date[:-1] + '+00:00'
if '+00:00+00:00' in cleaned_date:
cleaned_date = cleaned_date.replace('+00:00+00:00', '+00:00')
cleaned_date = re.sub(r'(\+\d{2}:\d{2})\+\d{2}:\d{2}$', r'\1', cleaned_date)
parsed_date = datetime.fromisoformat(cleaned_date)
if parsed_date.tzinfo is not None:
parsed_date = parsed_date.replace(tzinfo=None)
logger.debug(f"Успешно распарсена дата: {date_str} -> {parsed_date}")
return parsed_date
except Exception as e:
logger.warning(f"⚠️ Не удалось распарсить дату '{date_str}': {e}. Используем дефолтную дату.")
return datetime.utcnow() + timedelta(days=30)
async def get_system_statistics(self) -> Dict[str, Any]:
try:
@@ -58,7 +89,6 @@ class RemnaWaveService:
logger.error(f"Ошибка получения статистики нод: {e}")
nodes_stats = {}
from datetime import datetime
total_download = sum(node.get('downloadBytes', 0) for node in realtime_usage)
total_upload = sum(node.get('uploadBytes', 0) for node in realtime_usage)
@@ -199,7 +229,6 @@ class RemnaWaveService:
'GBPS': 1024 ** 3
}
import re
match = re.match(r'([0-9.,]+)([A-Z]+)', bandwidth_str)
if match:
value_str = match.group(1).replace(',', '.')
@@ -397,212 +426,185 @@ class RemnaWaveService:
async def sync_users_from_panel(self, db: AsyncSession, sync_type: str = "all") -> Dict[str, int]:
try:
stats = {"created": 0, "updated": 0, "errors": 0, "deleted": 0}
logger.info(f"🔄 Начинаем синхронизацию типа: {sync_type}")
async with self.api as api:
panel_users_data = await api._make_request('GET', '/api/users')
panel_users = panel_users_data['response']['users']
logger.info(f"👥 Найдено пользователей в панели: {len(panel_users)}")
logger.info(f"🔄 Начинаем синхронизацию типа: {sync_type}")
async with self.api as api:
panel_users = []
start = 0
size = 100
bot_users = await get_users_list(db, offset=0, limit=10000)
bot_users_by_telegram_id = {user.telegram_id: user for user in bot_users}
panel_telegram_ids = set()
for i, panel_user in enumerate(panel_users):
try:
telegram_id = panel_user.get('telegramId')
if not telegram_id:
logger.debug(f"➡️ Пропускаем пользователя без telegram_id")
continue
panel_telegram_ids.add(telegram_id)
logger.info(f"🔄 Обрабатываем пользователя {i+1}/{len(panel_users)}: {telegram_id}")
while True:
logger.info(f"📥 Загружаем пользователей: start={start}, size={size}")
db_user = bot_users_by_telegram_id.get(telegram_id)
response = await api.get_all_users(start=start, size=size)
users_batch = response['users']
total_users = response['total']
if not db_user:
if sync_type in ["new_only", "all"]:
logger.info(f"🆕 Создание пользователя для telegram_id {telegram_id}")
from app.database.crud.user import create_user
logger.info(f"📊 Получено {len(users_batch)} пользователей из {total_users}")
for user_obj in users_batch:
user_dict = {
'uuid': user_obj.uuid,
'shortUuid': user_obj.short_uuid,
'username': user_obj.username,
'status': user_obj.status.value,
'telegramId': user_obj.telegram_id,
'expireAt': user_obj.expire_at.isoformat() + 'Z',
'trafficLimitBytes': user_obj.traffic_limit_bytes,
'usedTrafficBytes': user_obj.used_traffic_bytes,
'hwidDeviceLimit': user_obj.hwid_device_limit,
'subscriptionUrl': user_obj.subscription_url,
'activeInternalSquads': user_obj.active_internal_squads
}
panel_users.append(user_dict)
if len(users_batch) < size:
break
start += size
if start > total_users:
break
logger.info(f"✅ Всего загружено пользователей из панели: {len(panel_users)}")
bot_users = await get_users_list(db, offset=0, limit=10000)
bot_users_by_telegram_id = {user.telegram_id: user for user in bot_users}
logger.info(f"📊 Пользователей в боте: {len(bot_users)}")
panel_users_with_tg = [
user for user in panel_users
if user.get('telegramId') is not None
]
logger.info(f"📊 Пользователей в панели с Telegram ID: {len(panel_users_with_tg)}")
panel_telegram_ids = set()
for i, panel_user in enumerate(panel_users_with_tg):
try:
telegram_id = panel_user.get('telegramId')
if not telegram_id:
continue
panel_telegram_ids.add(telegram_id)
if (i + 1) % 10 == 0:
logger.info(f"🔄 Обрабатываем пользователя {i+1}/{len(panel_users_with_tg)}: {telegram_id}")
db_user = bot_users_by_telegram_id.get(telegram_id)
if not db_user:
if sync_type in ["new_only", "all"]:
logger.info(f"🆕 Создание пользователя для telegram_id {telegram_id}")
db_user = await create_user(
db=db,
telegram_id=telegram_id,
username=panel_user.get('username') or f"user_{telegram_id}",
first_name=f"Panel User {telegram_id}",
language="ru"
)
from app.database.crud.user import create_user
db_user = await create_user(
db=db,
telegram_id=telegram_id,
username=panel_user.get('username') or f"user_{telegram_id}",
first_name=f"Panel User {telegram_id}",
language="ru"
)
await update_user(db, db_user, remnawave_uuid=panel_user.get('uuid'))
await self._create_subscription_from_panel_data(db, db_user, panel_user)
stats["created"] += 1
logger.info(f"✅ Создан пользователь {telegram_id} с подпиской")
else:
if sync_type in ["update_only", "all"]:
logger.debug(f"🔄 Обновление пользователя {telegram_id}")
if not db_user.remnawave_uuid:
await update_user(db, db_user, remnawave_uuid=panel_user.get('uuid'))
await self._create_subscription_from_panel_data(db, db_user, panel_user)
await self._update_subscription_from_panel_data(db, db_user, panel_user)
stats["created"] += 1
logger.info(f"Создан пользователь {telegram_id} с подпиской")
stats["updated"] += 1
logger.debug(f"Обновлён пользователь {telegram_id}")
else:
if sync_type in ["update_only", "all"]:
logger.debug(f"🔄 Обновление пользователя {telegram_id}")
if not db_user.remnawave_uuid:
await update_user(db, db_user, remnawave_uuid=panel_user.get('uuid'))
await self._update_subscription_from_panel_data(db, db_user, panel_user)
stats["updated"] += 1
logger.debug(f"✅ Обновлён пользователь {telegram_id}")
except Exception as user_error:
logger.error(f"❌ Ошибка обработки пользователя {telegram_id}: {user_error}")
stats["errors"] += 1
continue
except Exception as user_error:
logger.error(f"❌ Ошибка обработки пользователя {telegram_id}: {user_error}")
stats["errors"] += 1
continue
if sync_type == "all":
logger.info("🗑️ Деактивация подписок пользователей, отсутствующих в панели...")
if sync_type == "all":
logger.info("🗑️ ПОЛНАЯ очистка подписок пользователей, отсутствующих в панели...")
for telegram_id, db_user in bot_users_by_telegram_id.items():
if telegram_id not in panel_telegram_ids and db_user.subscription:
try:
logger.info(f"🗑️ ПОЛНАЯ очистка данных подписки пользователя {telegram_id} (нет в панели)")
subscription = db_user.subscription
if db_user.remnawave_uuid:
try:
for telegram_id, db_user in bot_users_by_telegram_id.items():
if telegram_id not in panel_telegram_ids and db_user.subscription:
try:
logger.info(f"🗑️ Деактивация подписки пользователя {telegram_id} (нет в панели)")
subscription = db_user.subscription
if db_user.remnawave_uuid:
try:
async with self.api as api:
devices_reset = await api.reset_user_devices(db_user.remnawave_uuid)
if devices_reset:
logger.info(f"🔧 Сброшены HWID устройства для пользователя {telegram_id}")
else:
logger.warning(f"⚠️ Не удалось сбросить HWID устройства для пользователя {telegram_id}")
except Exception as hwid_error:
logger.error(f"❌ Ошибка сброса HWID устройств для {telegram_id}: {hwid_error}")
except Exception as hwid_error:
logger.error(f"❌ Ошибка сброса HWID устройств для {telegram_id}: {hwid_error}")
try:
from sqlalchemy import delete
from app.database.models import SubscriptionServer
try:
from app.database.crud.subscription import get_subscription_server_ids, remove_subscription_servers
from sqlalchemy import delete
from app.database.models import SubscriptionServer
await db.execute(
delete(SubscriptionServer).where(
SubscriptionServer.subscription_id == subscription.id
)
await db.execute(
delete(SubscriptionServer).where(
SubscriptionServer.subscription_id == subscription.id
)
logger.info(f"🗑️ УДАЛЕНЫ ВСЕ записи SubscriptionServer для подписки {subscription.id}")
except Exception as servers_error:
logger.warning(f"⚠️ Не удалось удалить серверы подписки: {servers_error}")
try:
from sqlalchemy import delete
from app.database.models import Transaction
await db.execute(
delete(Transaction).where(Transaction.user_id == db_user.id)
)
logger.info(f"🗑️ УДАЛЕНЫ ВСЕ транзакции пользователя {telegram_id}")
except Exception as transactions_error:
logger.warning(f"⚠️ Не удалось удалить транзакции: {transactions_error}")
try:
from sqlalchemy import delete
from app.database.models import ReferralEarning
await db.execute(
delete(ReferralEarning).where(ReferralEarning.user_id == db_user.id)
)
await db.execute(
delete(ReferralEarning).where(ReferralEarning.referral_id == db_user.id)
)
logger.info(f"🗑️ УДАЛЕНЫ ВСЕ реферальные доходы пользователя {telegram_id}")
except Exception as referral_error:
logger.warning(f"⚠️ Не удалось удалить реферальные доходы: {referral_error}")
try:
from sqlalchemy import delete
from app.database.models import PromoCodeUse
await db.execute(
delete(PromoCodeUse).where(PromoCodeUse.user_id == db_user.id)
)
logger.info(f"🗑️ УДАЛЕНЫ ВСЕ использования промокодов пользователя {telegram_id}")
except Exception as promo_error:
logger.warning(f"⚠️ Не удалось удалить использования промокодов: {promo_error}")
try:
db_user.balance_kopeks = 0
logger.info(f"💰 Сброшен баланс пользователя {telegram_id}")
except Exception as balance_error:
logger.warning(f"⚠️ Не удалось сбросить баланс: {balance_error}")
from app.database.models import SubscriptionStatus
from datetime import datetime
subscription.status = SubscriptionStatus.DISABLED.value
subscription.is_trial = True
subscription.end_date = datetime.utcnow()
subscription.traffic_limit_gb = 0
subscription.traffic_used_gb = 0.0
subscription.device_limit = 1
subscription.connected_squads = []
subscription.autopay_enabled = False
subscription.autopay_days_before = 3
subscription.remnawave_short_uuid = None
subscription.subscription_url = ""
db_user.remnawave_uuid = None
db_user.has_had_paid_subscription = False
db_user.used_promocodes = 0
await db.commit()
stats["deleted"] += 1
logger.info(f"✅ ПОЛНОСТЬЮ очищены ВСЕ данные подписки пользователя {telegram_id}")
except Exception as delete_error:
logger.error(f"❌ Ошибка полной очистки данных подписки {telegram_id}: {delete_error}")
stats["errors"] += 1
await db.rollback()
logger.info(f"🎯 Синхронизация завершена: создано {stats['created']}, обновлено {stats['updated']}, удалено {stats['deleted']}, ошибок {stats['errors']}")
)
logger.info(f"🗑️ Удалены серверы подписки для {telegram_id}")
except Exception as servers_error:
logger.warning(f"⚠️ Не удалось удалить серверы подписки: {servers_error}")
from app.database.models import SubscriptionStatus
subscription.status = SubscriptionStatus.DISABLED.value
subscription.is_trial = True
subscription.end_date = datetime.utcnow()
subscription.traffic_limit_gb = 0
subscription.traffic_used_gb = 0.0
subscription.device_limit = 1
subscription.connected_squads = []
subscription.autopay_enabled = False
subscription.remnawave_short_uuid = None
subscription.subscription_url = ""
db_user.remnawave_uuid = None
await db.commit()
stats["deleted"] += 1
logger.info(f"✅ Деактивирована подписка пользователя {telegram_id} (сохранен баланс)")
except Exception as delete_error:
logger.error(f"❌ Ошибка деактивации подписки {telegram_id}: {delete_error}")
stats["errors"] += 1
await db.rollback()
logger.info(f"🎯 Синхронизация завершена: создано {stats['created']}, обновлено {stats['updated']}, деактивировано {stats['deleted']}, ошибок {stats['errors']}")
return stats
except Exception as e:
logger.error(f"❌ Критическая ошибка синхронизации пользователей: {e}")
return {"created": 0, "updated": 0, "errors": 1, "deleted": 0}
async def _create_subscription_from_panel_data(self, db: AsyncSession, user, panel_user):
try:
from app.database.crud.subscription import create_subscription
from app.database.models import SubscriptionStatus
from datetime import datetime, timedelta
import pytz
expire_at_str = panel_user.get('expireAt', '')
try:
if expire_at_str:
if expire_at_str.endswith('Z'):
expire_at_str = expire_at_str[:-1] + '+00:00'
expire_at = datetime.fromisoformat(expire_at_str)
if expire_at.tzinfo is not None:
expire_at = expire_at.replace(tzinfo=None)
else:
expire_at = datetime.utcnow() + timedelta(days=30)
except Exception as date_error:
logger.warning(f"⚠️ Ошибка парсинга даты {expire_at_str}: {date_error}")
expire_at = datetime.utcnow() + timedelta(days=30)
expire_at = self._parse_remnawave_date(expire_at_str)
panel_status = panel_user.get('status', 'ACTIVE')
current_time = datetime.utcnow()
@@ -672,7 +674,6 @@ class RemnaWaveService:
try:
from app.database.crud.subscription import get_subscription_by_user_id
from app.database.models import SubscriptionStatus
from datetime import datetime, timedelta
subscription = await get_subscription_by_user_id(db, user.id)
@@ -683,19 +684,12 @@ class RemnaWaveService:
panel_status = panel_user.get('status', 'ACTIVE')
expire_at_str = panel_user.get('expireAt', '')
try:
if expire_at_str:
if expire_at_str.endswith('Z'):
expire_at_str = expire_at_str[:-1] + '+00:00'
expire_at = datetime.fromisoformat(expire_at_str)
if expire_at.tzinfo is not None:
expire_at = expire_at.replace(tzinfo=None)
if abs((subscription.end_date - expire_at).total_seconds()) > 60:
subscription.end_date = expire_at
logger.debug(f"Обновлена дата окончания подписки до {expire_at}")
except Exception as date_error:
logger.warning(f"⚠️ Ошибка парсинга даты при обновлении {expire_at_str}: {date_error}")
if expire_at_str:
expire_at = self._parse_remnawave_date(expire_at_str)
if abs((subscription.end_date - expire_at).total_seconds()) > 60:
subscription.end_date = expire_at
logger.debug(f"Обновлена дата окончания подписки до {expire_at}")
current_time = datetime.utcnow()
if panel_status == 'ACTIVE' and subscription.end_date > current_time:
@@ -1001,7 +995,6 @@ class RemnaWaveService:
node_realtime = stats
break
from datetime import datetime, timedelta
end_date = datetime.now()
start_date = end_date - timedelta(days=7)
@@ -1089,7 +1082,6 @@ class RemnaWaveService:
logger.error(f"❌ Ошибка удаления связанных записей: {records_error}")
try:
from datetime import datetime
user.balance_kopeks = 0
user.remnawave_uuid = None
@@ -1213,7 +1205,6 @@ class RemnaWaveService:
from app.database.crud.subscription import get_all_subscriptions
from app.database.models import SubscriptionStatus
from datetime import datetime
page = 1
limit = 100
@@ -1266,7 +1257,6 @@ class RemnaWaveService:
from app.database.crud.subscription import get_all_subscriptions
from app.database.models import SubscriptionStatus
from datetime import datetime
page = 1
limit = 100
+25 -21
View File
@@ -229,36 +229,40 @@ class SubscriptionService:
devices: int,
db: AsyncSession
) -> Tuple[int, List[int]]:
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
from app.config import PERIOD_PRICES
from app.database.crud.server_squad import get_server_squad_by_id
if settings.MAX_DEVICES_LIMIT > 0 and devices > settings.MAX_DEVICES_LIMIT:
raise ValueError(f"Превышен максимальный лимит устройств: {settings.MAX_DEVICES_LIMIT}")
base_price = PERIOD_PRICES.get(period_days, 0)
traffic_price = TRAFFIC_PRICES.get(traffic_gb, 0)
traffic_price = settings.get_traffic_price(traffic_gb)
server_prices = []
total_servers_price = 0
for server_id in server_squad_ids:
server = await get_server_squad_by_id(db, server_id)
if server and server.is_available and not server.is_full:
server_prices.append(server.price_kopeks)
total_servers_price += server.price_kopeks
logger.debug(f"🏷️ Сервер {server.display_name}: {server.price_kopeks/100}")
logger.debug(f"Сервер {server.display_name}: {server.price_kopeks/100}")
else:
server_prices.append(0)
logger.warning(f"⚠️ Сервер ID {server_id} недоступен")
devices_price = max(0, devices - 1) * settings.PRICE_PER_DEVICE
logger.warning(f"Сервер ID {server_id} недоступен")
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_price = base_price + traffic_price + total_servers_price + devices_price
logger.info(f"💰 Расчет стоимости новой подписки:")
logger.info(f" 📅 Период {period_days} дней: {base_price/100}")
logger.info(f" 📊 Трафик {traffic_gb} ГБ: {traffic_price/100}")
logger.info(f" 🌍 Серверы ({len(server_squad_ids)}): {total_servers_price/100}")
logger.info(f" 📱 Устройства ({devices}): {devices_price/100}")
logger.info(f" 💎 ИТОГО: {total_price/100}")
logger.info(f"Расчет стоимости новой подписки:")
logger.info(f" Период {period_days} дней: {base_price/100}")
logger.info(f" Трафик {traffic_gb} ГБ: {traffic_price/100}")
logger.info(f" Серверы ({len(server_squad_ids)}): {total_servers_price/100}")
logger.info(f" Устройства ({devices}): {devices_price/100}")
logger.info(f" ИТОГО: {total_price/100}")
return total_price, server_prices
@@ -269,7 +273,7 @@ class SubscriptionService:
db: AsyncSession
) -> int:
try:
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
from app.config import PERIOD_PRICES
base_price = PERIOD_PRICES.get(period_days, 0)
@@ -277,9 +281,9 @@ class SubscriptionService:
subscription.connected_squads, db
)
devices_price = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
traffic_price = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
traffic_price = settings.get_traffic_price(subscription.traffic_limit_gb)
total_price = base_price + servers_price + devices_price + traffic_price
@@ -364,7 +368,7 @@ class SubscriptionService:
prices_list.append(price)
logger.debug(f"🏷️ Страна {server.display_name}: {price/100}")
else:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
logger.warning(f"⚠️ Сервер {country_uuid} недоступен, используем базовую цену: {default_price/100}")
@@ -374,7 +378,7 @@ class SubscriptionService:
except Exception as e:
logger.error(f"Ошибка получения цен стран: {e}")
default_prices = [1000] * len(country_uuids)
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
async def _get_countries_price(self, country_uuids: List[str], db: AsyncSession) -> int:
+96 -51
View File
@@ -2,7 +2,7 @@ import logging
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete
from sqlalchemy import delete, select, update
from app.database.crud.user import (
get_user_by_id, get_user_by_telegram_id, get_users_list,
@@ -13,7 +13,7 @@ from app.database.crud.transaction import get_user_transactions_count
from app.database.crud.subscription import get_subscription_by_user_id
from app.database.models import (
User, UserStatus, Subscription, Transaction, PromoCodeUse,
ReferralEarning, SubscriptionServer
ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory
)
from app.config import settings
@@ -242,9 +242,81 @@ class UserService:
from app.services.subscription_service import SubscriptionService
subscription_service = SubscriptionService()
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
logger.info(f" ✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
except Exception as e:
logger.error(f" Ошибка деактивации RemnaWave пользователя: {e}")
logger.warning(f" ⚠️ Ошибка деактивации RemnaWave: {e}")
try:
from app.database.models import YooKassaPayment
from sqlalchemy import select
yookassa_result = await db.execute(
select(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
)
yookassa_payments = yookassa_result.scalars().all()
if yookassa_payments:
logger.info(f" 🔄 Удаляем {len(yookassa_payments)} YooKassa платежей")
await db.execute(
delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
)
await db.flush()
logger.info(f" ✅ YooKassa платежи удалены")
except Exception as e:
logger.error(f" ❌ Ошибка удаления YooKassa платежей: {e}")
try:
transactions_result = await db.execute(
select(Transaction).where(Transaction.user_id == user_id)
)
transactions = transactions_result.scalars().all()
if transactions:
logger.info(f" 🔄 Удаляем {len(transactions)} транзакций")
await db.execute(
delete(Transaction).where(Transaction.user_id == user_id)
)
await db.flush()
logger.info(f" ✅ Транзакции удалены")
except Exception as e:
logger.error(f" ❌ Ошибка удаления транзакций: {e}")
try:
await db.execute(
delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалены использования промокодов пользователя {user_id}")
except Exception as e:
logger.error(f" ❌ Ошибка удаления использований промокодов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.user_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалены реферальные доходы пользователя {user_id}")
except Exception as e:
logger.error(f" ❌ Ошибка удаления реферальных доходов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.referral_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалены реферальные записи о пользователе {user_id}")
except Exception as e:
logger.error(f" ❌ Ошибка удаления реферальных записей: {e}")
try:
from app.database.models import BroadcastHistory
await db.execute(
delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалена история рассылок админа {user_id}")
except Exception as e:
logger.error(f" ❌ Ошибка удаления истории рассылок: {e}")
if user.subscription:
try:
@@ -253,74 +325,47 @@ class UserService:
SubscriptionServer.subscription_id == user.subscription.id
)
)
logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
await db.flush()
logger.info(f" 🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
logger.error(f" ❌ Ошибка удаления SubscriptionServer: {e}")
if user.subscription:
try:
await db.delete(user.subscription)
logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
from app.database.models import Subscription
await db.execute(
delete(Subscription).where(Subscription.user_id == user_id)
)
await db.flush()
logger.info(f" 🗑️ Удалена подписка пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления подписки: {e}")
try:
await db.execute(
delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id)
)
logger.info(f"🗑️ Удалены использования промокодов пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.user_id == user.id)
)
logger.info(f"🗑️ Удалены реферальные доходы пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.referral_id == user.id)
)
logger.info(f"🗑️ Удалены реферальные записи о пользователе {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления реферальных записей: {e}")
logger.error(f" ❌ Ошибка удаления подписки: {e}")
try:
from sqlalchemy import update
await db.execute(
referrals_result = await db.execute(
update(User)
.where(User.referred_by_id == user.id)
.where(User.referred_by_id == user_id)
.values(referred_by_id=None)
)
logger.info(f"🗑️ Очищены реферальные ссылки у рефералов пользователя {user_id}")
if referrals_result.rowcount > 0:
logger.info(f" 🔗 Очищены реферальные ссылки у {referrals_result.rowcount} рефералов")
await db.flush()
except Exception as e:
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
logger.error(f" ❌ Ошибка очистки реферальных ссылок: {e}")
try:
await db.execute(
delete(Transaction).where(Transaction.user_id == user.id)
delete(User).where(User.id == user_id)
)
logger.info(f"🗑️ Удалены транзакции пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления транзакций: {e}")
try:
user.status = UserStatus.DELETED.value
user.balance_kopeks = 0
user.remnawave_uuid = None
user.updated_at = datetime.utcnow()
await db.commit()
logger.info(f"✅ Пользователь {user_id} помечен как удаленный и обнулен")
logger.info(f" ✅ Пользователь {user_id} окончательно удален из базы")
except Exception as e:
logger.error(f"❌ Ошибка обновления статуса пользователя: {e}")
logger.error(f" ❌ Ошибка финального удаления пользователя: {e}")
await db.rollback()
return False
logger.info(f"✅ Пользователь {user_id} (Telegram ID: {user.telegram_id}) полностью удален админом {admin_id}")
logger.info(f"✅ Пользователь {user.telegram_id} (ID: {user_id}) полностью удален администратором {admin_id}")
return True
except Exception as e:
+2
View File
@@ -52,6 +52,8 @@ class AdminStates(StatesGroup):
editing_squad_price = State()
editing_traffic_price = State()
editing_device_price = State()
editing_user_devices = State()
editing_user_traffic = State()
editing_rules_page = State()
+77
View File
@@ -0,0 +1,77 @@
services:
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-remnawave_bot}
POSTGRES_USER: ${POSTGRES_USER:-remnawave_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password_123}
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:7-alpine
container_name: remnawave_bot_redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- bot_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-remnawave_user}:${POSTGRES_PASSWORD:-secure_password_123}@postgres:5432/${POSTGRES_DB:-remnawave_bot}
REDIS_URL: redis://redis:6379/0
volumes:
- ./logs:/app/logs:rw
- ./data:/app/data:rw
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "${TRIBUTE_WEBHOOK_PORT:-8081}:8081"
- "${YOOKASSA_WEBHOOK_PORT:-8082}:8082"
networks:
- bot_network
user: "1000:1000"
command: >
bash -c "
mkdir -p /app/logs /app/data &&
python main.py
"
volumes:
postgres_data:
redis_data:
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
+54 -9
View File
@@ -12,7 +12,9 @@ from app.config import settings
from app.database.database import init_db
from app.services.monitoring_service import monitoring_service
from app.services.maintenance_service import maintenance_service
from app.services.payment_service import PaymentService
from app.external.webhook_server import WebhookServer
from app.external.yookassa_webhook import start_yookassa_webhook_server
from app.database.universal_migration import run_universal_migration
@@ -44,6 +46,7 @@ async def main():
signal.signal(signal.SIGTERM, killer.exit_gracefully)
webhook_server = None
yookassa_server_task = None
monitoring_task = None
maintenance_task = None
polling_task = None
@@ -74,34 +77,68 @@ async def main():
bot, dp = await setup_bot()
monitoring_service.bot = bot
maintenance_service.set_bot(bot)
logger.info("🔗 Бот подключен к сервисам мониторинга и техработ")
payment_service = PaymentService(bot)
if settings.TRIBUTE_ENABLED:
logger.info("🌐 Запуск webhook сервера для Tribute...")
logger.info("🌐 Запуск Tribute webhook сервера...")
webhook_server = WebhookServer(bot)
await webhook_server.start()
else:
logger.info("ℹ️ Tribute отключен, webhook сервер не запускается")
logger.info("🔍 Запуск службы мониторинга...")
if settings.is_yookassa_enabled():
logger.info("💳 Запуск YooKassa webhook сервера...")
yookassa_server_task = asyncio.create_task(
start_yookassa_webhook_server(payment_service)
)
else:
logger.info("ℹ️ YooKassa отключена, webhook сервер не запускается")
logger.info("📊 Запуск службы мониторинга...")
monitoring_task = asyncio.create_task(monitoring_service.start_monitoring())
logger.info("🔧 Запуск службы техработ...")
maintenance_task = asyncio.create_task(maintenance_service.start_monitoring())
logger.info("🔧 Проверка службы техработ...")
if not maintenance_service._check_task or maintenance_service._check_task.done():
logger.info("🔧 Запуск службы техработ...")
maintenance_task = asyncio.create_task(maintenance_service.start_monitoring())
else:
logger.info("🔧 Служба техработ уже запущена")
maintenance_task = None
logger.info("🔄 Запуск polling...")
polling_task = asyncio.create_task(dp.start_polling(bot, skip_updates=True))
logger.info("=" * 50)
logger.info("🎯 Активные webhook endpoints:")
if settings.TRIBUTE_ENABLED:
logger.info(f" Tribute: {settings.WEBHOOK_URL}:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
if settings.is_yookassa_enabled():
logger.info(f" YooKassa: {settings.WEBHOOK_URL}:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
logger.info("=" * 50)
try:
while not killer.exit:
await asyncio.sleep(1)
if yookassa_server_task and yookassa_server_task.done():
exception = yookassa_server_task.exception()
if exception:
logger.error(f"YooKassa webhook сервер завершился с ошибкой: {exception}")
logger.info("🔄 Перезапуск YooKassa webhook сервера...")
yookassa_server_task = asyncio.create_task(
start_yookassa_webhook_server(payment_service)
)
if monitoring_task.done():
exception = monitoring_task.exception()
if exception:
logger.error(f"Служба мониторинга завершилась с ошибкой: {exception}")
monitoring_task = asyncio.create_task(monitoring_service.start_monitoring())
if maintenance_task.done():
if maintenance_task and maintenance_task.done():
exception = maintenance_task.exception()
if exception:
logger.error(f"Служба техработ завершилась с ошибкой: {exception}")
@@ -123,8 +160,16 @@ async def main():
finally:
logger.info("🛑 Начинается корректное завершение работы...")
if yookassa_server_task and not yookassa_server_task.done():
logger.info("ℹ️ Остановка YooKassa webhook сервера...")
yookassa_server_task.cancel()
try:
await yookassa_server_task
except asyncio.CancelledError:
pass
if monitoring_task and not monitoring_task.done():
logger.info("️ Остановка службы мониторинга...")
logger.info("️ Остановка службы мониторинга...")
monitoring_service.stop_monitoring()
monitoring_task.cancel()
try:
@@ -133,7 +178,7 @@ async def main():
pass
if maintenance_task and not maintenance_task.done():
logger.info("️ Остановка службы техработ...")
logger.info("️ Остановка службы техработ...")
await maintenance_service.stop_monitoring()
maintenance_task.cancel()
try:
@@ -142,7 +187,7 @@ async def main():
pass
if polling_task and not polling_task.done():
logger.info("️ Остановка polling...")
logger.info("️ Остановка polling...")
polling_task.cancel()
try:
await polling_task
@@ -150,7 +195,7 @@ async def main():
pass
if webhook_server:
logger.info("️ Остановка webhook сервера...")
logger.info(" Остановка Tribute webhook сервера...")
await webhook_server.stop()
if 'bot' in locals():