223 Commits

Author SHA1 Message Date
Vladislav Lisitsyn d30e932f94 Merge pull request #125 from hteppl/fix-tv-preview
Удаление превью ссылок в сообщении текста для ТВ
2024-12-29 02:26:05 +03:00
hteppl 87a3e124f4 Fix CONNECT_TV_TEXT: add disable_web_page_preview=True 2024-12-29 02:05:47 +03:00
Vladless 579a8ca7e4 TV/Send_message/custom_refferal_level/buttons 2024-12-28 22:46:20 +03:00
Vladless b1276c42a4 TV/Send_message/custom_refferal_level/buttons 2024-12-28 22:28:38 +03:00
Vladless 0996f15c30 TV/Send_message/custom_refferal_level/buttons 2024-12-28 22:22:56 +03:00
Vladislav Lisitsyn b2fb41edc3 Merge pull request #124 from hteppl/mics-updates
Небольшие изменения
2024-12-26 23:03:38 +03:00
hteppl 10ce155b8c Fix key back button 2024-12-26 03:40:26 +03:00
hteppl 0ca647fd64 Remove Telegram URL prefix when finding a user by username 2024-12-26 03:36:54 +03:00
hteppl 33d4fd2e2e Monthly payments in admin stats 2024-12-26 03:36:28 +03:00
Vladislav Lisitsyn 7a2f5cd899 Merge pull request #123 from hteppl/license
Лицензия и обновленный README
2024-12-24 17:40:48 +03:00
Vladless 6fbe1632bc gift_links 2024-12-24 15:02:48 +03:00
Vladless 36baacc0e1 buttons_pay/security_update/MacOS 2024-12-24 02:30:53 +03:00
Vladless eb7effed4c buttons_pay/security_update 2024-12-24 02:30:09 +03:00
hteppl f27b62f8e7 Update README.md 2024-12-23 16:01:16 +03:00
hteppl 4aa9be2408 Add CC BY-NC 4.0 LICENSE 2024-12-23 16:00:48 +03:00
hteppl 4fca2cad95 Remove legacy installation scripts 2024-12-23 16:00:26 +03:00
Vladless b41314af76 minor update 2024-12-23 01:23:34 +03:00
Vladless ce3985ff96 gift_code/buttons/commands 2024-12-23 01:14:28 +03:00
Vladless 9b9a72ef04 add support for business/optimization 2024-12-20 23:29:27 +03:00
Vladless ce78caf065 fix_refferal/GIFTS/redirect/name_sub and more 2024-12-19 20:39:07 +03:00
Vladless c01b528055 fix_refferal/GIFTS/redirect/name_sub and more 2024-12-19 20:37:41 +03:00
Vladislav Lisitsyn 725167f663 Merge pull request #122 from hteppl/fix-referrals
WIP: фикс уявзимости, где айди приглашенного реферала, совпадает с айди пригласившего
2024-12-16 23:41:29 +03:00
hteppl 18dde7e882 Fix self-referred bug 2024-12-16 23:19:39 +03:00
Vladislav Lisitsyn f5ec703994 Merge pull request #120 from crell2100/main
add CHANEL_EXISTS variable
2024-12-16 23:09:42 +03:00
crell2100 5e07feb815 donats_enable->donations_enable (rename variable) 2024-12-16 16:40:01 +03:00
crell2100 c394fb3d2a Merge branch 'main' into main 2024-12-15 22:51:32 +03:00
crell2100 a7979af27a add DONATS_ENABLE variable 2024-12-15 22:30:20 +03:00
crell2100 d29d454727 add CHANEL_EXISTS variable 2024-12-15 18:07:52 +03:00
Vladless de6a79a634 remove gifts 2024-12-15 00:23:08 +03:00
Vladless fd3d5d8de6 add import 2024-12-15 00:23:08 +03:00
Vladless f1358b36b7 add import 2024-12-15 00:23:08 +03:00
Vladislav Lisitsyn 831472ab51 Update README.md 2024-12-14 21:24:42 +03:00
Vladislav Lisitsyn bbb554e4e9 Merge pull request #119 from TrackLine/main
Add Yoomoney pay service
2024-12-14 19:07:25 +03:00
TrackLine 7677ae9193 Add Yoomoney pay service 2024-12-14 19:49:54 +04:00
Vladless ca87286898 sub_name 2024-12-14 18:46:43 +03:00
Vladless f821241edb fix middleware 2024-12-14 17:39:39 +03:00
Vladislav Lisitsyn 9706730d37 Merge pull request #118 from izzzzzi/main
Migrate to ruff
2024-12-14 02:02:09 +03:00
Vladislav Lisitsyn 8b11c65c8e Merge branch 'main' into main 2024-12-14 02:02:00 +03:00
Vladless 2750517be4 vat_code 2024-12-14 00:37:20 +03:00
Vladless e5da607c9d sub_name/delete user 2024-12-13 23:01:58 +03:00
Zakhar Izmaylov 1e912487af Fix requirements.txt 2024-12-13 12:13:50 +03:00
Zakhar Izmaylov 69f7d05527 Migrate to ruff 2024-12-13 12:09:20 +03:00
Vladless 2b572da089 rm wrench 2024-12-11 15:41:45 +03:00
Vladislav Lisitsyn c75fd8516a Merge pull request #113 from Vladless/hteppl-optimize-sources
Hteppl optimize sources
2024-12-10 04:14:24 +02:00
Vladless a81b09eb1e pic for tariffs 2024-12-10 05:13:54 +03:00
Vladless 145c716529 pic for tariffs 2024-12-10 05:12:49 +03:00
Vladless bc1ddc349a Revert "Merge pull request #112 from Vladless/hteppl-optimize-sources"
This reverts commit 5f557fa2b2, reversing
changes made to 4ea1f101ff.
2024-12-10 03:53:07 +03:00
Vladislav Lisitsyn 5f557fa2b2 Merge pull request #112 from Vladless/hteppl-optimize-sources
Hteppl optimize sources
2024-12-10 02:27:34 +02:00
Vladless 940867baed Merge branch 'optimize-sources' of https://github.com/hteppl/Solo_bot into hteppl-optimize-sources 2024-12-10 03:26:42 +03:00
Vladislav Lisitsyn 4ea1f101ff Merge pull request #108 from hteppl/optimize-routing-system-v2
Оптимизация системы рутинга 2 // LP
2024-12-10 00:54:32 +02:00
Vladislav Lisitsyn d0d36d9f1a Merge pull request #104 from hteppl/create-sql-schema
Оптимизация создания базы данных // LP
2024-12-10 00:54:22 +02:00
hteppl eee8d86426 Make utils as package + fix python unresolved imports 2024-12-08 05:32:23 +03:00
hteppl 94d335d950 Move payments utils into utils folder 2024-12-08 05:13:38 +03:00
hteppl 9756d2e2b7 Move keys utils into utils folder 2024-12-08 05:11:31 +03:00
hteppl 3e4d7972c7 Move utils.py to the utils folder 2024-12-08 05:02:41 +03:00
hteppl 0e9c128a15 Move util-related files into separated folder + refactor imports 2024-12-08 04:57:20 +03:00
Oleg Kraev e850bd85a8 Merge branch 'Vladless:main' into optimize-routing-system-v2 2024-12-08 04:37:31 +03:00
hteppl b02c716992 Implement global router 2024-12-08 04:36:59 +03:00
hteppl 822d845241 Include routers only if payment method enabled in the config 2024-12-08 04:23:59 +03:00
hteppl 8dc478a606 Create and implement router for admin 2024-12-08 04:21:37 +03:00
hteppl d664036f5e Create and implement router for instructions 2024-12-08 04:20:09 +03:00
hteppl fe655a7ee5 Create and implement router for keys 2024-12-08 04:19:16 +03:00
hteppl 56ff84f7a5 Create and implement router for payments 2024-12-08 04:17:50 +03:00
hteppl 69b1705fd5 Create basic main router for handlers 2024-12-08 04:15:52 +03:00
Vladislav Lisitsyn 956cdaf020 Merge pull request #107 from hteppl/linux-start-scripts
Linux cкрипты для автоматической настройки зависимостей и запуска
2024-12-07 15:32:56 +02:00
Vladless 0b44237317 add pay button 2024-12-07 16:32:16 +03:00
Vladislav Lisitsyn 67880704a0 Update README.md 2024-12-07 00:27:53 +03:00
Vladislav Lisitsyn 682fb0bdb0 Update README.md 2024-12-07 00:17:48 +03:00
hteppl 71c7a10165 Add linux shell scripts to configure dependencies automatically 2024-12-06 18:39:35 +03:00
Vladless 5577780565 ping servers and notifications 2024-12-06 05:19:44 +03:00
Vladless 5db2b7c861 ping servers and notifications 2024-12-06 04:48:58 +03:00
Vladless 370ceb8382 back button in custom pay 2024-12-06 02:25:40 +03:00
Vladless 3226438e69 back button in custom pay 2024-12-06 02:25:18 +03:00
Vladless c432edf8fc preview pc_instruction disable 2024-12-06 01:42:14 +03:00
Vladless 49aa863eb4 Server availability 2024-12-06 01:14:25 +03:00
Vladless c58039cb9e logs folder and autoremove 2024-12-06 00:46:54 +03:00
Vladless ee242c98c7 sales in texts 2024-12-05 23:56:06 +03:00
Vladless f3dfb03749 backup for 3x-ui 2024-12-05 23:31:48 +03:00
Vladless 02ea333af1 fix refferal 2024-12-05 22:59:34 +03:00
Vladless 142a5ffaab fix refferal 2024-12-05 22:45:10 +03:00
Vladless db24a9abda backup time 2024-12-05 21:09:37 +03:00
hteppl 3fcb6adaad Create schema.sql database template + optimize init_db 2024-12-05 07:59:49 +03:00
Vladless 5be9632c5e fix trial 2024-12-04 17:25:32 +03:00
Vladless 42faa00e32 change version 2024-12-04 03:10:15 +03:00
Vladless f6fbf31109 unique server_name/ link moderation 2024-12-04 03:02:29 +03:00
Vladless 8751c9d4f2 unique server_name 2024-12-04 02:46:57 +03:00
Vladless 04e1510001 plan on create new key after trial 2024-12-04 01:28:28 +03:00
Vladless 2b1c62e869 admin_update 2024-12-04 00:16:23 +03:00
Vladless 2899de2632 add button for MacOS 2024-12-04 00:06:21 +03:00
Vladless 9451deec4c Clusters on/off in config 2024-12-02 04:58:14 +03:00
Vladless d8b0ff9961 profile/reorganization and news 2024-12-02 04:46:05 +03:00
Vladless cb6bd1135b sale size 2024-12-02 04:20:32 +03:00
Vladless 47210fc20a rename server info 2024-12-02 04:08:36 +03:00
Vladless 2815b8a340 move button 2024-12-02 04:06:46 +03:00
Vladless 1301b81e79 pc button for trial_key 2024-12-02 03:54:07 +03:00
Vladless 0353d9d628 pc button for trial_key 2024-12-02 03:37:28 +03:00
Vladless d0084f4062 add/delete servers in admin 2024-12-02 03:25:06 +03:00
Vladless 4ce36e3f22 servers in admin_editor 2024-12-02 00:28:33 +03:00
Vladless 14cfe61e33 servers in database 2024-12-01 23:40:37 +03:00
Vladless 0e5961bdc5 servers in database 2024-12-01 23:24:53 +03:00
Vladless d9064ff016 autoconfig servers in database 2024-12-01 23:24:13 +03:00
Vladless efc687d0fe notification inactive users 2024-12-01 22:24:00 +03:00
Vladless 1c3e42f94a sudo in restart 2024-11-29 00:01:48 +03:00
Vladless fa7347a85c autoname for trial in profile 2024-11-28 20:48:34 +03:00
Vladless 7f1c783239 /start 2024-11-28 15:37:21 +03:00
Vladless 8c85bdd4d8 link Successful payment 2024-11-27 21:18:07 +03:00
Vladless 7991a3aa4d fix stars 2024-11-27 17:30:53 +03:00
Vladless 2c86417c9d update subs 2024-11-26 20:10:14 +03:00
Vladless f18af7010f fix admin 2024-11-26 20:03:21 +03:00
Vladless bbcd72bab4 notifications 2024-11-26 17:48:41 +03:00
Vladless 54d7299977 fix renew 2024-11-26 17:30:41 +03:00
Vladless ca8b032230 random email for add device 2024-11-26 17:21:15 +03:00
Vladislav Lisitsyn be53b79221 Merge pull request #102 from izzzzzi/main
Refactor
2024-11-26 03:25:33 +03:00
Zakhar Izmaylov f6af8e3212 Fix start 2024-11-26 03:21:43 +03:00
Zakhar Izmaylov a6e07c8080 Small fix 2024-11-26 02:54:32 +03:00
Zakhar Izmaylov 7db545e249 Enhanced notifications and admin: CSV export for users/payments, better feedback/navigation, DEV_MODE checks for notifications, .csv in .gitignore, fixed trial expiry with TRIAL_TIME. 2024-11-26 02:52:15 +03:00
Zahar Izmailov 7d751e0588 Merge branch 'Vladless:main' into main 2024-11-26 00:27:58 +03:00
Vladless 033b978a37 back button 2024-11-25 01:16:06 +03:00
Vladless d9676bd355 fix session 2024-11-25 00:58:46 +03:00
Zakhar Izmaylov 6a7890845d Refactor notification and admin user editor handlers to improve user feedback. Comment out inactive trial user notifications in notifications.py. Enhance error messages with consistent "Назад" button for better navigation in admin_user_editor.py. 2024-11-25 00:29:06 +03:00
Zakhar Izmaylov 79ea8d431d Update button texts in start and admin user editor handlers for improved navigation. Change "Личный кабинет" to "Поддержка" in start.py and adjust "Назад" callback data in admin_user_editor.py for consistency. 2024-11-25 00:16:12 +03:00
Zakhar Izmaylov 27db7f6814 Refactor import statements in notification, profile, key management, and keys handlers for improved organization and consistency. Ensure RENEWAL_PLANS is included in all relevant files. 2024-11-24 23:45:38 +03:00
Zakhar Izmaylov 900ffd3e2e Refactor notification and profile handlers to include RENEWAL_PLANS from config. Update key management and keys handlers to streamline imports and enhance code organization. 2024-11-24 23:45:27 +03:00
Zahar Izmailov 7eeacd94c1 Merge branch 'Vladless:main' into main 2024-11-24 23:37:58 +03:00
Vladless ac61783fef 3.2 2024-11-24 23:10:22 +03:00
Vladless 80838a2796 reset traffic 2024-11-24 23:02:34 +03:00
Zakhar Izmaylov b820a70f01 Merge branch 'main' of https://github.com/izzzzzi/Solo_bot 2024-11-24 22:37:23 +03:00
Zakhar Izmaylov e3584587b8 Remove unused __init__.py file and update payment handler callback buttons to unify navigation. Change "back_to_profile" callback to "pay" in multiple payment processing files for consistency. 2024-11-24 22:35:43 +03:00
Vladless 1370ff983c ID_Inbound 2024-11-24 22:29:32 +03:00
Vladless 274bbc66be ID_Inbound 2024-11-24 22:29:05 +03:00
Vladislav Lisitsyn 7a7f4ee3e2 Merge pull request #100 from izzzzzi/main
Refactoring and Update
2024-11-24 21:54:53 +03:00
Vladislav Lisitsyn 1cbf0f643b Merge branch 'main' into main 2024-11-24 21:54:30 +03:00
Zakhar Izmaylov 0e5236184c Refactor database notification query to remove unnecessary string conversion for hours parameter, improving data handling efficiency. 2024-11-24 20:48:16 +03:00
Zakhar Izmaylov 55517c1398 Refactor database and notification handlers for improved readability and consistency. Clean up import statements, enhance logging messages, and streamline function calls across multiple files. Ensure proper session handling in payment processing functions. 2024-11-24 20:29:22 +03:00
Vladless 12a2a12273 fix back cryptobot 2024-11-24 20:19:23 +03:00
Zakhar Izmaylov 02120262cd Enhance notification system for inactive trial users by adding notification time checks and logging. Implement functionality to record notifications sent to users, ensuring they are notified no more than once every 24 hours. 2024-11-24 20:02:40 +03:00
Zakhar Izmaylov 89c5a4db2c Refactor referral system to support multi-level bonuses and enhance logging. Update database interactions for referral handling and statistics retrieval. Change key display format in notifications and admin user editor for better readability. 2024-11-24 19:38:59 +03:00
Zakhar Izmaylov 2cfbfc5fd0 Update table 2024-11-24 14:14:06 +03:00
Zakhar Izmaylov 3f7d0a26fd Fix delete msg 2024-11-24 13:51:18 +03:00
Zakhar Izmaylov dccc6166e8 notify_inactive_trial_users 2024-11-24 11:58:43 +03:00
Zakhar Izmaylov a79c51dce4 Fix admin 2024-11-24 11:51:17 +03:00
Zakhar Izmaylov e99c41ae0c Update connection_exists 2024-11-24 11:42:29 +03:00
Zakhar Izmaylov 5ca72a4a65 Fix srssion 2024-11-24 11:26:54 +03:00
Zakhar Izmaylov 97d4031f0c Update id 2024-11-24 11:07:52 +03:00
Zakhar Izmaylov 2b73c601d0 Fix 2024-11-24 10:54:08 +03:00
Zakhar Izmaylov c6d372c080 Fix get_trial 2024-11-24 10:35:12 +03:00
Zakhar Izmaylov 592afcf56e Add logging 2024-11-24 09:15:09 +03:00
Zakhar Izmaylov fb64afe06e Fix trial_status 2024-11-24 08:27:27 +03:00
Zakhar Izmaylov 7d4a33e679 Fix 2024-11-24 08:13:52 +03:00
Zakhar Izmaylov 53dc006f2d Update InlineKeyboardBuilder 2024-11-24 01:57:42 +03:00
Zakhar Izmaylov a672a0ddb0 Fix 2024-11-24 01:47:37 +03:00
Zakhar Izmaylov cfc3cedc2e Add notify_inactive_trial_users 2024-11-24 01:34:39 +03:00
Zakhar Izmaylov 7fe5c3d870 Merge branch 'main' of https://github.com/izzzzzi/Solo_bot 2024-11-24 01:24:10 +03:00
Zakhar Izmaylov 2e5bcbe407 Merge branch 'main' of https://github.com/izzzzzi/Solo_bot 2024-11-24 01:19:06 +03:00
Zakhar Izmaylov 0867ebdd61 Fix typing 2024-11-24 01:19:00 +03:00
Zakhar Izmaylov fe59470256 Refactoring 2024-11-24 01:18:42 +03:00
Vladless 9cd7ea1451 update clusters 2024-11-23 20:21:47 +03:00
Vladless c55764d1a4 fix users 2024-11-23 17:30:39 +03:00
Vladislav Lisitsyn 14364dbcfe Merge pull request #98 from izzzzzi/main
fix ref/serach from username and new stats in admin
2024-11-23 08:18:10 +03:00
Zakhar Izmaylov 4b05bd8e18 Fix admin 2024-11-23 07:14:58 +03:00
Zakhar Izmaylov 5363f15fe7 Add pylint 2024-11-22 21:34:56 +03:00
Zakhar Izmaylov 42ad24df1d Update config 2024-11-22 21:24:55 +03:00
Zakhar Izmaylov f7e03fb2b9 Fix ref 2024-11-22 17:45:38 +03:00
Zakhar Izmaylov 4e3fea2b11 Update text admin 2024-11-22 01:14:23 +03:00
Zakhar Izmaylov 32b9c0e779 Update push 2024-11-22 01:12:19 +03:00
Zakhar Izmaylov 50c7aa52c0 Add search from username 2024-11-22 00:43:23 +03:00
Zakhar Izmaylov f742d3bad5 Update user_stats_menu 2024-11-21 21:14:44 +03:00
Zakhar Izmaylov d100dc7b2d Update instructions 2024-11-21 20:49:58 +03:00
Zakhar Izmaylov 384c2c4c1c Update text and add view_tariffs 2024-11-21 20:31:24 +03:00
Zakhar Izmaylov 6f0cbf6c8c Fix send_to_alls 2024-11-20 17:30:10 +03:00
Zakhar Izmaylov ef850506d6 Update admin 2024-11-20 17:18:28 +03:00
Zakhar Izmaylov 3a4ab1a956 Add new table and add stats in admin 2024-11-20 16:47:21 +03:00
Zakhar Izmaylov 187daba997 Update logger 2024-11-20 15:46:46 +03:00
Zakhar Izmaylov 6d4c98b901 Fix img and logger add button back for donate 2024-11-20 15:36:25 +03:00
Zahar Izmailov 1119d607e8 Merge branch 'Vladless:main' into main 2024-11-20 15:28:44 +03:00
Zakhar Izmaylov 6f2b9694f3 Merge branch 'main' of https://github.com/izzzzzi/Solo_bot 2024-11-20 09:43:22 +03:00
Zakhar Izmaylov 4b1549ca65 Fix 2024-11-20 09:43:16 +03:00
Vladless acc7c701fe fix bugs 2024-11-20 00:18:29 +03:00
Vladless 96a76f0425 clusters update 2024-11-19 19:31:31 +03:00
Vladless 2ab8d093dc Merge branch 'main' of https://github.com/Vladless/Solo_bot 2024-11-19 14:58:25 +03:00
Vladless f0aa11f711 security update 1.0 2024-11-19 14:58:07 +03:00
Vladislav Lisitsyn 7a7f3cac95 Update start.py 2024-11-19 11:52:06 +03:00
Vladless 9a44f4e37f security update 1.0 2024-11-19 11:43:22 +03:00
Vladless 1db43423fc security update 1.0 2024-11-19 11:21:34 +03:00
Vladislav Lisitsyn cf776c46c8 Merge pull request #97 from izzzzzi/main
Fix key and session
2024-11-19 09:03:20 +03:00
Zakhar Izmaylov 7628b8d45b Merge branch 'main' of https://github.com/izzzzzi/Solo_bot 2024-11-19 08:32:33 +03:00
Zakhar Izmaylov ba717bd5e5 Fix key and session 2024-11-19 08:32:30 +03:00
Vladislav Lisitsyn 3cea1f9653 Merge pull request #96 from izzzzzi/main
Add back button for coupons
2024-11-18 22:51:16 +03:00
Vladless 713f0a0eac renew/delete 2024-11-18 22:40:59 +03:00
Zakhar Izmaylov 8d7c1cbcb8 Fix admin 2024-11-18 22:19:17 +03:00
Zakhar Izmaylov 97ca831e43 Add back button for coupons 2024-11-18 22:03:51 +03:00
Vladless 488f52248f add time in robokassa 2024-11-18 20:03:45 +03:00
Vladless 956644fd57 fix update_sub 2024-11-18 16:04:29 +03:00
Vladless 942adc4490 Fixed unexpected behavior of the device selection button 2024-11-18 14:57:12 +03:00
Vladless e04ccd893f Fixed unexpected behavior of the device selection button 2024-11-18 14:56:41 +03:00
Vladislav Lisitsyn 12b9b9c77e Merge pull request #95 from izzzzzi/main
Add admin middleware
2024-11-18 12:54:05 +03:00
Zakhar Izmaylov 663996493c Update Middleware 2024-11-17 23:42:09 +03:00
Zakhar Izmaylov f4651b587e Fix 2024-11-17 16:24:12 +03:00
Zakhar Izmaylov a7c9556761 Add button 2024-11-17 16:19:47 +03:00
Zakhar Izmaylov 5f8280061a Formatting 2024-11-17 16:15:51 +03:00
Zakhar Izmaylov 222803f147 Fix Middleware 2024-11-17 16:14:35 +03:00
Zakhar Izmaylov 9903298592 Fix admin 2024-11-17 16:10:58 +03:00
Zakhar Izmaylov e45b64e2ad Fix admin 2024-11-17 16:05:20 +03:00
Zakhar Izmaylov d33fe0040e Add default admin 2024-11-17 16:01:18 +03:00
Zakhar Izmaylov be0e9d46e9 Fix start 2024-11-17 15:53:31 +03:00
Zakhar Izmaylov d5d61e4cc8 Fix commands 2024-11-17 15:47:31 +03:00
Zakhar Izmaylov 3573c8f7d0 Fix start 2024-11-17 15:44:41 +03:00
Zakhar Izmaylov db206dc8be Fix 2024-11-17 15:39:53 +03:00
Zakhar Izmaylov 92f8a03f76 Fix admin middleware 2024-11-17 15:39:27 +03:00
Zakhar Izmaylov ff9c01a941 Fix send backup 2024-11-17 15:29:56 +03:00
Zakhar Izmaylov 20d424f2d4 Fix admin and create coupons 2024-11-17 15:26:03 +03:00
Zakhar Izmaylov a91629bd7f Fix admin 2024-11-17 15:10:28 +03:00
Zakhar Izmaylov 7a02410f73 Update img 2024-11-17 14:17:51 +03:00
Zakhar Izmaylov 657035db5d Fix img 2024-11-17 14:15:27 +03:00
Zakhar Izmaylov 1eab4cf29c Fix 2024-11-17 14:09:05 +03:00
Zakhar Izmaylov bea360b978 Update delete models 2024-11-17 14:08:51 +03:00
Zakhar Izmaylov 09251f1e9e Add new table and middleware 2024-11-17 14:05:31 +03:00
Zakhar Izmaylov 755f871251 Refactoring and fix 2024-11-17 13:33:17 +03:00
Zakhar Izmaylov c2cc341a34 Add admin middleware 2024-11-17 13:11:49 +03:00
Zahar Izmailov 35383724f1 Merge branch 'Vladless:main' into main 2024-11-17 10:42:43 +03:00
Vladless 5d4c55e5be version 2024-11-17 03:20:06 +03:00
Zakhar Izmaylov 371cbcf10d Merge branch 'main' of https://github.com/izzzzzi/Solo_bot 2024-11-16 23:56:54 +03:00
Zakhar Izmaylov d3e88402cb #TODO Admin and bash 2024-11-16 23:56:45 +03:00
79 changed files with 114190 additions and 4299 deletions
-6
View File
@@ -1,6 +0,0 @@
[flake8]
max-line-length = 250
ignore = E203, E266, E501, W503, F541, E704, W293, W291, E126, E121, E123, E128, E302, E131, E231, W292, E402, E261, E305
max-complexity = 25
select = B, C, E, F, W, T4, B9
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.txt
+6 -1
View File
@@ -50,4 +50,9 @@ handlers/texts.py
Thumbs.db
nginx.conf
scripts/nginx.sh
scripts
models.py
Dockerfile
.csv
setup.py
-3
View File
@@ -1,3 +0,0 @@
[settings]
profile=black
line_length = 250
-23
View File
@@ -1,23 +0,0 @@
FROM python:3.10-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN apt-get update && \
apt-get install -y postgresql-client locales && \
pip install --upgrade pip && pip install -r requirements.txt && \
sed -i '/ru_RU.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen ru_RU.UTF-8
ENV LANG=ru_RU.UTF-8
ENV LANGUAGE=ru_RU:ru
ENV LC_ALL=ru_RU.UTF-8
COPY . .
RUN sed -i "s|DATABASE_URL = .*|DATABASE_URL = '${DATABASE_URL}'|" config.py
CMD ["python", "main.py"]
+330
View File
@@ -0,0 +1,330 @@
Creative Commons Attribution-NonCommercial 4.0 International Public
License © Vladless, 2024.
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
d. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
g. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
i. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
j. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
k. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
l. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the Adapted
Material from complying with this Public License.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
+5 -1
View File
@@ -1,2 +1,6 @@
formatting:
@black . && isort . && flake8
@echo "Running Ruff..." && ruff check . --fix
@echo "Running Ruff format..." && ruff format .
lint:
@echo "Running Ruff checks..." && ruff check .
+73 -67
View File
@@ -1,27 +1,31 @@
# 🚀 SoloBot
**SoloBot** — ваш идеальный помощник для управления API 3x-UI VPN на протоколе VLESS.
**SoloBot** — ваш идеальный помощник для управления API 3x-UI VPN на протоколе VLESS.
Три версии — море возможностей:
- v1.4 — бот для продажи ключей vless:
![Основные окна](preview.jpg)
- v2.3.1 — стабильная версия подписок вместо ключей с кнопками автодобавления в приложение:
- [v1.4](https://github.com/Vladless/Solo_bot/releases/tag/v1.4) — бот для продажи ключей VLESS:
![Основные окна](preview_v_2.jpg)
- v3.0.4_beta — полноценные подписки с выбором серверов из клиентского приложения (не в боте):
![](preview.jpg)
- [v2.3.1](https://github.com/Vladless/Solo_bot/releases/tag/v2.3.1) — стабильная версия подписок вместо ключей с
кнопками автодобавления в приложение:
![](preview_v_2.jpg)
Если вам не хватает функций — направьте их в issue, мы реализуем
- [v3.1](https://github.com/Vladless/Solo_bot/releases/tag/v3.1) — версия бота со значительным расширением возможностей.
[Последние актуальные релизы.](https://github.com/Vladless/Solo_bot/releases)
Если вам не хватает функций — направьте их в [issue](https://github.com/Vladless/Solo_bot/issues), мы реализуем.
## 📋 Оглавление
1. [Описание](#описание)
2. [Стек технологий](#стек-технологий)
3. [Установка](#установка)
4. [Конфигурация](#конфигурация)
5. [Запуск](#запуск)
6. [Контакты](#контакты)
1. [Описание](#-описание)
2. [Стек технологий](#-стек-технологий)
3. [Установка](#-установка)
4. [Конфигурация](#-конфигурация)
5. [Запуск](#-запуск)
6. [Контакты](#-solobot-в-telegram-и-полная-версия)
---
@@ -29,47 +33,47 @@
SoloBot реализует множество функций, включая:
- **Выдача ключей на пробный период**.
- **Выдача ключей на длительные периоды** (1 месяц, 3 месяца, полгода, год).
- **Выдача подписок на различные периоды** (1 месяц, 3 месяца, полгода, год).
- **Использование тестового периода подписки.**
- **Продление ключей** на указанные периоды.
- Полный контроль клиента над своими ключами:
- Удаление ключей.
- Продление ключей.
- Просмотр информации о ключе (сервер, оставшееся время, сам ключ).
- **Полный контроль** клиента над своими ключами:
- **Просмотр** информации о ключе (сервер, оставшееся время, сам ключ).
- **Продление** ключей.
- **Удаление** ключей.
- **Смена локации** (перемещение ключа между серверами).
- Поддержка нескольких ключей для одного клиента (несколько устройств).
- **Реферальная программа** с пригласительной ссылкой.
- Доступ к **инструкциям**.
- **Пополнение баланса**:
* через Юкасса (самозанятость и ИП)
* через freekassa (Физические Лица)
* Криптовалюта (Thanks [izzzzzi](https://github.com/izzzzzi))
- **Поддержка нескольких ключей и устройств** для одного клиента.
- **Реферальная программа** с уникальной ссылкой для приглашений.
- Доступ к **инструкциям** для подключения к сервису.
- Пополнение баланса:
* **Юкасса** (Самозанятость и ИП)
* **Юмани** (Физические Лица) ([TrackLine](https://github.com/TrackLine))
* **FreeKassa** (Физические Лица)
* **Robokassa** (Физические Лица)
* **Cryptobot** (Криптовалюта) ([izzzzzi](https://github.com/izzzzzi))
* **Звезды Telegram**
- Периодические **бэкапы базы данных клиентов**.
- Уведомления:
- Произвольные сообщения через админку.
- Уведомления о неиспользованных пробниках.
- Уведомления о истекающих ключах (за сутки, за 6 часов и в момент истечения).
- Произвольные сообщения через панель администратора.
- Уведомления о неиспользованных пробниках.
- Уведомления об истекающих ключах (за сутки, за 6 часов и в момент истечения).
- **Чат поддержки** и канал для связи.
- **Автоматическое продление ключа** при наличии достаточного баланса.
- **Удобная админка прямо в боте**
- **мультисерверность** добавляй сервера в конфиг, и они автоматически будут в боте
- **Удобная панель администратора прямо в боте**
- Серверы:
- **Мультисерверность** — добавление сервера прямо в админке
- **Проверка на доступность** — бот следит за серверами и их откликом
- **Балансировка** — бот выдает подписку на наименее загруженный сервер
---
## 💻 Стек технологий
Проект использует следующие технологии:
Основные технологии, используемые в проекте:
- **Python** версии 3.8 или выше.
- **Git** для клонирования репозитория.
- **Virtualenv** для создания виртуального окружения (рекомендуется).
- **PostgreSQL** для хранения данных.
- **Nginx** для работы с вебхуками.
- **aiogram** (версия 3.13) для взаимодействия с Telegram API.
- **youkassa** для обработки платежей.
- **aiohttp** для API запросов к панелям.
- **Python** версии 3.10 (3.12, для последних версий).
- **PostgreSQL** — для хранения данных.
- **aiogram** для взаимодействия с Telegram API.
- **aiohttp** — для API запросов к панелям.
Проект полностью асинхронный, что обеспечивает высокую скорость работы.
@@ -86,7 +90,9 @@ git clone https://github.com/Vladless/Solo_bot.git
cd solo_bot
```
### 2️⃣ Шаг 2: Создание и активация виртуального окружения
### 2️⃣ Шаг 2: Создание и подключение виртуального окружения
Создайте виртуальное окружение **Python** и перейдите в сессию:
```
python3 -m venv venv
@@ -95,6 +101,8 @@ source venv/bin/activate
### 3️⃣ Шаг 3: Установка зависимостей
Установите зависимости:
```
pip install -r requirements.txt
```
@@ -103,46 +111,44 @@ pip install -r requirements.txt
Для правильной работы вам нужно:
* установить и запустить postgresql, создать пользователя для работы с базой данных и выдать ему права
* Настроить ваш сервер на работу с ботом, выпустить SSL сертификат для домена
* Настроить вебхуки и пути до них в NGINX
* Установить и запустить **PostgreSQL**, создать пользователя для работы с базой данных и выдать ему права
* Настроить ваш сервер для работы с ботом, выпустить SSL сертификат для домена
* Настроить вебхуки и указать пути до них через любой веб-сервер, например **nginx**
* Создать файл config.py в корневой папке проекта с вашими данными:
```
API_TOKEN = "токен вашего бота телеграм"
API_TOKEN = токен вашего бота телеграм
ADMIN_USERNAME = логин от вашей панели x-ray
ADMIN_PASSWORD = пароль от вашей панели x-ray
ADMIN_USERNAME = "логин от вашей панели x-ray"
ADMIN_PASSWORD = "пароль от вашей панели x-ray"
ADD_CLIENT_URL = f"{API_URL}/panel/api/inbounds/addClient"
GET_INBOUNDS_URL = f"{API_URL}/panel/api/inbounds/list/"
AUTH_URL = f"{API_URL}/login/"
DATABASE_URL = путь к вашей базе данных, имеет вид "postgresql://{user}:{password}@{Ip-adress}:{port}/{имя базы данных}"
ADMIN_ID = ID телеграм профиля администратора
CHANNEL_URL = ссылка на ваш телеграм канал
YOOKASSA_SECRET_KEY = ваш ключ юкассы
YOOKASSA_SHOP_ID = ваш шопайди
WEBHOOK_HOST = адрес вашего сервера для вебхуков
WEBHOOK_PATH = '/webhook/'
WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
SUPPORT_CHAT_URL = ваша ссылка на поддержку
```
**Мы высылаем детальный гайд и недостающие файлы в боте**
**Все описания в одном файле!** Удобно настроить бот под свой сервис изменив информацию и цены в одном месте
**Мы высылаем детальный гайд и недостающие файлы в поддержке бота!**
### 🚀 Запуск
введите команду из виртуального окружения
Введите команду из виртуального окружения `venv/bin/activate`
```
python main.py
python3 main.py
```
### 🔗 SoloBot в Telegram и Полная версия
Попробуйте SoloBot прямо сейчас в Telegram [по этой ссылке](https://t.me/SoloNetVPN_bot).
#### Наш [**➡ сайт**](https://pocomacho.ru/solonetbot/):
Вы также можете поддержать автора или купить полную версию бота с сопровождением по установке в поддержке бота!
Полная пошаговая инстуркция по запуску бота предоставляется в поддержке бота
Всегда актуальные гайды по установке, файлы для запуска и ссылка на общий чат:
![image](https://github.com/user-attachments/assets/737fbc21-e4b1-4c69-9323-71284d34e454)
Попробовать SoloBot прямо сейчас в Telegram [**➡ Попробовать**](https://t.me/SoloNetVPN_bot).
Связаться с нами через [**➡ поддержку**](https://t.me/solonet_sup). Там вы сможете купить полную версию и получить логин
и пароль от сайта, а также задать необходимые вопросы!
## Права на использование
[Этот проект использует лицензию CC BY-NC 4.0](LICENSE)
View File
+102
View File
@@ -0,0 +1,102 @@
CREATE TABLE IF NOT EXISTS users
(
tg_id BIGINT PRIMARY KEY NOT NULL,
username TEXT,
first_name TEXT,
last_name TEXT,
language_code TEXT,
is_bot BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS connections
(
tg_id BIGINT PRIMARY KEY NOT NULL,
balance REAL NOT NULL DEFAULT 0.0,
trial INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS payments
(
id SERIAL PRIMARY KEY,
tg_id BIGINT NOT NULL,
amount REAL NOT NULL,
payment_system TEXT NOT NULL,
status TEXT DEFAULT 'success',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tg_id) REFERENCES users (tg_id)
);
CREATE TABLE IF NOT EXISTS keys
(
tg_id BIGINT NOT NULL,
client_id TEXT NOT NULL,
email TEXT NOT NULL,
created_at BIGINT NOT NULL,
expiry_time BIGINT NOT NULL,
key TEXT NOT NULL,
server_id TEXT NOT NULL DEFAULT 'cluster1',
notified BOOLEAN NOT NULL DEFAULT FALSE,
notified_24h BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (tg_id, client_id)
);
CREATE TABLE IF NOT EXISTS referrals
(
referred_tg_id BIGINT PRIMARY KEY NOT NULL,
referrer_tg_id BIGINT NOT NULL,
reward_issued BOOLEAN DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS coupons
(
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
amount INTEGER NOT NULL,
usage_limit INTEGER NOT NULL DEFAULT 1,
usage_count INTEGER NOT NULL DEFAULT 0,
is_used BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS coupon_usages
(
coupon_id INTEGER NOT NULL REFERENCES coupons (id) ON DELETE CASCADE,
user_id BIGINT NOT NULL,
used_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (coupon_id, user_id)
);
CREATE TABLE IF NOT EXISTS notifications
(
tg_id BIGINT NOT NULL,
last_notification_time TIMESTAMP NOT NULL DEFAULT NOW(),
notification_type TEXT NOT NULL,
PRIMARY KEY (tg_id, notification_type)
);
CREATE TABLE IF NOT EXISTS servers
(
id SERIAL PRIMARY KEY,
cluster_name TEXT NOT NULL,
server_name TEXT NOT NULL,
api_url TEXT NOT NULL,
subscription_url TEXT NOT NULL,
inbound_id TEXT NOT NULL,
UNIQUE (cluster_name, server_name)
);
CREATE TABLE IF NOT EXISTS gifts
(
gift_id TEXT PRIMARY KEY NOT NULL,
sender_tg_id BIGINT NOT NULL,
selected_months INTEGER NOT NULL,
expiry_time TIMESTAMP WITH TIME ZONE NOT NULL,
gift_link TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_used BOOLEAN NOT NULL DEFAULT FALSE,
recipient_tg_id BIGINT,
CONSTRAINT fk_sender FOREIGN KEY (sender_tg_id) REFERENCES users (tg_id),
CONSTRAINT fk_recipient FOREIGN KEY (recipient_tg_id) REFERENCES users (tg_id)
);
+44 -8
View File
@@ -11,6 +11,15 @@ from logger import logger
async def backup_database():
from bot import bot
try:
if backup_file_path := _create_database_backup():
await _send_backup_to_admin(bot, backup_file_path)
_cleanup_old_backups()
except Exception as e:
logger.error(f"Ошибка при создании или отправке бэкапа: {e}")
def _create_database_backup():
USER = DB_USER
HOST = "localhost"
BACKUP_DIR = BACK_DIR
@@ -21,29 +30,53 @@ async def backup_database():
try:
subprocess.run(
["pg_dump", "-U", USER, "-h", HOST, "-F", "c", "-f", BACKUP_FILE, DB_NAME],
[
"pg_dump",
"-U",
USER,
"-h",
HOST,
"-F",
"c",
"-f",
BACKUP_FILE,
DB_NAME,
],
check=True,
)
logger.info(f"Бэкап базы данных создан: {BACKUP_FILE}")
return BACKUP_FILE
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при создании бэкапа базы данных: {e}")
return
return None
finally:
del os.environ["PGPASSWORD"]
async def _send_backup_to_admin(bot, backup_file_path):
try:
with open(BACKUP_FILE, "rb") as backup_file:
with open(backup_file_path, "rb") as backup_file:
backup_input_file = BufferedInputFile(
backup_file.read(), filename=os.path.basename(BACKUP_FILE)
backup_file.read(), filename=os.path.basename(backup_file_path)
)
await bot.send_document(ADMIN_ID, backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
admin_ids: int | list[int] = ADMIN_ID
if isinstance(admin_ids, list):
for id in admin_ids:
await bot.send_document(id, backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {id}")
else:
await bot.send_document(admin_ids, backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
except Exception as e:
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
def _cleanup_old_backups():
try:
subprocess.run(
[
"find",
BACKUP_DIR,
BACK_DIR,
"-type",
"f",
"-name",
@@ -61,4 +94,7 @@ async def backup_database():
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
del os.environ["PGPASSWORD"]
async def create_backup_and_send_to_admins(xui):
await xui.login()
await xui.database.export()
+30 -35
View File
@@ -1,49 +1,44 @@
from aiogram import Bot, Dispatcher, Router
import traceback
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import ErrorEvent
from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE
from config import API_TOKEN
from logger import logger
from middlewares.admin import AdminMiddleware
from middlewares.database import DatabaseMiddleware
from middlewares.delete import DeleteMessageMiddleware
from middlewares.logging import LoggingMiddleware
from middlewares.user import UserMiddleware
bot = Bot(token=API_TOKEN)
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
router = Router()
from handlers import commands, coupons, donate, notifications, pay, profile, start
from handlers.admin import admin_commands, admin_coupons, admin_panel, admin_user_editor
from handlers.instructions import instructions
from handlers.keys import key_management, keys
from handlers.payments import cryprobot_pay, freekassa_pay, robokassa_pay, stars_pay, yookassa_pay
dp.include_router(admin_commands.router)
dp.include_router(admin_coupons.router)
dp.include_router(admin_panel.router)
dp.include_router(admin_user_editor.router)
dp.include_router(commands.router)
dp.include_router(coupons.router)
dp.include_router(start.router)
dp.include_router(profile.router)
dp.include_router(keys.router)
dp.include_router(key_management.router)
dp.include_router(pay.router)
dp.include_router(notifications.router)
dp.include_router(instructions.router)
dp.include_router(donate.router)
if YOOKASSA_ENABLE:
dp.include_router(yookassa_pay.router)
if FREEKASSA_ENABLE:
dp.include_router(freekassa_pay.router)
if CRYPTO_BOT_ENABLE:
dp.include_router(cryprobot_pay.router)
if STARS_ENABLE:
dp.include_router(stars_pay.router)
if ROBOKASSA_ENABLE:
dp.include_router(robokassa_pay.router)
dp.message.middleware(LoggingMiddleware())
dp.callback_query.middleware(LoggingMiddleware())
dp.message.middleware(AdminMiddleware())
dp.callback_query.middleware(AdminMiddleware())
dp.message.middleware(UserMiddleware())
dp.callback_query.middleware(UserMiddleware())
dp.message.middleware(DatabaseMiddleware())
dp.callback_query.middleware(DatabaseMiddleware())
dp.message.outer_middleware(DeleteMessageMiddleware())
dp.callback_query.outer_middleware(DeleteMessageMiddleware())
@dp.error()
async def error_handler(event: ErrorEvent):
logger.error(
"Ошибка в боте:\n"
f"Исключение: {event.exception}\n"
f"Тип: {type(event.exception)}\n"
f"Update: {event.update}\n"
f"Трассировка:\n{traceback.format_exc()}"
)
+9 -6
View File
@@ -1,6 +1,6 @@
import py3xui
from config import TOTAL_GB
from config import LIMIT_IP
from logger import logger
@@ -14,6 +14,7 @@ async def add_client(
expiry_time: int,
enable: bool,
flow: str,
inbound_id: int,
):
"""
Adds a client to the server via 3x-ui.
@@ -25,7 +26,7 @@ async def add_client(
id=client_id,
email=email.lower(),
limit_ip=limit_ip,
total_gb=TOTAL_GB,
total_gb=total_gb,
expiry_time=expiry_time,
enable=enable,
tg_id=tg_id,
@@ -33,7 +34,7 @@ async def add_client(
flow=flow,
)
response = await xui.client.add(1, [client])
response = await xui.client.add(inbound_id, [client])
logger.info(f"Клиент {email} успешно добавлен с ID {client_id}.")
@@ -45,7 +46,7 @@ async def add_client(
async def extend_client_key(
xui, email: str, new_expiry_time: int, client_id: str, total_gb: int
xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int
):
"""
Функция для обновления срока действия ключа клиента по email.
@@ -72,9 +73,11 @@ async def extend_client_key(
client.sub_id = email
client.total_gb = total_gb
client.enable = True
client.limit_ip = 1
client.limit_ip = LIMIT_IP
client.inbound_id = inbound_id
await xui.client.update(client.id, client)
await xui.client.reset_stats(inbound_id, email)
logger.info(
f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}."
)
@@ -85,6 +88,7 @@ async def extend_client_key(
async def delete_client(
xui,
inbound_id: int,
email: str,
client_id: str,
) -> bool:
@@ -101,7 +105,6 @@ async def delete_client(
return False
client.id = client_id
inbound_id = 1
await xui.client.delete(inbound_id, client.id)
logger.info(f"Клиент с ID {client_id} был удален успешно.")
+1124 -277
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -1,3 +1,4 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
@@ -6,9 +7,10 @@ from config import ADMIN_ID
class IsAdminFilter(BaseFilter):
async def __call__(self, message: Message) -> bool:
if isinstance(ADMIN_ID, list):
return message.from_user.id in ADMIN_ID
elif isinstance(ADMIN_ID, int):
return message.from_user.id == ADMIN_ID
else:
try:
admin_ids: int | list[int] = ADMIN_ID
if isinstance(admin_ids, list):
return message.from_user.id in admin_ids
return message.from_user.id == admin_ids
except Exception:
return False
+29
View File
@@ -0,0 +1,29 @@
__all__ = ("router",)
from aiogram import Router
from .admin import router as admin_router
from .coupons import router as coupons_router
from .donate import router as donate_router
from .instructions import router as instructions_router
from .keys import router as keys_router
from .notifications import router as notifications_router
from .pay import router as pay_router
from .payments import router as payments_router
from .profile import router as profile_router
from .start import router as start_router
router = Router(name="handlers_main_router")
router.include_routers(
start_router,
profile_router,
pay_router,
donate_router,
coupons_router,
notifications_router,
payments_router,
keys_router,
instructions_router,
admin_router,
)
+17
View File
@@ -0,0 +1,17 @@
__all__ = ("router",)
from aiogram import Router
from .admin_coupons import router as coupons_router
from .admin_panel import router as panel_router
from .admin_servers import router as servers_router
from .admin_user_editor import router as user_editor_router
router = Router(name="admins_main_router")
router.include_routers(
panel_router,
servers_router,
coupons_router,
user_editor_router,
)
-160
View File
@@ -1,160 +0,0 @@
import asyncpg
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from bot import bot
from config import DATABASE_URL
from database import add_balance_to_client, check_connection_exists
from filters.admin import IsAdminFilter
from handlers.texts import TRIAL
from logger import logger
router = Router()
class Form(StatesGroup):
waiting_for_server_selection = State()
waiting_for_key_name = State()
viewing_profile = State()
waiting_for_message = State()
@router.message(Command("add_balance"), IsAdminFilter())
async def cmd_add_balance(message: types.Message):
try:
_, client_id, amount = message.text.split()
amount = float(amount)
if not await check_connection_exists(int(client_id)):
await message.reply(f"❌ Клиент с ID {client_id} не найден в базе данных.")
return
await add_balance_to_client(int(client_id), amount)
await message.reply(
f"✅ Баланс клиента {client_id} успешно пополнен на {amount}"
)
except ValueError:
await message.reply(
"❓ Неверный формат команды!\n"
"Пожалуйста, используйте следующий шаблон:\n"
"/add_balance <ID клиента> <сумма пополнения>"
)
except Exception as e:
await message.reply(f"🚨 Произошла непредвиденная ошибка: {e}")
@router.message(Command("backup"), IsAdminFilter())
async def backup_command(message: types.Message):
from backup import backup_database
await message.answer("🔄 Инициализация резервного копирования базы данных...")
await backup_database()
await message.answer(
"✅ Бэкап базы данных успешно завершен и отправлен администратору."
)
@router.message(Command("send_trial"), IsAdminFilter())
async def handle_send_trial_command(message: types.Message, state: FSMContext):
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch(
"""
SELECT tg_id FROM connections WHERE trial = 0
"""
)
if records:
success_count = 0
error_count = 0
blocked_count = 0
for record in records:
tg_id = record["tg_id"]
trial_message = TRIAL
try:
await bot.send_message(chat_id=tg_id, text=trial_message)
success_count += 1
except Exception as e:
if "Forbidden: bot was blocked by the user" in str(e):
blocked_count += 1
logger.info(
f"🚫 Бот заблокирован пользователем с tg_id: {tg_id}"
)
else:
error_count += 1
logger.error(
f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
await message.answer(
f"📊 Результаты рассылки пробных периодов:\n"
f"✅ Успешно отправлено: {success_count}\n"
f"🚫 Заблокировано: {blocked_count}\n"
f"❌ Ошибок: {error_count}"
)
else:
await message.answer(
"📭 Нет пользователей с неиспользованными пробными ключами."
)
finally:
await conn.close()
except Exception as e:
await message.answer(f"❗ Ошибка при отправке сообщений: {e}")
@router.message(Command("send_to_all"), IsAdminFilter())
async def send_message_to_all_clients(
message: types.Message, state: FSMContext, from_panel=False
):
if from_panel:
await message.answer(
"✍️ Введите текст сообщения, который вы хотите отправить всем клиентам:"
)
await state.set_state(Form.waiting_for_message)
@router.message(Form.waiting_for_message, IsAdminFilter())
async def process_message_to_all(
message: types.Message,
state: FSMContext,
):
text_message = message.text
try:
conn = await asyncpg.connect(DATABASE_URL)
tg_ids = await conn.fetch("SELECT tg_id FROM connections")
total_users = len(tg_ids)
success_count = 0
error_count = 0
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
success_count += 1
except Exception as e:
error_count += 1
logger.error(
f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
await message.answer(
f"📤 Рассылка завершена:\n"
f"👥 Всего пользователей: {total_users}\n"
f"✅ Успешно отправлено: {success_count}\n"
f"❌ Не доставлено: {error_count}"
)
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
await message.answer("❌ Произошла ошибка при отправке сообщения.")
finally:
await conn.close()
await state.clear()
+51 -94
View File
@@ -1,3 +1,5 @@
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
@@ -5,6 +7,7 @@ from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from database import create_coupon, delete_coupon_from_db, get_all_coupons
from filters.admin import IsAdminFilter
from logger import logger
@@ -15,53 +18,37 @@ class AdminCouponsState(StatesGroup):
router = Router()
@router.callback_query(F.data == "coupons_editor")
async def show_coupon_management_menu(callback_query: types.CallbackQuery):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
@router.callback_query(F.data == "coupons_editor", IsAdminFilter())
async def show_coupon_management_menu(
callback_query: types.CallbackQuery, state: FSMContext
):
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text=" Создать купон", callback_data="create_coupon")
)
builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons"))
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_admin_menu")
)
markup = builder.as_markup()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(
"🛠 Меню управления купонами:", reply_markup=markup
"🛠 Меню управления купонами:", reply_markup=builder.as_markup()
)
await callback_query.answer()
@router.callback_query(F.data == "coupons")
async def show_coupon_list(callback_query: types.CallbackQuery):
@router.callback_query(F.data == "coupons", IsAdminFilter())
async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
try:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
coupons = await get_all_coupons()
coupons = await get_all_coupons(session)
if not coupons:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
markup = builder.as_markup()
await callback_query.message.answer(
"❌ На данный момент нет доступных купонов.\n"
"Вы можете вернуться в меню управления.",
parse_mode="HTML",
reply_markup=markup,
"❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
return
coupon_list = "📜 Список всех купонов:\n\n"
@@ -69,10 +56,10 @@ async def show_coupon_list(callback_query: types.CallbackQuery):
for coupon in coupons:
coupon_list += (
f"<b>Код:</b> {coupon['code']}\n"
f"<b>Сумма:</b> {coupon['amount']} рублей\n"
f"<b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
f"<b>Использовано:</b> {coupon['usage_count']} раз\n\n"
f"🏷️ <b>Код:</b> {coupon['code']}\n"
f"💰 <b>Сумма:</b> {coupon['amount']} рублей\n"
f"🔢 <b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
f"<b>Использовано:</b> {coupon['usage_count']} раз\n\n"
)
builder.row(
@@ -85,76 +72,62 @@ async def show_coupon_list(callback_query: types.CallbackQuery):
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
markup = builder.as_markup()
await callback_query.message.answer(
coupon_list, parse_mode="HTML", reply_markup=markup
coupon_list, reply_markup=builder.as_markup()
)
except Exception as e:
logger.error(f"Ошибка при получении списка купонов: {e}")
await callback_query.message.answer(
f"❌ Произошла ошибка при получении списка купонов: {e}", parse_mode="HTML"
)
await callback_query.answer()
@router.callback_query(F.data.startswith("delete_coupon_"))
async def handle_delete_coupon(callback_query: types.CallbackQuery):
@router.callback_query(F.data.startswith("delete_coupon_"), IsAdminFilter())
async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any):
coupon_code = callback_query.data[len("delete_coupon_") :]
try:
result = await delete_coupon_from_db(coupon_code)
result = await delete_coupon_from_db(coupon_code, session)
if result:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await show_coupon_list(callback_query)
await show_coupon_list(callback_query, session)
else:
await callback_query.message.answer(
f"❌ Купон с кодом <b>{coupon_code}</b> не найден.", parse_mode="HTML"
f"❌ Купон с кодом <b>{coupon_code}</b> не найден.",
)
await show_coupon_list(callback_query)
await show_coupon_list(callback_query, session)
except Exception as e:
logger.error(f"Ошибка при удалении купона: {e}")
await callback_query.message.answer(
f"❌ Произошла ошибка при удалении купона: {e}", parse_mode="HTML"
)
await callback_query.answer()
@router.callback_query(F.data == "create_coupon")
@router.callback_query(F.data == "create_coupon", IsAdminFilter())
async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
await callback_query.message.answer(
"<b>Введите данные для создания купона в формате:</b>\n\n"
"<i>код</i> <i>сумма</i> <i>лимит</i>\n\n"
"Пример: <b>'COUPON1 50 5'</b>\n\n",
parse_mode="HTML",
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\n\n"
"Пример: <b>'COUPON1 50 5'</b> 👈\n\n",
reply_markup=builder.as_markup(),
)
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
await callback_query.answer()
@router.message(AdminCouponsState.waiting_for_coupon_data)
async def process_coupon_data(message: types.Message, state: FSMContext):
@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter())
async def process_coupon_data(message: types.Message, state: FSMContext, session: Any):
text = message.text.strip()
parts = text.split()
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
if len(parts) != 3:
await message.answer(
"<b>Некорректный формат!</b> Пожалуйста, введите данные в формате:\n"
"<b>код</b> <b>сумма</b> <b>лимит</b>\n"
"Пример: <b>'COUPON1 50 5'</b>",
parse_mode="HTML",
"<b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
"🏷️ <b>код</b> 💰 <b>сумма</b> 🔢 <b>лимит</b>\n"
"Пример: <b>'COUPON1 50 5'</b> 👈",
reply_markup=builder.as_markup(),
)
return
@@ -164,44 +137,28 @@ async def process_coupon_data(message: types.Message, state: FSMContext):
usage_limit = int(parts[2])
except ValueError:
await message.answer(
"<b>⚠️ Проверьте правильность введенных данных.</b>\n"
"Сумма должна быть числом, а лимит — целым числом.",
parse_mode="HTML",
"⚠️ <b>Проверьте правильность введенных данных!</b>\n"
"💱 Сумма должна быть числом, 🔢 а лимит — целым числом.",
reply_markup=builder.as_markup(),
)
return
try:
await create_coupon(coupon_code, coupon_amount, usage_limit)
await create_coupon(coupon_code, coupon_amount, usage_limit, session)
result_message = (
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
f"Сумма: <b>{coupon_amount} рублей</b>\n"
f"Лимит использования: <b>{usage_limit} раз</b>."
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан! 🎉\n"
f"Сумма: <b>{coupon_amount} рублей</b> 💰\n"
f"Лимит использования: <b>{usage_limit} раз</b> 🔢."
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
markup = builder.as_markup()
try:
await message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await message.answer(result_message, parse_mode="HTML", reply_markup=markup)
await message.answer(result_message, reply_markup=builder.as_markup())
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
await message.answer(
f"<b>❌ Ошибка при создании купона:</b> {e}", parse_mode="HTML"
)
@router.callback_query(F.data == "back_to_coupons_menu")
async def back_to_coupons_menu(callback_query: types.CallbackQuery):
"""Возвращаем пользователя в меню управления купонами"""
await show_coupon_management_menu(callback_query)
+283 -83
View File
@@ -1,19 +1,19 @@
import subprocess
from datetime import datetime
from io import BytesIO
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from backup import backup_database
from bot import bot
from config import DATABASE_URL
from filters.admin import IsAdminFilter
from handlers.admin.admin_commands import send_message_to_all_clients
from logger import logger
router = Router()
@@ -21,10 +21,21 @@ router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
displaying_user_info = State()
waiting_for_restart_confirmation = State()
waiting_for_message = State()
@router.message(Command("admin"), IsAdminFilter())
async def handle_admin_command(message: types.Message):
@router.callback_query(F.data == "admin", IsAdminFilter())
async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext):
await handle_admin_message(callback_query.message, state)
@router.message(Command("admin"), F.data == "admin", IsAdminFilter())
async def handle_admin_message(message: types.Message, state: FSMContext):
await state.clear()
BOT_VERSION = "3.2.4-beta" # Укажите текущую версию бота
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
@@ -36,6 +47,11 @@ async def handle_admin_command(message: types.Message):
text="👥 Управление пользователями", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(
text="🖥️ Управление серверами", callback_data="servers_editor"
)
)
builder.row(
InlineKeyboardButton(
text="🎟️ Управление купонами", callback_data="coupons_editor"
@@ -44,72 +60,290 @@ async def handle_admin_command(message: types.Message):
builder.row(
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")
)
builder.row(
InlineKeyboardButton(
text="🤖 Управление Ботом", callback_data="bot_management"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
await message.answer(
f"🤖 Панель администратора\n\nВерсия бота: <b>{BOT_VERSION}</b>",
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
@router.callback_query(F.data == "bot_management")
async def handle_bot_management(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")
)
builder.row(
InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")
)
await bot.send_message(
message.chat.id, "🤖 Панель администратора", reply_markup=builder.as_markup()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin")
)
await callback_query.message.answer(
"🤖 Управление ботом",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "user_stats", IsAdminFilter())
async def user_stats_menu(callback_query: CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)
async def user_stats_menu(callback_query: CallbackQuery, session: Any):
try:
total_users = await conn.fetchval("SELECT COUNT(*) FROM connections")
total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals")
total_users = await session.fetchval("SELECT COUNT(*) FROM connections")
total_keys = await session.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await session.fetchval("SELECT COUNT(*) FROM referrals")
active_keys = await conn.fetchval(
total_payments_today = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= CURRENT_DATE"
)
total_payments_week = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)"
)
total_payments_month = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
total_payments_all_time = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments"
)
active_keys = await session.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
int(datetime.utcnow().timestamp() * 1000),
)
expired_keys = total_keys - active_keys
stats_message = (
f"📈 <b>Подробная статистика проекта:</b>\n\n"
f"👤 Зарегистрированных пользователей: <b>{total_users}</b>\n"
f"🔑 Всего сгенерированных ключей: <b>{total_keys}</b>\n"
f"🤝 Привлеченных рефералов: <b>{total_referrals}</b>\n"
f"✅ Действующих ключей: <b>{active_keys}</b>\n"
f"❌ Просроченных ключей: <b>{expired_keys}</b>"
f"📊 <b>Подробная статистика проекта:</b>\n\n"
f"👥 Пользователи:\n"
f" 🌐 Зарегистрировано: <b>{total_users}</b>\n"
f" 🤝 Привлеченных рефералов: <b>{total_referrals}</b>\n\n"
f"🔑 Ключи:\n"
f" 🌈 Всего сгенерировано: <b>{total_keys}</b>\n"
f" ✅ Действующих: <b>{active_keys}</b>\n"
f" ❌ Просроченных: <b>{expired_keys}</b>\n\n"
f"💰 Финансовая статистика:\n"
f" 📅 За день: <b>{total_payments_today} ₽</b>\n"
f" 📆 За неделю: <b>{total_payments_week} ₽</b>\n"
f" 📆 За месяц: <b>{total_payments_month} ₽</b>\n"
f" 🏦 За все время: <b>{total_payments_all_time} ₽</b>\n"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats")
)
builder.row(
InlineKeyboardButton(
text="🔙 Вернуться в меню", callback_data="back_to_admin_menu"
text="📥 Выгрузить пользователей в CSV",
callback_data="export_users_csv",
)
)
await callback_query.message.edit_text(
stats_message, reply_markup=builder.as_markup(), parse_mode="HTML"
builder.row(
InlineKeyboardButton(
text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv"
)
)
builder.row(
InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")
)
await callback_query.message.answer(
stats_message, reply_markup=builder.as_markup()
)
except Exception as e:
logger.error(f"Error in user_stats_menu: {e}")
@router.callback_query(F.data == "export_users_csv", IsAdminFilter())
async def export_users_csv(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats"))
try:
users = await session.fetch(
"""
SELECT
u.tg_id,
u.username,
u.first_name,
u.last_name,
u.language_code,
u.is_bot,
c.balance,
c.trial
FROM users u
LEFT JOIN connections c ON u.tg_id = c.tg_id
"""
)
if not users:
await callback_query.message.answer(
"📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup()
)
return
csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n"
for user in users:
csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n"
file_name = BytesIO(csv_data.encode("utf-8-sig"))
file_name.seek(0)
file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv")
await callback_query.message.answer_document(
file,
caption="📥 Экспорт пользователей в CSV",
reply_markup=builder.as_markup(),
)
file_name.close()
except Exception as e:
logger.error(f"Ошибка при экспорте пользователей в CSV: {e}")
await callback_query.message.answer(
"❗ Произошла ошибка при экспорте пользователей.",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "export_payments_csv", IsAdminFilter())
async def export_payments_csv(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats"))
try:
payments = await session.fetch(
"""
SELECT
u.tg_id,
u.username,
u.first_name,
u.last_name,
p.amount,
p.payment_system,
p.status,
p.created_at
FROM users u
JOIN payments p ON u.tg_id = p.tg_id
"""
)
if not payments:
await callback_query.message.answer(
"📭 Нет платежей для экспорта.", reply_markup=builder.as_markup()
)
return
csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV
for payment in payments:
csv_data += f"{payment['tg_id']},{payment['username']},{payment['first_name']},{payment['last_name']},{payment['amount']},{payment['payment_system']},{payment['status']},{payment['created_at']}\n"
file_name = BytesIO(csv_data.encode("utf-8-sig"))
file_name.seek(0)
file = BufferedInputFile(file_name.getvalue(), filename="payments_export.csv")
await callback_query.message.answer_document(
file, caption="📥 Экспорт платежей в CSV", reply_markup=builder.as_markup()
)
file_name.close()
except Exception as e:
logger.error(f"Ошибка при экспорте платежей в CSV: {e}")
await callback_query.message.answer(
"❗ Произошла ошибка при экспорте платежей.",
reply_markup=builder.as_markup(),
)
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(F.data == "send_to_alls", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
await send_message_to_all_clients(callback_query.message, state, from_panel=True)
await callback_query.answer()
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(
"✍️ Введите текст сообщения, который вы хотите отправить всем клиентам 📢🌐:",
reply_markup=builder.as_markup(),
)
await state.set_state(UserEditorState.waiting_for_message)
@router.message(UserEditorState.waiting_for_message, IsAdminFilter())
async def process_message_to_all(
message: types.Message, state: FSMContext, session: Any
):
text_message = message.text
try:
tg_ids = await session.fetch("SELECT tg_id FROM connections")
total_users = len(tg_ids)
success_count = 0
error_count = 0
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
success_count += 1
except Exception as e:
error_count += 1
logger.error(
f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
await message.answer(
f"📤 Рассылка завершена:\n"
f"👥 Всего пользователей: {total_users}\n"
f"✅ Успешно отправлено: {success_count}\n"
f"❌ Не доставлено: {error_count}"
)
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
await state.clear()
@router.callback_query(F.data == "backups", IsAdminFilter())
async def handle_backup(message: Message):
await message.answer("💾 Инициализация резервного копирования базы данных...")
async def handle_backup(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.answer(
"💾 Инициализация резервного копирования базы данных..."
)
await backup_database()
await message.answer(
await callback_query.message.answer(
"✅ Резервная копия успешно создана и отправлена администратору."
)
@router.callback_query(F.data == "restart_bot", IsAdminFilter())
async def handle_restart(callback_query: CallbackQuery):
async def handle_restart(callback_query: CallbackQuery, state: FSMContext):
await state.set_state(UserEditorState.waiting_for_restart_confirmation)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Да, перезапустить", callback_data="confirm_restart"
),
InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"),
)
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
await callback_query.message.answer(
"🤔 Вы уверены, что хотите перезапустить бота?",
reply_markup=builder.as_markup(),
)
@router.callback_query(
F.data == "confirm_restart",
UserEditorState.waiting_for_restart_confirmation,
IsAdminFilter(),
)
async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
try:
subprocess.run(
["sudo", "systemctl", "restart", "bot.service"],
@@ -117,10 +351,18 @@ async def handle_restart(callback_query: CallbackQuery):
capture_output=True,
text=True,
)
await callback_query.message.answer("🔄 Бот успешно перезапущен.")
except subprocess.CalledProcessError as e:
await state.clear()
await callback_query.message.answer(
f"⚠️ Перезагрузка бота будет выполнена через 30 секунд. Детали: {e.stderr}"
"🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()
)
except subprocess.CalledProcessError:
await callback_query.message.answer(
"🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()
)
except Exception as e:
await callback_query.message.answer(
f"⚠️ Ошибка при перезагрузке бота: {e.stderr}",
reply_markup=builder.as_markup(),
)
@@ -129,7 +371,8 @@ async def user_editor_menu(callback_query: CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔍 Поиск по названию ключа", callback_data="search_by_key_name"
text="🔍 Поиск по названию ключа",
callback_data="search_by_key_name",
)
)
builder.row(
@@ -139,53 +382,10 @@ async def user_editor_menu(callback_query: CallbackQuery):
)
builder.row(
InlineKeyboardButton(
text="🔙 Вернуться назад", callback_data="back_to_admin_menu"
text="🌐 Поиск по Username", callback_data="search_by_username"
)
)
await callback_query.message.edit_text(
builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin"))
await callback_query.message.answer(
"👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "back_to_admin_menu", IsAdminFilter())
async def back_to_admin_menu(callback_query: CallbackQuery):
try:
await callback_query.message.delete()
except Exception:
pass
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📊 Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(
text="👥 Управление пользователями", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")
)
builder.row(
InlineKeyboardButton(
text="🎟️ Управление купонами", callback_data="coupons_editor"
)
)
builder.row(
InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")
)
builder.row(
InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")
)
await bot.send_message(
tg_id, "🤖 Панель администратора", reply_markup=builder.as_markup()
)
async def handle_error(tg_id, callback_query, message):
await bot.edit_message_text(
message, chat_id=tg_id, message_id=callback_query.message.message_id
)
+531
View File
@@ -0,0 +1,531 @@
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers_from_db
from filters.admin import IsAdminFilter
router = Router()
class UserEditorState(StatesGroup):
waiting_for_cluster_name = State()
waiting_for_api_url = State()
waiting_for_inbound_id = State()
waiting_for_server_name = State()
waiting_for_subscription_url = State()
@router.callback_query(F.data == "servers_editor", IsAdminFilter())
async def handle_servers_editor(callback_query: types.CallbackQuery):
servers = await get_servers_from_db()
builder = InlineKeyboardBuilder()
for cluster_name, cluster_servers in servers.items():
builder.row(
InlineKeyboardButton(
text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}"
)
)
builder.row(
InlineKeyboardButton(text=" Добавить кластер", callback_data="add_cluster")
)
builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin"))
await callback_query.message.answer(
"<b>🔧 Управление кластерами</b>\n\n"
"<i>📌 Здесь вы можете добавить новый кластер.</i>\n\n"
"<i>🌐 <b>Кластеры</b> — это пространство серверов, в пределах которого создается подписка.</i>\n"
"💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n"
"<i>⚠️ <b>Важно:</b> Кластеры удаляются автоматически, если удалить все серверы внутри них.</i>\n\n",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "add_cluster", IsAdminFilter())
async def handle_add_cluster(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.answer(
"🔧 <b>Введите имя нового кластера:</b>\n\n"
"<b>Имя кластера должно быть уникальным и на английском языке.</b>\n"
"<i>Пример:</i> <code>cluster2</code> или <code>us_east_1</code>",
parse_mode="HTML",
)
await state.set_state(UserEditorState.waiting_for_cluster_name)
@router.message(UserEditorState.waiting_for_cluster_name, IsAdminFilter())
async def handle_cluster_name_input(message: types.Message, state: FSMContext):
cluster_name = message.text.strip()
if cluster_name == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
await message.answer(
"Процесс создания кластера отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
)
return
if not cluster_name:
await message.answer("❌ Имя кластера не может быть пустым. Попробуйте снова.")
return
await state.update_data(cluster_name=cluster_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await message.answer(
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
"Рекомендуется указать локацию сервера в имени.\n\n"
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await state.set_state(UserEditorState.waiting_for_server_name)
@router.message(UserEditorState.waiting_for_server_name, IsAdminFilter())
async def handle_server_name_input(message: types.Message, state: FSMContext):
server_name = message.text.strip()
if server_name == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
)
return
if not server_name:
await message.answer("❌ Имя сервера не может быть пустым. Попробуйте снова.")
return
server_unique = await check_unique_server_name(server_name)
if not server_unique:
await message.answer(
"❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя."
)
return
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
await state.update_data(server_name=server_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await message.answer(
f"<b>Введите API URL для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"API URL должен быть в следующем формате:\n\n"
"<code>https://your_domain:port/panel_path</code>\n\n"
"URL должен быть без слэша на конце!\n",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await state.set_state(UserEditorState.waiting_for_api_url)
@router.message(UserEditorState.waiting_for_api_url, IsAdminFilter())
async def handle_api_url_input(message: types.Message, state: FSMContext):
api_url = message.text.strip()
if api_url == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
)
return
if not api_url.startswith("https://"):
await message.answer(
"❌ API URL должен начинаться с <code>https://</code>. Попробуйте снова.",
parse_mode="HTML",
)
return
api_url = api_url.rstrip("/")
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
server_name = user_data.get("server_name")
await state.update_data(api_url=api_url)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await message.answer(
f"<b>Введите subscription_url для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"Subscription URL должен быть в следующем формате:\n\n"
"<code>https://your_domain:port_sub/sub_path</code>\n\n"
"URL должен быть без слэша и имени клиента на конце!\n"
"Его можно увидеть в панели 3x-ui в информации о клиенте.",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await state.set_state(UserEditorState.waiting_for_subscription_url)
@router.message(UserEditorState.waiting_for_subscription_url, IsAdminFilter())
async def handle_subscription_url_input(message: types.Message, state: FSMContext):
subscription_url = message.text.strip()
if subscription_url == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
)
return
if not subscription_url.startswith("https://"):
await message.answer(
"❌ subscription_url должен начинаться с <code>https://</code>. Попробуйте снова.",
parse_mode="HTML",
)
return
subscription_url = subscription_url.rstrip("/")
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
server_name = user_data.get("server_name")
await state.update_data(subscription_url=subscription_url)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await message.answer(
f"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"Это номер подключения vless в вашей панели 3x-ui. Обычно это <b>1</b> при чистой настройке по гайду.\n\n",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await state.set_state(UserEditorState.waiting_for_inbound_id)
@router.message(UserEditorState.waiting_for_inbound_id, IsAdminFilter())
async def handle_inbound_id_input(message: types.Message, state: FSMContext):
inbound_id = message.text.strip()
if not inbound_id.isdigit():
await message.answer(
"❌ inbound_id должен быть числовым значением. Попробуйте снова."
)
return
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
server_name = user_data.get("server_name")
api_url = user_data.get("api_url")
subscription_url = user_data.get("subscription_url")
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute(
"""
INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id)
VALUES ($1, $2, $3, $4, $5)
""",
cluster_name,
server_name,
api_url,
subscription_url,
inbound_id,
)
await conn.close()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад к кластерам", callback_data="servers_editor"
)
)
await message.answer(
f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!",
reply_markup=builder.as_markup(),
)
await state.clear()
@router.callback_query(F.data.startswith("manage_cluster|"), IsAdminFilter())
async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMContext):
cluster_name = callback_query.data.split("|")[1]
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
builder = InlineKeyboardBuilder()
for server in cluster_servers:
builder.row(
InlineKeyboardButton(
text=f"🌍 {server['server_name']}",
callback_data=f"manage_server|{server['server_name']}",
)
)
builder.row(
InlineKeyboardButton(
text=" Добавить сервер", callback_data=f"add_server|{cluster_name}"
)
)
builder.row(
InlineKeyboardButton(
text="🌐 Доступность серверов",
callback_data=f"server_availability|{cluster_name}",
)
)
builder.row(
InlineKeyboardButton(
text="💾 Создать бэкап кластера",
callback_data=f"backup_cluster|{cluster_name}",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад в управление кластерами", callback_data="servers_editor"
)
)
await callback_query.message.answer(
f"🔧 Управление серверами для кластера {cluster_name}",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter())
async def handle_check_server_availability(callback_query: types.CallbackQuery):
cluster_name = callback_query.data.split("|")[1]
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
if not cluster_servers:
await callback_query.answer(f"Кластер '{cluster_name}' не содержит серверов.")
return
in_progress_message = await callback_query.message.answer(
f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n"
"Это может занять до 1 минуты, пожалуйста, подождите..."
)
availability_message = (
f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
)
for server in cluster_servers:
xui = AsyncApi(
server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD
)
try:
await xui.login()
online_users = len(await xui.client.online())
availability_message += (
f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
)
except Exception as e:
availability_message += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"
)
)
await in_progress_message.edit_text(
availability_message, reply_markup=builder.as_markup()
)
await callback_query.answer()
@router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter())
async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext):
server_name = callback_query.data.split("|")[1]
servers = await get_servers_from_db()
server = None
cluster_name = None
for cluster, cluster_servers in servers.items():
server = next(
(s for s in cluster_servers if s["server_name"] == server_name), None
)
if server:
cluster_name = cluster
break
if server:
api_url = server["api_url"]
subscription_url = server["subscription_url"]
inbound_id = server["inbound_id"]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🗑️ Удалить", callback_data=f"delete_server|{server_name}"
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"
)
)
await callback_query.message.answer(
f"<b>🔧 Информация о сервере {server_name}:</b>\n\n"
f"<b>📡 API URL:</b> {api_url}\n"
f"<b>🌐 Subscription URL:</b> {subscription_url}\n"
f"<b>🔑 Inbound ID:</b> {inbound_id}",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await callback_query.message.answer("❌ Сервер не найден.")
@router.callback_query(F.data.startswith("delete_server|"), IsAdminFilter())
async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMContext):
server_name = callback_query.data.split("|")[1]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Да", callback_data=f"confirm_delete_server|{server_name}"
),
InlineKeyboardButton(
text="❌ Нет", callback_data=f"manage_server|{server_name}"
),
)
await callback_query.message.answer(
f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter())
async def handle_confirm_delete_server(
callback_query: types.CallbackQuery, state: FSMContext
):
server_name = callback_query.data.split("|")[1]
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute(
"""
DELETE FROM servers WHERE server_name = $1
""",
server_name,
)
await conn.close()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в управление кластерами", callback_data="servers_editor"
)
)
await callback_query.message.answer(
f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup()
)
@router.callback_query(F.data.startswith("add_server|"), IsAdminFilter())
async def handle_add_server(callback_query: types.CallbackQuery, state: FSMContext):
cluster_name = callback_query.data.split("|")[1]
await state.update_data(cluster_name=cluster_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await callback_query.message.answer(
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
"Рекомендуется указать локацию сервера в имени.\n\n"
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await state.set_state(UserEditorState.waiting_for_server_name)
@router.callback_query(F.data.startswith("backup_cluster|"), IsAdminFilter())
async def handle_backup_cluster(callback_query: types.CallbackQuery):
cluster_name = callback_query.data.split("|")[1]
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
for server in cluster_servers:
xui = AsyncApi(
server["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
await create_backup_and_send_to_admins(xui)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor"))
await callback_query.message.answer(
f"<b>Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!</b>\n\n"
f"🔔 <i>Бэкапы отправлены в боты панелей.</i>",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
GIFT = "🎁 Подарить подписку"
MY_GIFTS = "🎁 Мои подарки"
PROFILE = "👤 Личный кабинет"
BACK = "🔙 Назад"
GIFTS_ABOUT = "<b>Дарите подарки и следите, чтобы они дошли до адресата! 🎄</b>"
+8
View File
@@ -0,0 +1,8 @@
ADD_SUB = " Устройство"
MY_SUBS = "📱 Мои устройства"
PAYMENT = "💳 Пополнить баланс"
INVITE = "👥 Пригласить"
GIFTS = "🎁 Подарить"
TARRIFS = "💡 Тарифы"
INSTRUCTIONS = "📘 Инструкции"
MAIN_MENU = "⬅️ Главное меню"
+3
View File
@@ -0,0 +1,3 @@
PAY = "Пополнить"
BACK = "⬅️ Назад"
CUSTOM_SUM = "💰 Ввести свою сумму"
-17
View File
@@ -1,17 +0,0 @@
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from handlers.start import start_command
router = Router()
@router.message(Command("start"))
async def handle_start(message: types.Message, state: FSMContext):
await start_command(message)
@router.message(Command("menu"))
async def handle_menu(message: types.Message, state: FSMContext):
await start_command(message)
+48 -79
View File
@@ -1,15 +1,13 @@
from datetime import datetime
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL
from database import update_balance
from logger import logger
class CouponActivationState(StatesGroup):
@@ -23,106 +21,77 @@ router = Router()
async def handle_activate_coupon(
callback_query: types.CallbackQuery, state: FSMContext
):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
"<b>Введите код купона:</b>\n\n"
"Пожалуйста, введите действующий код купона, который вы хотите активировать.",
parse_mode="HTML",
"<b>🎫 Введите код купона:</b>\n\n"
"📝 Пожалуйста, введите действующий код купона, который вы хотите активировать. 🔑",
reply_markup=builder.as_markup(),
)
await state.set_state(CouponActivationState.waiting_for_coupon_code)
await callback_query.answer()
@router.message(CouponActivationState.waiting_for_coupon_code)
async def process_coupon_code(message: types.Message, state: FSMContext):
try:
await message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
async def process_coupon_code(message: types.Message, state: FSMContext, session: Any):
coupon_code = message.text.strip()
activation_result = await activate_coupon(message.from_user.id, coupon_code)
activation_result = await activate_coupon(message.chat.id, coupon_code, session)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
activation_result, reply_markup=builder.as_markup(), parse_mode="HTML"
)
markup = builder.as_markup()
await message.answer(activation_result, reply_markup=markup, parse_mode="HTML")
await state.clear()
async def activate_coupon(user_id: int, coupon_code: str):
"""Функция для активации купона"""
conn = await asyncpg.connect(DATABASE_URL)
async def activate_coupon(user_id: int, coupon_code: str, session: Any):
coupon_record = await session.fetchrow(
"""
SELECT id, usage_limit, usage_count, is_used, amount
FROM coupons
WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
""",
coupon_code,
)
try:
coupon_record = await conn.fetchrow(
if not coupon_record:
return "<b>❌ Купон не найден</b> 🚫 или его использование ограничено. 🔒 Пожалуйста, проверьте код и попробуйте снова. 🔍"
usage_exists = await session.fetchrow(
"""
SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2
""",
coupon_record["id"],
user_id,
)
if usage_exists:
return "<b>❌ Вы уже активировали этот купон.</b> 🚫 Купоны могут быть активированы только один раз. 🔒"
coupon_amount = coupon_record["amount"]
async with session.transaction():
await session.execute(
"""
SELECT id, usage_limit, usage_count, is_used, amount
FROM coupons
WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
UPDATE coupons
SET usage_count = usage_count + 1,
is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END
WHERE id = $1
""",
coupon_code,
coupon_record["id"],
)
if not coupon_record:
return "<b>❌ Купон не найден</b> или его использование ограничено. Пожалуйста, проверьте код и попробуйте снова."
usage_exists = await conn.fetchrow(
await session.execute(
"""
SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2
INSERT INTO coupon_usages (coupon_id, user_id, used_at)
VALUES ($1, $2, $3)
""",
coupon_record["id"],
user_id,
datetime.utcnow(),
)
if usage_exists:
return "<b>❌ Вы уже активировали этот купон.</b> Купоны могут быть активированы только один раз."
coupon_amount = coupon_record["amount"]
async with conn.transaction():
await conn.execute(
"""
UPDATE coupons
SET usage_count = usage_count + 1,
is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END
WHERE id = $1
""",
coupon_record["id"],
)
await conn.execute(
"""
INSERT INTO coupon_usages (coupon_id, user_id, used_at)
VALUES ($1, $2, $3)
""",
coupon_record["id"],
user_id,
datetime.utcnow(),
)
await update_balance(user_id, coupon_amount)
return f"<b>✅ Купон успешно активирован!</b>\n\nНа ваш баланс добавлено <b>{coupon_amount} рублей</b>."
except Exception as e:
logger.error(f"Ошибка при активации купона: {e}")
return (
"<b>⚠️ Произошла ошибка при активации купона.</b>\nПопробуйте ещё раз позже."
)
finally:
await conn.close()
await update_balance(user_id, coupon_amount)
return f"<b>✅ Купон успешно активирован! 🎉</b>\n\nНа ваш баланс добавлено <b>{coupon_amount} рублей</b> 💰."
+15 -47
View File
@@ -4,7 +4,6 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import RUB_TO_XTR
from logger import logger
@@ -20,11 +19,7 @@ router = Router()
@router.callback_query(F.data == "donate")
async def process_donate(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
@@ -34,39 +29,34 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести сумму доната", callback_data="enter_custom_donate_amount"
text="💰 Ввести сумму доната",
callback_data="enter_custom_donate_amount",
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await bot.send_message(
chat_id=callback_query.from_user.id,
text="🌟 Поддержите наш проект!\n\n"
"Каждый донат помогает развивать и улучшать сервис. "
"Мы ценим вашу поддержку и работаем над тем, чтобы сделать наш продукт еще лучше. 💡",
await callback_query.message.answer(
text="🌟 Поддержите наш проект! 💪\n\n"
"💖 Каждый донат помогает развивать и улучшать сервис. "
"🤝 Мы ценим вашу поддержку и работаем над тем, чтобы сделать наш продукт еще лучше. 🚀💡",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "enter_custom_donate_amount")
async def process_enter_donate_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(f"💸 Введите сумму доната в рублях:")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate"))
await callback_query.message.answer(
"💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup()
)
await state.set_state(DonateState.entering_donate_amount)
await callback_query.answer()
@router.message(DonateState.entering_donate_amount)
async def process_donate_amount_input(message: types.Message, state: FSMContext):
try:
await message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_XTR <= 0:
@@ -95,7 +85,6 @@ async def process_donate_amount_input(message: types.Message, state: FSMContext)
await state.set_state(DonateState.waiting_for_donate_payment)
except Exception as e:
logger.error(f"Ошибка при создании доната: {e}")
await message.answer("Произошла ошибка при создании доната.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
@@ -108,37 +97,16 @@ async def on_pre_checkout_query(pre_checkout_query: PreCheckoutQuery):
@router.message(F.successful_payment, DonateState.waiting_for_donate_payment)
async def on_successful_donate(message: types.Message, state: FSMContext):
try:
user_id = int(message.from_user.id)
amount = float(message.successful_payment.invoice_payload.split("_")[0])
logger.debug(f"Donate succeeded for user_id: {user_id}, amount: {amount}")
state_data = await state.get_data()
previous_message_id = state_data.get("last_message_id")
if previous_message_id:
try:
await bot.delete_message(
chat_id=user_id, message_id=previous_message_id
)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Вернуться в профиль", callback_data="view_profile"
)
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
sent_message = await bot.send_message(
chat_id=user_id,
await message.answer(
text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖",
reply_markup=builder.as_markup(),
)
await state.update_data(last_message_id=sent_message.message_id)
await state.clear()
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
except Exception as e:
+11
View File
@@ -0,0 +1,11 @@
__all__ = ("router",)
from aiogram import Router
from .instructions import router as instructions_router
router = Router(name="instructions_main_router")
router.include_routers(
instructions_router,
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

+119 -39
View File
@@ -1,77 +1,157 @@
import os
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import CONNECT_WINDOWS, SUPPORT_CHAT_URL
from handlers.texts import INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE
from logger import logger
from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
from handlers.texts import CONNECT_TV_TEXT, INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE, SUBSCRIPTION_DETAILS_TEXT
router = Router()
async def send_instructions(callback_query: types.CallbackQuery):
await callback_query.message.delete()
@router.callback_query(F.data == "instructions")
@router.message(F.text == "/instructions")
async def send_instructions(callback_query_or_message: types.CallbackQuery | types.Message):
instructions_message = INSTRUCTIONS
image_path = os.path.join(os.path.dirname(__file__), "instructions.jpg")
image_path = os.path.join("img", "instructions.jpg")
if not os.path.isfile(image_path):
await callback_query.message.answer("Файл изображения не найден.")
await callback_query.answer()
if isinstance(callback_query_or_message, types.CallbackQuery):
await callback_query_or_message.message.answer("Файл изображения не найден.")
else:
await callback_query_or_message.answer("Файл изображения не найден.")
return
back_button = InlineKeyboardButton(
text="⬅️ Вернуться в профиль", callback_data="view_profile"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"),
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
if isinstance(callback_query_or_message, types.CallbackQuery):
send_photo = callback_query_or_message.message.answer_photo
else:
send_photo = callback_query_or_message.answer_photo
with open(image_path, "rb") as image_from_buffer:
await callback_query.message.answer_photo(
await send_photo(
BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"),
caption=instructions_message,
parse_mode="Markdown",
reply_markup=keyboard,
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data.startswith("connect_pc|"))
async def process_connect_pc(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
key = callback_query.data.split("|")[1]
async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
record = await session.fetchrow(
"""
SELECT k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
)
if not record:
await callback_query.message.answer(
"❌ <b>Ключ не найден. Проверьте имя ключа.</b> 🔍"
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
return
key = record["key"]
key_message = KEY_MESSAGE.format(key)
instruction_message = f"{key_message}{INSTRUCTION_PC}"
connect_windows_button = types.InlineKeyboardButton(
text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"
)
)
builder.row(
InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_MACOS}{key}")
)
builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
instruction_message,
reply_markup=builder.as_markup(),
parse_mode="HTML",
disable_web_page_preview=True,
)
support_button = types.InlineKeyboardButton(
text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}"
@router.callback_query(F.data.startswith("connect_tv|"))
async def process_connect_tv(callback_query: types.CallbackQuery):
key_name = callback_query.data.split("|")[1]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="▶ Продолжить", callback_data=f"continue_tv|{key_name}"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
back_button = types.InlineKeyboardButton(
text="🔙 Назад в профиль", callback_data="view_profile"
await callback_query.message.answer(
text=CONNECT_TV_TEXT,
reply_markup=builder.as_markup(),
parse_mode="HTML",
disable_web_page_preview=True
)
inline_keyboard = [[connect_windows_button], [support_button], [back_button]]
keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
await bot.send_message(
tg_id, instruction_message, reply_markup=keyboard, parse_mode="HTML"
@router.callback_query(F.data.startswith("continue_tv|"))
async def process_continue_tv(callback_query: types.CallbackQuery):
key_name = callback_query.data.split("|")[1]
tg_id = callback_query.from_user.id
print(f"tg_id: {tg_id}, key_name: {key_name}") # Отладочный вывод
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"""
SELECT k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
)
print(f"Query result: {record}") # Отладочный вывод результата
finally:
await conn.close()
subscription_link = record['key']
message_text = SUBSCRIPTION_DETAILS_TEXT.format(subscription_link=subscription_link)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
await callback_query.answer()
await callback_query.message.answer(
text=message_text,
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
+13
View File
@@ -0,0 +1,13 @@
__all__ = ("router",)
from aiogram import Router
from .key_management import router as management_router
from .keys import router as keys_router
router = Router(name="keys_main_router")
router.include_routers(
keys_router,
management_router,
)
+138 -235
View File
@@ -1,21 +1,28 @@
import asyncio
import uuid
from datetime import datetime, timedelta
from typing import Any
import asyncpg
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot, dp
from config import CONNECT_ANDROID, CONNECT_IOS, DATABASE_URL, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK
from database import add_connection, get_balance, store_key, update_balance
from handlers.instructions.instructions import send_instructions
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
PUBLIC_LINK,
RENEWAL_PRICES,
SUPPORT_CHAT_URL,
TRIAL_TIME,
)
from database import get_balance, get_trial, store_key, update_balance
from handlers.keys.key_utils import create_key_on_cluster
from handlers.profile import process_callback_view_profile
from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, RENEWAL_PLANS, key_message_success
from handlers.utils import get_least_loaded_cluster, sanitize_key_name
from handlers.texts import DISCOUNTS, key_message_success
from handlers.utils import generate_random_email, get_least_loaded_cluster
from logger import logger
router = Router()
@@ -28,220 +35,115 @@ class Form(StatesGroup):
waiting_for_message = State()
@dp.callback_query(F.data == "create_key")
async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
server_id = "все сервера"
await state.update_data(selected_server_id=server_id)
await select_server(callback_query, state)
await callback_query.answer()
async def select_server(callback_query: CallbackQuery, state: FSMContext):
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_connection = await conn.fetchrow(
"SELECT trial FROM connections WHERE tg_id = $1",
callback_query.from_user.id,
)
finally:
await conn.close()
trial_status = existing_connection["trial"] if existing_connection else 0
if trial_status == 1:
await bot.send_message(
chat_id=callback_query.from_user.id,
text=KEY,
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="✅ Да, подключить новое устройство",
callback_data="confirm_create_new_key",
)
],
[
InlineKeyboardButton(
text="↩️ Назад", callback_data="cancel_create_key"
)
],
]
),
)
await state.update_data(creating_new_key=True)
else:
await bot.send_message(
chat_id=callback_query.from_user.id, text=KEY_TRIAL, parse_mode="HTML"
)
await state.set_state(Form.waiting_for_key_name)
await callback_query.answer()
@dp.callback_query(F.data == "confirm_create_new_key")
async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
@router.callback_query(F.data == "create_key")
async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} confirmed creation of a new key.")
balance = await get_balance(tg_id)
if balance < RENEWAL_PLANS["1"]["price"]:
replenish_button = InlineKeyboardButton(
text="Перейти в профиль", callback_data="view_profile"
logger.info(f"Balance for user {tg_id} is sufficient. Proceeding with key creation.")
await handle_key_creation(tg_id, state, session, callback_query)
async def handle_key_creation(
tg_id: int, state: FSMContext, session: Any, message_or_query: Message | CallbackQuery
):
"""Создание ключа с учётом выбора тарифного плана."""
current_time = datetime.utcnow()
trial_status = await get_trial(tg_id, session)
if trial_status == 0:
expiry_time = current_time + timedelta(days=TRIAL_TIME)
logger.info(f"Assigned 1-day trial to user {tg_id}.")
await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id)
await create_key(tg_id, expiry_time, state, session, message_or_query)
else:
builder = InlineKeyboardBuilder()
for index, (plan_id, price) in enumerate(RENEWAL_PRICES.items()):
discount_text = ""
if plan_id in DISCOUNTS:
discount_percentage = DISCOUNTS[plan_id]
discount_text = f" ({discount_percentage}% скидка)"
if index == len(RENEWAL_PRICES) - 1:
discount_text = f" ({discount_percentage}% 🔥)"
builder.row(
InlineKeyboardButton(
text=f"📅 {plan_id} мес. - {price}{discount_text}", callback_data=f"select_plan_{plan_id}"
)
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message_or_query.message.answer(
"💳 Выберите тарифный план для создания нового ключа:", reply_markup=builder.as_markup()
)
await state.update_data(tg_id=tg_id)
await state.set_state(Form.waiting_for_server_selection)
@router.callback_query(F.data.startswith("select_plan_"))
async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
plan_id = callback_query.data.split("_")[-1]
plan_price = RENEWAL_PRICES.get(plan_id)
if plan_price is None:
await callback_query.message.answer("🚫 Неверный тарифный план.")
return
duration_days = int(plan_id) * 30
balance = await get_balance(tg_id)
if balance < plan_price:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
"💳 Недостаточно средств для создания подписки. Пополните баланс в личном кабинете.",
reply_markup=builder.as_markup(),
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await callback_query.message.edit_text(NULL_BALANCE, reply_markup=keyboard)
await state.clear()
return
logger.info(f"Balance for user {tg_id} is sufficient. Asking for device name.")
await update_balance(tg_id, -plan_price)
await callback_query.message.edit_text(
"🔑 Пожалуйста, введите имя подключаемого устройства:"
)
await state.set_state(Form.waiting_for_key_name)
logger.info(f"State set to waiting_for_key_name for user {tg_id}")
await state.update_data(creating_new_key=True)
expiry_time = datetime.utcnow() + timedelta(days=duration_days)
await callback_query.answer()
await create_key(tg_id, expiry_time, state, session, callback_query)
@dp.callback_query(F.data == "cancel_create_key")
async def cancel_create_key(callback_query: CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
await callback_query.answer()
async def create_key(
tg_id: int, expiry_time: datetime, state: FSMContext, session: Any, message_or_query: Message | CallbackQuery
):
"""Создаёт ключ с заданным сроком действия."""
while True:
key_name = generate_random_email()
logger.info(f"Generated random key name for user {tg_id}: {key_name}")
@router.message(Form.waiting_for_key_name)
async def handle_key_name_input(message: Message, state: FSMContext):
tg_id = message.from_user.id
key_name = sanitize_key_name(message.text)
logger.info(f"User {tg_id} is attempting to create a key with the name: {key_name}")
if not key_name:
await message.bot.send_message(
tg_id, "📝 Пожалуйста, назовите устройство на английском языке."
existing_key = await session.fetchrow(
"SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
key_name,
tg_id,
)
logger.warning(f"User {tg_id} entered an invalid key name: {key_name}")
return
conn = await asyncpg.connect(DATABASE_URL)
try:
logger.info(
f"Checking if key name '{key_name}' already exists in the database."
)
existing_key = await conn.fetchrow(
"SELECT * FROM keys WHERE email = $1", key_name.lower()
)
if existing_key:
await message.bot.send_message(
tg_id,
"❌ Упс! Это имя уже используется. Выберите другое уникальное название для ключа.",
)
logger.warning(
f"Key name '{key_name}' already exists in the database for user {tg_id}."
)
await state.set_state(Form.waiting_for_key_name)
return
finally:
await conn.close()
if not existing_key:
break
logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.")
client_id = str(uuid.uuid4())
email = key_name.lower()
current_time = datetime.utcnow()
expiry_time = None
conn = await asyncpg.connect(DATABASE_URL)
try:
logger.info(f"Checking trial status for user {tg_id}.")
existing_connection = await conn.fetchrow(
"SELECT trial FROM connections WHERE tg_id = $1", tg_id
)
finally:
await conn.close()
trial_status = existing_connection["trial"] if existing_connection else 0
if trial_status == 0:
expiry_time = current_time + timedelta(days=1, hours=3)
logger.info(f"Assigned 1-day trial to user {tg_id}.")
else:
balance = await get_balance(tg_id)
if balance < RENEWAL_PLANS["1"]["price"]:
replenish_button = InlineKeyboardButton(
text="Перейти в профиль", callback_data="view_profile"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await message.bot.send_message(
tg_id,
"💳 Недостаточно средств для создания подписки на новое устройство. Пополните баланс в личном кабинете.",
reply_markup=keyboard,
)
logger.warning(f"User {tg_id} has insufficient funds for key creation.")
await state.clear()
return
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
expiry_time = current_time + timedelta(days=30, hours=3)
logger.info(f"User {tg_id} balance deducted for key creation.")
expiry_timestamp = int(expiry_time.timestamp() * 1000)
public_link = f"{PUBLIC_LINK}{email}"
logger.info(f"Generated public link for the key: {public_link}")
button_profile = InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
button_iphone = InlineKeyboardButton(
text="🍏 Подключить", url=f"{CONNECT_IOS}{public_link}"
)
button_android = InlineKeyboardButton(
text="🤖 Подключить",
url=f"{CONNECT_ANDROID}{public_link}",
)
button_download_ios = InlineKeyboardButton(text="🍏 Скачать", url=DOWNLOAD_IOS)
button_download_android = InlineKeyboardButton(
text="🤖 Скачать",
url=DOWNLOAD_ANDROID,
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[button_download_ios, button_download_android],
[button_iphone, button_android],
[button_profile],
]
)
remaining_time = expiry_time - current_time
days = remaining_time.days
key_message = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
logger.info(f"Sending key message to user {tg_id} with the public link.")
await message.bot.send_message(
tg_id, key_message, parse_mode="HTML", reply_markup=keyboard
)
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
try:
least_loaded_cluster = await get_least_loaded_cluster()
tasks = []
tasks.append(
tasks = [
asyncio.create_task(
create_key_on_cluster(
least_loaded_cluster,
@@ -251,43 +153,44 @@ async def handle_key_name_input(message: Message, state: FSMContext):
expiry_timestamp,
)
)
)
]
await asyncio.gather(*tasks)
logger.info(f"Key created on cluster {least_loaded_cluster} for user {tg_id}.")
conn = await asyncpg.connect(DATABASE_URL)
try:
logger.info(f"Updating trial status for user {tg_id} in the database.")
existing_connection = await conn.fetchrow(
"SELECT * FROM connections WHERE tg_id = $1", tg_id
)
if existing_connection:
await conn.execute(
"UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id
)
else:
await add_connection(tg_id, 0, 1)
finally:
await conn.close()
logger.info(f"Storing key for user {tg_id} in the database.")
await store_key(
tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster
)
await store_key(tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster, session)
except Exception as e:
logger.error(f"Error while creating the key for user {tg_id}: {e}")
await message.bot.send_message(tg_id, f"❌ Ошибка при создании ключа: {e}")
logger.error(f"Error while creating the key for user {tg_id} on cluster: {e}")
if isinstance(message_or_query, Message):
await message_or_query.answer("❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова.")
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(
"❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова."
)
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS),
InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{public_link}"),
InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{public_link}"),
)
builder.row(InlineKeyboardButton(text="💻 Windows/Linux/MacOS", callback_data=f"connect_pc|{email}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
remaining_time = expiry_time - datetime.utcnow()
days = remaining_time.days
key_message = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
if isinstance(message_or_query, Message):
await message_or_query.answer(key_message, reply_markup=builder.as_markup())
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(key_message, reply_markup=builder.as_markup())
await state.clear()
@dp.callback_query(F.data == "instructions")
async def handle_instructions(callback_query: CallbackQuery):
await send_instructions(callback_query)
@dp.callback_query(F.data == "back_to_main")
async def handle_back_to_main(callback_query: CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
await callback_query.answer()
+68 -27
View File
@@ -4,25 +4,34 @@ import asyncpg
from py3xui import AsyncApi
from client import add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, LIMIT_IP, TOTAL_GB
from database import get_servers_from_db
from logger import logger
async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_timestamp):
try:
tasks = []
cluster = CLUSTERS.get(cluster_id)
servers = await get_servers_from_db()
cluster = servers.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
for server_info in cluster.values():
for server_info in cluster:
xui = AsyncApi(
server_info["API_URL"],
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
inbound_id = server_info.get("inbound_id")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_info.get('server_name', 'unknown')}. Пропуск."
)
continue
conn = await asyncpg.connect(DATABASE_URL)
existing_key = await conn.fetchrow(
"SELECT 1 FROM keys WHERE email = $1", email
@@ -37,11 +46,12 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time
client_id,
email,
tg_id,
limit_ip=1,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
)
)
await conn.close()
@@ -54,26 +64,37 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time
async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb):
"""
Функция для продления срока действия ключа на всех серверах в кластере
и обновления объема трафика.
"""
try:
cluster = CLUSTERS.get(cluster_id)
servers = await get_servers_from_db()
cluster = servers.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
tasks = []
for server_info in cluster.values():
for server_info in cluster:
xui = AsyncApi(
server_info["API_URL"],
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
inbound_id = server_info.get("inbound_id")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_info.get('server_name', 'unknown')}. Пропуск."
)
continue
tasks.append(
extend_client_key(xui, email, new_expiry_time, client_id, total_gb)
extend_client_key(
xui,
int(inbound_id),
email,
new_expiry_time,
client_id,
total_gb,
)
)
await asyncio.gather(*tasks)
@@ -85,34 +106,45 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
raise e
async def delete_key_from_db(client_id):
"""Удаление ключа из базы данных"""
async def delete_key_from_db(client_id, session):
try:
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id)
await session.execute("DELETE FROM keys WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
finally:
await conn.close()
async def delete_key_from_cluster(cluster_id, email, client_id):
"""Удаление ключа с серверов в кластере"""
try:
cluster = CLUSTERS.get(cluster_id)
servers = await get_servers_from_db()
cluster = servers.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
tasks = []
for server_info in cluster.values():
for server_info in cluster:
xui = AsyncApi(
server_info["API_URL"],
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
tasks.append(delete_client(xui, email, client_id))
inbound_id = server_info.get("inbound_id")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_info.get('server_name', 'unknown')}. Пропуск."
)
continue
tasks.append(
delete_client(
xui,
int(inbound_id),
email,
client_id,
)
)
await asyncio.gather(*tasks)
@@ -125,30 +157,39 @@ async def delete_key_from_cluster(cluster_id, email, client_id):
async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id):
try:
cluster = CLUSTERS.get(cluster_id)
servers = await get_servers_from_db()
cluster = servers.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
tasks = []
for server_info in cluster.values():
for server_info in cluster:
xui = AsyncApi(
server_info["API_URL"],
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
inbound_id = server_info.get("inbound_id")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_info.get('server_name', 'unknown')}. Пропуск."
)
continue
tasks.append(
add_client(
xui,
client_id,
email,
tg_id,
limit_ip=1,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_time,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
)
)
+477 -570
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+162 -23
View File
@@ -1,74 +1,213 @@
import base64
from datetime import datetime
import aiohttp
import asyncpg
from aiohttp import web
from config import CLUSTERS
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, TRANSITION_DATE_STR
from database import get_servers_from_db
from logger import logger
async def fetch_url_content(url):
async def fetch_url_content(url, tg_id):
try:
logger.debug(f"Получение URL: {url}")
logger.info(f"Получение URL: {url} для tg_id: {tg_id}")
async with aiohttp.ClientSession() as session:
async with session.get(url, ssl=False) as response:
if response.status == 200:
content = await response.text()
logger.debug(f"Успешно получен контент с {url}")
logger.info(f"Успешно получен контент с {url} для tg_id: {tg_id}")
return base64.b64decode(content).decode("utf-8").split("\n")
else:
logger.error(
f"Не удалось получить {url}, статус: {response.status}"
f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}"
)
return []
except Exception as e:
logger.error(f"Ошибка при получении {url}: {e}")
logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}")
return []
async def combine_unique_lines(urls, query_string):
async def combine_unique_lines(urls, tg_id, query_string):
all_lines = []
logger.debug(f"Начинаем объединение подписок для запроса: {query_string}")
logger.info(
f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}"
)
urls_with_query = [f"{url}?{query_string}" for url in urls]
logger.debug(f"Составлены URL-адреса: {urls_with_query}")
logger.info(f"Составлены URL-адреса: {urls_with_query}")
for url in urls_with_query:
lines = await fetch_url_content(url)
lines = await fetch_url_content(url, tg_id)
all_lines.extend(lines)
all_lines = list(set(filter(None, all_lines)))
logger.debug(
f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов"
logger.info(
f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}"
)
return all_lines
async def handle_subscription(request):
email = request.match_info["email"]
logger.info(f"Получен запрос на подписку для email: {email}")
transition_date = datetime.strptime(TRANSITION_DATE_STR, "%Y-%m-%d %H:%M:%S")
urls = []
for cluster in CLUSTERS.values():
for server in cluster.values():
server_subscription_url = f"{server['SUBSCRIPTION']}/{email}"
transition_timestamp_ms = int(transition_date.timestamp() * 1000)
transition_timestamp_ms_adjusted = transition_timestamp_ms - (3 * 60 * 60 * 1000)
logger.info(
f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}"
)
async def handle_old_subscription(request):
email = request.match_info.get("email")
if not email:
logger.warning("Получен запрос без email")
return web.Response(
text="❌ Неверные параметры запроса. Требуется email.",
status=400,
)
logger.info(f"Обработка запроса для старого клиента с email: {email}")
conn = await asyncpg.connect(DATABASE_URL)
try:
key_info = await conn.fetchrow(
"SELECT created_at, server_id FROM keys WHERE email = $1", email
)
if not key_info:
logger.warning(f"Клиент с email {email} не найден в базе.")
return web.Response(
text="❌ Клиент с таким email не найден.",
status=404,
)
created_at_ms = key_info["created_at"]
cluster_name = key_info.get("server_id")
if not cluster_name:
logger.warning(f"У клиента с email {email} отсутствует cluster_name.")
return web.Response(
text="❌ Устаревшие данные. Обратитесь в поддержку.",
status=400,
)
logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}")
created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000)
logger.info(
f"Время создания клиента в формате datetime (UTC): {created_at_datetime}"
)
if created_at_ms >= transition_timestamp_ms_adjusted:
logger.info(f"Клиент с email {email} является новым.")
return web.Response(
text="❌ Эта ссылка устарела. Пожалуйста, обновите ссылку.",
status=400,
)
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
logger.info(f"Сервера в кластере: {cluster_servers}")
urls = []
for server in cluster_servers:
server_subscription_url = f"{server['subscription_url']}/{email}"
urls.append(server_subscription_url)
query_string = request.query_string
logger.debug(f"Извлечен query string: {query_string}")
combined_subscriptions = await combine_unique_lines(urls, email, "")
combined_subscriptions = await combine_unique_lines(urls, query_string)
base64_encoded = base64.b64encode(
"\n".join(combined_subscriptions).encode("utf-8")
).decode("utf-8")
encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}"
headers = {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
"profile-title": "base64:" + base64.b64encode(
encoded_project_name.encode("utf-8")
).decode("utf-8"),
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
return web.Response(text=base64_encoded, headers=headers)
finally:
await conn.close()
async def handle_new_subscription(request):
email = request.match_info.get("email")
tg_id = request.match_info.get("tg_id")
if not email or not tg_id:
logger.warning("Получен запрос с отсутствующими параметрами email или tg_id")
return web.Response(
text="❌ Неверные параметры запроса. Требуются email и tg_id.",
status=400,
)
logger.info(f"Обработка запроса для нового клиента: email={email}, tg_id={tg_id}")
conn = await asyncpg.connect(DATABASE_URL)
try:
client_data = await conn.fetchrow(
"SELECT tg_id, server_id FROM keys WHERE email = $1", email
)
if not client_data:
logger.warning(f"Клиент с email {email} не найден в базе.")
return web.Response(
text="❌ Клиент с таким email не найден.",
status=404,
)
stored_tg_id = client_data["tg_id"]
cluster_name = client_data["server_id"]
if str(tg_id) != str(stored_tg_id):
logger.warning(f"Неверный tg_id для клиента с email {email}.")
return web.Response(
text="❌ Неверные данные. Получите свой ключ в боте.",
status=403,
)
finally:
await conn.close()
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
urls = []
for server in cluster_servers:
server_subscription_url = f"{server['subscription_url']}/{email}"
urls.append(server_subscription_url)
query_string = request.query_string
logger.info(f"Извлечен query string: {query_string}")
combined_subscriptions = await combine_unique_lines(urls, tg_id, query_string)
base64_encoded = base64.b64encode(
"\n".join(combined_subscriptions).encode("utf-8")
).decode("utf-8")
encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}"
headers = {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
"profile-title": email,
"profile-title": "base64:" + base64.b64encode(
encoded_project_name.encode("utf-8")
).decode("utf-8"),
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
+41 -84
View File
@@ -1,111 +1,68 @@
import asyncio
import uuid
from datetime import datetime, timedelta
from typing import Any
import asyncpg
from py3xui import AsyncApi
from client import add_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, PUBLIC_LINK, TRIAL_TIME
from database import store_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME
from database import get_servers_from_db, store_key, use_trial
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
async def create_trial_key(tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
try:
client_id = str(uuid.uuid4())
email = generate_random_email()
async def create_trial_key(tg_id: int, session: Any):
client_id = str(uuid.uuid4())
email = generate_random_email()
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
instructions = INSTRUCTIONS
result = {"key": public_link, "instructions": instructions, "email": email}
current_time = datetime.utcnow()
expiry_time = current_time + timedelta(days=TRIAL_TIME)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
public_link = f"{PUBLIC_LINK}{email}"
instructions = INSTRUCTIONS
clusters = await get_servers_from_db()
result = {"key": public_link, "instructions": instructions}
least_loaded_cluster = await get_least_loaded_cluster()
asyncio.create_task(
generate_and_store_keys(tg_id, client_id, email, public_link)
)
if least_loaded_cluster not in clusters:
raise ValueError(f"Кластер {least_loaded_cluster} не найден в базе данных.")
return result
servers_in_cluster = clusters[least_loaded_cluster]
finally:
await conn.close()
tasks = []
async def generate_and_store_keys(
tg_id: int, client_id: str, email: str, public_link: str
):
conn = await asyncpg.connect(DATABASE_URL)
try:
current_time = datetime.utcnow()
expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
least_loaded_cluster = await get_least_loaded_cluster()
tasks = []
for server_id, server in CLUSTERS[least_loaded_cluster].items():
task = create_key_on_server(
least_loaded_cluster,
server_id,
for server_info in servers_in_cluster:
tasks.append(
add_client(
AsyncApi(
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
),
client_id,
email,
tg_id,
expiry_timestamp,
limit_ip=1,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(server_info["inbound_id"]),
)
tasks.append(task)
await asyncio.gather(*tasks)
await store_key(
tg_id,
client_id,
email,
expiry_timestamp,
public_link,
server_id=least_loaded_cluster,
)
await conn.execute(
"""
INSERT INTO connections (tg_id, trial)
VALUES ($1, 1)
ON CONFLICT (tg_id)
DO UPDATE SET trial = 1
""",
tg_id,
)
finally:
await conn.close()
await asyncio.gather(*tasks)
async def create_key_on_server(
cluster_id: str,
server_id: str,
client_id: str,
email: str,
tg_id: int,
expiry_timestamp: int,
):
"""Создает ключ на сервере в указанном кластере и возвращает результат."""
xui = AsyncApi(
CLUSTERS[cluster_id][server_id]["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
response = await add_client(
xui,
await store_key(
tg_id,
client_id,
email,
tg_id,
limit_ip=1,
total_gb=0,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
expiry_timestamp,
public_link,
server_id=least_loaded_cluster,
session=session,
)
return response
await use_trial(tg_id, session)
return result
+229 -141
View File
@@ -3,13 +3,29 @@ from datetime import datetime, timedelta
import asyncpg
from aiogram import Bot, Router, types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from client import delete_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB
from database import delete_key, get_balance, update_balance, update_key_expiry
from handlers.keys.key_utils import renew_key_in_cluster
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
DATABASE_URL,
DEV_MODE,
RENEWAL_PLANS,
TOTAL_GB,
TRIAL_TIME,
)
from database import (
add_notification,
check_notification_time,
delete_key,
get_balance,
get_servers_from_db,
update_balance,
update_key_expiry,
)
from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED
from logger import logger
router = Router()
@@ -21,14 +37,20 @@ async def notify_expiring_keys(bot: Bot):
conn = await asyncpg.connect(DATABASE_URL)
logger.info("Подключение к базе данных успешно.")
current_time = datetime.utcnow().timestamp() * 1000
threshold_time_10h = (
datetime.utcnow() + timedelta(hours=10)
).timestamp() * 1000
threshold_time_24h = (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000
current_time = int(datetime.utcnow().timestamp() * 1000)
threshold_time_10h = int(
(datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000
)
threshold_time_24h = int(
(datetime.utcnow() + timedelta(days=1)).timestamp() * 1000
)
logger.info("Начало обработки уведомлений.")
await notify_inactive_trial_users(bot, conn)
await asyncio.sleep(1)
await check_online_users()
await asyncio.sleep(1)
await notify_10h_keys(bot, conn, current_time, threshold_time_10h)
await asyncio.sleep(1)
await notify_24h_keys(bot, conn, current_time, threshold_time_24h)
@@ -45,6 +67,8 @@ async def notify_expiring_keys(bot: Bot):
async def is_bot_blocked(bot: Bot, chat_id: int) -> bool:
if DEV_MODE:
return False
try:
member = await bot.get_chat_member(chat_id, bot.id)
blocked = member.status == "left"
@@ -60,7 +84,10 @@ async def is_bot_blocked(bot: Bot, chat_id: int) -> bool:
async def notify_10h_keys(
bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_10h: float
bot: Bot,
conn: asyncpg.Connection,
current_time: float,
threshold_time_10h: float,
):
records = await conn.fetch(
"""
@@ -93,31 +120,19 @@ async def notify_10h_keys(
email=email,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
days_left_message=days_left_message,
price=RENEWAL_PLANS["1"]["price"],
)
if not await is_bot_blocked(bot, tg_id):
if not await is_bot_blocked(bot, tg_id) and not DEV_MODE:
try:
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
],
[
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
],
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
],
]
keyboard = InlineKeyboardBuilder()
keyboard.button(
text="🔄 Продлить VPN", callback_data=f"renew_key|{email}"
)
keyboard.button(text="💳 Пополнить баланс", callback_data="pay")
keyboard.button(text="👤 Личный кабинет", callback_data="profile")
keyboard.adjust(1)
keyboard = keyboard.as_markup()
await bot.send_message(tg_id, message, reply_markup=keyboard)
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
except Exception as e:
@@ -136,7 +151,10 @@ async def notify_10h_keys(
async def notify_24h_keys(
bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_24h: float
bot: Bot,
conn: asyncpg.Connection,
current_time: float,
threshold_time_24h: float,
):
logger.info("Проверка истекших ключей...")
@@ -173,29 +191,28 @@ async def notify_24h_keys(
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
)
if not await is_bot_blocked(bot, tg_id):
if not await is_bot_blocked(bot, tg_id) and not DEV_MODE:
try:
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
],
[
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
],
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
],
]
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f"renew_key|{email}",
)
)
builder.row(
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
)
builder.row(
types.InlineKeyboardButton(
text="👤 Личный кабинет",
callback_data="profile",
)
)
keyboard = builder.as_markup()
await bot.send_message(tg_id, message_24h, reply_markup=keyboard)
logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.")
except Exception as e:
@@ -215,113 +232,184 @@ async def notify_24h_keys(
await asyncio.sleep(1)
async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
logger.info("Проверка пользователей, не активировавших пробный период...")
inactive_trial_users = await conn.fetch(
"""
SELECT tg_id, username FROM users
WHERE tg_id IN (
SELECT tg_id FROM connections
WHERE trial = 0
) AND tg_id NOT IN (
SELECT DISTINCT tg_id FROM keys
)
"""
)
logger.info(f"Найдено {len(inactive_trial_users)} неактивных пользователей.")
for user in inactive_trial_users:
tg_id = user["tg_id"]
username = user.get("username", "Пользователь")
try:
can_notify = await check_notification_time(
tg_id, "inactive_trial", hours=24, session=conn
)
if can_notify and not await is_bot_blocked(bot, tg_id):
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="🚀 Активировать пробный период",
callback_data="create_key",
)
)
builder.row(
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
)
keyboard = builder.as_markup()
message = (
f"👋 Привет, {username}!\n\n"
f"🎉 У тебя есть бесплатный пробный период на {TRIAL_TIME} дней!\n"
"🕒 Не упусти возможность попробовать наш VPN прямо сейчас.\n\n"
"💡 Нажми на кнопку ниже, чтобы активировать пробный доступ."
)
await bot.send_message(tg_id, message, reply_markup=keyboard)
logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.")
await add_notification(tg_id, "inactive_trial", session=conn)
except Exception as e:
logger.error(
f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}"
)
await asyncio.sleep(1)
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float):
logger.info("Проверка истекших ключей...")
adjusted_current_time = current_time + (3 * 60 * 60 * 1000)
expiring_keys = await conn.fetch(
"""
SELECT tg_id, client_id, expiry_time, email FROM keys
WHERE expiry_time <= $1
""",
adjusted_current_time,
current_time,
)
logger.info(f"current_time {current_time}")
logger.info(f"Найдено {len(expiring_keys)} истекающих ключей.")
async def process_key(record):
tg_id = record["tg_id"]
client_id = record["client_id"]
email = record["email"]
balance = await get_balance(tg_id)
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
await asyncio.gather(*[process_key(record, bot, conn) for record in expiring_keys])
logger.info(
f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}"
)
message_expired = (
f"❌ Ваша подписка {email} истекла и была удалена!\n\n"
"🔍 Перейдите в профиль для создания новой подписки.\n"
"💡 Не откладывайте подключение VPN!"
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
]
]
)
async def process_key(record, bot, conn):
tg_id = record["tg_id"]
client_id = record["client_id"]
email = record["email"]
balance = await get_balance(tg_id)
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
try:
if balance >= RENEWAL_PLANS["1"]["price"]:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
logger.info(
f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}"
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
await update_key_expiry(client_id, new_expiry_time)
]
]
)
for cluster_id in CLUSTERS:
await renew_key_in_cluster(
cluster_id, email, client_id, new_expiry_time, TOTAL_GB
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
)
try:
if balance >= RENEWAL_PLANS["1"]["price"]:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
)
await update_key_expiry(client_id, new_expiry_time)
await conn.execute(
"""
UPDATE keys
SET notified = FALSE, notified_24h = FALSE
WHERE client_id = $1
""",
client_id,
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id, email, client_id, new_expiry_time, TOTAL_GB
)
logger.info(
f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}."
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
)
try:
await bot.send_message(
tg_id, text=KEY_RENEWED, reply_markup=keyboard
)
logger.info(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except Exception as e:
logger.error(
f"Ошибка при отправке уведомления клиенту {tg_id}: {e}"
)
else:
await safe_send_message(
bot, tg_id, message_expired, reply_markup=keyboard
await conn.execute(
"""
UPDATE keys
SET notified = FALSE, notified_24h = FALSE
WHERE client_id = $1
""",
client_id,
)
logger.info(
f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}."
)
try:
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
logger.info(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
await delete_key(client_id)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления клиенту {tg_id}: {e}")
for cluster_id, cluster in CLUSTERS.items():
for server_id, server in cluster.items():
xui = AsyncApi(
server["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
await delete_client(xui, email, client_id)
except Exception as e:
logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
await asyncio.gather(*[process_key(record) for record in expiring_keys])
async def safe_send_message(bot, tg_id, text, reply_markup=None):
try:
await bot.send_message(tg_id, text, reply_markup=reply_markup)
except Exception as e:
if "chat not found" in str(e):
logger.warning(f"Чат для клиента {tg_id} не найден.")
else:
logger.error(f"Ошибка при отправке сообщения клиенту {tg_id}: {e}")
message_expired = "Ваша подписка истекла и была удалена. Получите новую через личный кабинет"
try:
await bot.send_message(
tg_id, text=message_expired, reply_markup=keyboard
)
logger.info(
f"Уведомление об истечении подписки и удалении ключа отправлено пользователю {tg_id}."
)
except Exception as e:
logger.error(
f"Ошибка при отправке уведомления об истечении подписки пользователю {tg_id}: {e}"
)
servers = await get_servers_from_db()
for cluster_id in servers:
await delete_key_from_cluster(cluster_id, email, client_id)
logger.info(f"Клиент {client_id} удален из кластера {cluster_id}.")
await delete_key(client_id)
logger.info(f"Ключ для клиента с ID {client_id} удален из базы данных.")
except Exception as e:
logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
async def check_online_users():
servers = await get_servers_from_db()
for cluster_id, cluster in servers.items():
for server_id, server in enumerate(cluster):
xui = AsyncApi(
server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD
)
await xui.login()
try:
online_users = len(await xui.client.online())
logger.info(
f"Сервер '{server['server_name']}' доступен, текущее количество активных пользователей: {online_users}."
)
except Exception as e:
logger.error(
f"Не удалось проверить пользователей на сервере {server_id}: {e}"
)
+21 -23
View File
@@ -2,16 +2,21 @@ from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE
from database import get_trial
from handlers.start import send_welcome_message
from config import (
CRYPTO_BOT_ENABLE,
DONATIONS_ENABLE,
FREEKASSA_ENABLE,
ROBOKASSA_ENABLE,
STARS_ENABLE,
YOOKASSA_ENABLE,
YOOMONEY_ENABLE,
)
router = Router()
@router.callback_query(F.data == "pay")
async def handle_pay(callback_query: CallbackQuery):
await callback_query.message.delete()
builder = InlineKeyboardBuilder()
if YOOKASSA_ENABLE:
@@ -21,6 +26,13 @@ async def handle_pay(callback_query: CallbackQuery):
callback_data="pay_yookassa",
)
)
if YOOMONEY_ENABLE:
builder.row(
InlineKeyboardButton(
text="💳 ЮМани: оплата по карте",
callback_data="pay_yoomoney",
)
)
if FREEKASSA_ENABLE:
builder.row(
InlineKeyboardButton(
@@ -49,31 +61,17 @@ async def handle_pay(callback_query: CallbackQuery):
callback_data="pay_robokassa",
)
)
builder.row(
InlineKeyboardButton(
text="🎟️ Активировать купон", callback_data="activate_coupon"
)
)
builder.row(
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")
)
builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon"))
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
"💸 <b>Выберите удобный способ пополнения баланса:</b>\n\n"
"• Быстро и безопасно\n"
"• Поддержка разных платежных систем\n"
"• Моментальное зачисление средств 🚀",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
await send_welcome_message(callback_query.from_user.id, trial_status)
await callback_query.answer()
+34
View File
@@ -0,0 +1,34 @@
__all__ = ("router",)
from aiogram import Router
from config import (
CRYPTO_BOT_ENABLE,
FREEKASSA_ENABLE,
ROBOKASSA_ENABLE,
STARS_ENABLE,
YOOKASSA_ENABLE,
YOOMONEY_ENABLE,
)
from .cryprobot_pay import router as cryprobot_router
from .gift import router as gift_router
from .robokassa_pay import router as robokassa_router
from .stars_pay import router as stars_router
from .yookassa_pay import router as yookassa_router
from .yoomoney_pay import router as yoomoney_router
router = Router(name="payments_main_router")
if YOOKASSA_ENABLE:
router.include_router(yookassa_router)
if YOOMONEY_ENABLE:
router.include_router(yoomoney_router)
if ROBOKASSA_ENABLE:
router.include_router(robokassa_router)
if CRYPTO_BOT_ENABLE:
router.include_router(cryprobot_router)
if STARS_ENABLE:
router.include_router(stars_router)
router.include_router(gift_router)
File diff suppressed because it is too large Load Diff
Binary file not shown.
-272
View File
@@ -1,272 +0,0 @@
from aiocryptopay import AioCryptoPay, Networks
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from bot import bot
from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
router = Router()
if CRYPTO_BOT_ENABLE:
crypto = AioCryptoPay(token=CRYPTO_BOT_TOKEN, network=Networks.MAIN_NET)
class ReplenishBalanceState(StatesGroup):
choosing_amount_crypto = State()
waiting_for_payment_confirmation_crypto = State()
entering_custom_amount_crypto = State()
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_cryptobot")
async def process_callback_pay_cryptobot(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'crypto_{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'crypto_{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'crypto_{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_crypto"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_crypto)
await callback_query.answer()
@router.callback_query(F.data.startswith("crypto_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
if len(data) != 2:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Неверные данные для выбора суммы.")
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Некорректная сумма.")
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_crypto)
try:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
invoice = await crypto.create_invoice(
asset="USDT",
amount=str(int(amount // RUB_TO_USDT)),
description=f"Пополнения баланса на {amount} руб",
payload=f"{callback_query.from_user.id}:{int(amount)}",
)
if hasattr(invoice, "bot_invoice_url"):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
await bot.send_message(
chat_id=callback_query.from_user.id,
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=builder.as_markup(),
)
else:
await callback_query.message.answer("Ошибка при создании платежа.")
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await callback_query.message.answer("Произошла ошибка при создании платежа.")
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def cryptobot_webhook(request):
try:
data = await request.json()
logger.info(f"Получены данные вебхука: {data}")
if data.get("update_type") == "invoice_paid":
await process_crypto_payment(data["payload"])
return web.Response(status=200)
else:
logger.warning(
f"Неподдерживаемый тип обновления: {data.get('update_type')}"
)
return web.Response(status=400)
except Exception as e:
logger.error(f"Ошибка обработки вебхука: {e}")
return web.Response(status=500)
async def process_crypto_payment(payload):
if payload["status"] == "paid":
custom_payload = payload["payload"]
user_id_str, amount_str = custom_payload.split(":")
try:
user_id = int(user_id_str)
amount = int(amount_str)
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
else:
logger.warning(f"Получен неоплаченный инвойс: {payload}")
@router.callback_query(F.data == "enter_custom_amount_crypto")
async def process_enter_custom_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount_crypto)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_crypto)
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_USDT <= 0:
await message.answer(
f"Сумма должна быть больше {RUB_TO_USDT}. Пожалуйста, введите сумму еще раз:"
)
return
await state.update_data(amount=amount)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_crypto
)
try:
invoice = await crypto.create_invoice(
asset="USDT",
amount=str(int(amount // RUB_TO_USDT)),
description=f"Пополнения баланса на {amount} руб",
payload=f"{message.from_user.id}:{amount}",
)
if hasattr(invoice, "bot_invoice_url"):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)
)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"),
)
await message.message.edit_text(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=builder.as_markup(),
)
else:
await send_message_with_deletion(
message.from_user.id, "Ошибка при создании платежа.", state=state
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
-224
View File
@@ -1,224 +0,0 @@
import hashlib
import hmac
import logging
import time
import uuid
import requests
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from bot import bot
from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID
from database import update_balance
from handlers.texts import PAYMENT_OPTIONS
router = Router()
logging.basicConfig(level=logging.DEBUG)
class ReplenishBalanceState(StatesGroup):
choosing_amount_freekassa = State()
waiting_for_payment_confirmation_freekassa = State()
entering_custom_amount_freekassa = State()
def generate_signature(params, api_key):
sorted_params = {k: params[k] for k in sorted(params)}
sign_string = "|".join(str(value) for value in sorted_params.values())
return hmac.new(api_key.encode(), sign_string.encode(), hashlib.sha256).hexdigest()
async def create_payment(user_id, amount, email, ip):
payment_id = str(uuid.uuid4())
nonce = int(time.time() * 1000)
params = {
"shopId": FREEKASSA_SHOP_ID,
"amount": amount,
"currency": "RUB",
"paymentId": payment_id,
"email": email,
"ip": ip,
"i": 6,
"nonce": nonce,
}
params["signature"] = generate_signature(params, FREEKASSA_API_KEY)
try:
response = requests.post(
"https://api.freekassa.com/v1/orders/create", json=params
)
response_data = response.json()
logging.debug(f"Ответ от FreeKassa при создании платежа: {response_data}")
if response_data.get("type") == "success":
return response_data["location"]
else:
logging.error(f"Ошибка создания платежа: {response_data}")
return None
except Exception as e:
logging.error(f"Ошибка запроса к FreeKassa: {e}")
return None
async def send_payment_success_notification(user_id, amount):
try:
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
)
except Exception as e:
logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def freekassa_webhook(request):
data = await request.json()
logging.debug(f"Получен вебхук от FreeKassa: {data}")
logging.debug(f"Данные вебхука от FreeKassa: {data}")
if data["status"] == "completed":
user_id = data["metadata"]["user_id"]
amount = float(data["amount"])
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
return web.Response(status=200)
@router.callback_query(lambda c: c.data == "pay_freekassa")
async def process_callback_pay_freekassa(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'freekassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'freekassa_{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'freekassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_freekassa"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения через FreeKassa:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_freekassa)
await callback_query.answer()
@router.callback_query(F.data.startswith("freekassa_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await bot.send_message(callback_query.from_user.id, "Некорректная сумма.")
return
user_email = f"{callback_query.from_user.id}@solo.net"
user_ip = callback_query.message.chat.id
payment_url = await create_payment(
callback_query.from_user.id, amount, user_email, user_ip
)
if payment_url:
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=f"Оплатить {amount} рублей", url=payment_url
)
],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")],
]
)
await bot.send_message(
callback_query.from_user.id,
f"Вы выбрали оплату на {amount} рублей. Перейдите по ссылке для завершения оплаты:",
reply_markup=confirm_keyboard,
)
else:
await bot.send_message(
callback_query.from_user.id,
"Ошибка при создании платежа. Попробуйте позже.",
)
await callback_query.answer()
@router.callback_query(F.data == "enter_custom_amount_freekassa")
async def process_enter_custom_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount_freekassa)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_freekassa)
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount <= 0:
await message.answer(
"Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:"
)
return
user_email = f"{message.from_user.id}@solo.net"
user_ip = message.chat.id
payment_url = await create_payment(
message.from_user.id, amount, user_email, user_ip
)
if payment_url:
keyboard = InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]]
)
await message.answer(
f"Вы выбрали оплату на {amount} рублей. Перейдите по ссылке для завершения оплаты:",
reply_markup=keyboard,
)
else:
await message.answer("Ошибка при создании платежа. Попробуйте позже.")
else:
await message.answer("Пожалуйста, введите корректную сумму.")
File diff suppressed because it is too large Load Diff
Binary file not shown.
+84 -118
View File
@@ -1,4 +1,5 @@
import hashlib
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
@@ -6,13 +7,25 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from loguru import logger
from robokassa import HashAlgorithm, Robokassa
from bot import bot
from config import ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE
from database import add_connection, check_connection_exists, get_key_count, update_balance
from config import (
ROBOKASSA_ENABLE,
ROBOKASSA_LOGIN,
ROBOKASSA_PASSWORD1,
ROBOKASSA_PASSWORD2,
ROBOKASSA_TEST_MODE,
)
from database import (
add_connection,
add_payment,
check_connection_exists,
get_key_count,
update_balance,
)
from handlers.payments.utils import send_payment_success_notification
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
router = Router()
@@ -22,18 +35,19 @@ class ReplenishBalanceState(StatesGroup):
waiting_for_payment_confirmation_robokassa = State()
robokassa = Robokassa(
merchant_login=ROBOKASSA_LOGIN,
password1=ROBOKASSA_PASSWORD1,
password2=ROBOKASSA_PASSWORD2,
algorithm=HashAlgorithm.md5,
is_test=ROBOKASSA_TEST_MODE,
)
if ROBOKASSA_ENABLE:
robokassa = Robokassa(
merchant_login=ROBOKASSA_LOGIN,
password1=ROBOKASSA_PASSWORD1,
password2=ROBOKASSA_PASSWORD2,
algorithm=HashAlgorithm.md5,
is_test=ROBOKASSA_TEST_MODE,
)
logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN)
logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN)
def generate_payment_link(amount, inv_id, description):
def generate_payment_link(amount, inv_id, description, tg_id):
"""Генерация ссылки на оплату."""
logger.debug(
f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}"
@@ -41,46 +55,18 @@ def generate_payment_link(amount, inv_id, description):
payment_link = robokassa._payment.link.generate_by_script(
out_sum=amount,
inv_id=inv_id,
description="description",
description="пополнение баланса",
id=f"{tg_id}",
)
logger.info(f"Generated payment link: {payment_link}")
return payment_link
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
logger.debug(
f"Deleting previous message with ID: {previous_message_id}"
)
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
logger.debug(f"Sent new message with ID: {sent_message.message_id}")
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_robokassa")
async def process_callback_pay_robokassa(
callback_query: types.CallbackQuery, state: FSMContext
callback_query: types.CallbackQuery, state: FSMContext, session: Any
):
tg_id = callback_query.from_user.id
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated Robokassa payment.")
builder = InlineKeyboardBuilder()
@@ -105,35 +91,26 @@ async def process_callback_pay_robokassa(
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_robokassa"
text="💰 Ввести свою сумму",
callback_data="enter_custom_amount_robokassa",
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
await add_connection(tg_id, balance=0.0, trial=0, session=session)
logger.info(f"Created new connection for user {tg_id} with balance 0.0.")
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
logger.debug(f"Deleted message with ID: {callback_query.message.message_id}")
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
await callback_query.message.answer(
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_robokassa)
logger.info(f"Displayed amount selection for user {tg_id}.")
await callback_query.answer()
@router.callback_query(F.data.startswith("robokassa_amount|"))
@@ -145,12 +122,7 @@ async def process_amount_selection(
data = callback_query.data.split("|")
if len(data) != 3 or data[1] != "amount":
logger.error("Ошибка: callback_data не соответствует формату.")
await send_message_with_deletion(
chat_id=callback_query.from_user.id,
text="Неверные данные для выбора суммы.",
state=state,
)
await callback_query.answer("Ошибка: данные повреждены.")
await callback_query.message.answer("Ошибка: данные повреждены.")
return
amount_str = data[2]
@@ -160,25 +132,17 @@ async def process_amount_selection(
raise ValueError("Сумма должна быть положительным числом.")
except ValueError as e:
logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}")
await send_message_with_deletion(
chat_id=callback_query.from_user.id,
text="Некорректная сумма. Попробуйте снова.",
state=state,
)
await callback_query.answer("Некорректная сумма.")
await callback_query.message.answer("Некорректная сумма.")
return
await state.update_data(amount=amount)
logger.info(f"User {callback_query.from_user.id} selected amount: {amount}.")
logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.")
inv_id = 0
tg_id = callback_query.from_user.id
payment_url = generate_payment_link(
amount,
tg_id,
"Пополнение баланса",
)
tg_id = callback_query.message.chat.id
payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id)
logger.info(f"Payment URL for user {callback_query.from_user.id}: {payment_url}")
logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}")
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
@@ -187,95 +151,101 @@ async def process_amount_selection(
]
)
await callback_query.message.edit_text(
await callback_query.message.answer(
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
reply_markup=confirm_keyboard,
)
logger.info(f"Payment link sent to user {callback_query.from_user.id}.")
await callback_query.answer()
logger.info(f"Payment link sent to user {callback_query.message.chat.id}.")
async def robokassa_webhook(request):
"""Обработка webhook-уведомлений от Robokassa."""
"""Обработка webhook-уведомлений от Robokassa с учетом shp_id."""
try:
params = await request.post()
logger.debug(f"Received webhook params: {params}")
logger.info(f"Received webhook params: {params}")
amount = params.get("OutSum")
inv_id = params.get("InvId")
shp_id = params.get("shp_id")
signature_value = params.get("SignatureValue")
logger.info(
f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}"
)
if not check_payment_signature(params):
logger.error("Неверная подпись или данные запроса.")
return web.Response(status=400)
amount = params.get("OutSum")
inv_id = params.get("InvId")
if not amount or not inv_id:
if not amount or not inv_id or not shp_id:
logger.error("Отсутствуют обязательные параметры.")
return web.Response(status=400)
tg_id = inv_id
tg_id = shp_id
logger.info(f"Processing payment for user {tg_id} with amount {amount}.")
await update_balance(int(tg_id), float(amount))
await send_payment_success_notification(tg_id, float(amount))
await add_payment(int(tg_id), float(amount), "robokassa")
logger.info(f"Payment successful. Balance updated for user {tg_id}.")
return web.Response(text=f"OK{inv_id}")
except Exception as e:
logger.error(f"Error processing webhook: {e}")
return web.Response(status=500)
def check_payment_signature(params):
"""Проверка подписи запроса от Robokassa."""
"""Проверка подписи запроса от Robokassa с учетом shp_id."""
out_sum = params.get("OutSum")
inv_id = params.get("InvId")
signature_value = params.get("SignatureValue")
shp_id = params.get("shp_id")
signature_string = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}"
signature_string = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}:shp_id={shp_id}"
logger.info(f"Signature string before hashing: {signature_string}")
expected_signature = (
hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
)
logger.debug(f"Expected signature: {expected_signature}")
logger.debug(f"Received signature: {signature_value}")
logger.info(f"Expected signature: {expected_signature}")
logger.info(f"Received signature: {signature_value}")
return signature_value == expected_signature
async def send_payment_success_notification(user_id: int, amount: float):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
logger.info(f"Sent payment success notification to user {user_id}.")
return signature_value.upper() == expected_signature.upper()
@router.callback_query(F.data == "enter_custom_amount_robokassa")
async def process_custom_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} chose to enter a custom amount.")
await callback_query.message.edit_text(
text="Пожалуйста, введите сумму пополнения в рублях (например, 150):"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_robokassa"))
await callback_query.message.answer(
"Пожалуйста, введите сумму пополнения.",
reply_markup=builder.as_markup(),
)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_robokassa
)
await callback_query.answer()
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input(message: types.Message, state: FSMContext):
tg_id = message.from_user.id
tg_id = message.chat.id
logger.info(f"User {tg_id} entered custom amount: {message.text}")
inv_id = 0
try:
amount = int(message.text)
@@ -284,11 +254,7 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
await state.update_data(amount=amount)
payment_url = generate_payment_link(
amount,
tg_id,
"Пополнение баланса",
)
payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id)
logger.info(f"Generated payment link for user {tg_id}: {payment_url}")
File diff suppressed because it is too large Load Diff
Binary file not shown.
-243
View File
@@ -1,243 +0,0 @@
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import RUB_TO_XTR
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
router = Router()
class ReplenishBalanceState(StatesGroup):
choosing_amount_stars = State()
waiting_for_payment_confirmation_stars = State()
entering_custom_amount_stars = State()
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_stars")
async def process_callback_pay_stars(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'stars_{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'stars_{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'stars_{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_stars"
)
)
builder.row(
InlineKeyboardButton(
text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_stars)
await callback_query.answer()
@router.callback_query(F.data.startswith("stars_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
if len(data) != 2:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Неверные данные для выбора суммы.")
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Некорректная сумма.")
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_stars)
try:
await callback_query.message.delete()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", pay=True),
)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"),
)
await callback_query.message.answer_invoice(
title=f"Вы выбрали пополнение на {amount} рублей.",
description=f"Вы выбрали пополнение на {amount} рублей.",
prices=[LabeledPrice(label="XTR", amount=int(amount // RUB_TO_XTR))],
provider_token="",
payload=f"{amount}_stars",
currency="XTR",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await callback_query.message.answer("Произошла ошибка при создании платежа.")
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
@router.callback_query(F.data == "enter_custom_amount_stars")
async def process_enter_custom_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount_stars)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_stars)
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_XTR <= 0:
await message.answer(
f"Сумма должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:"
)
return
await state.update_data(amount=amount)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_stars
)
try:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", pay=True),
)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"),
)
await message.answer_invoice(
title=f"Вы выбрали пополнение на {amount} рублей.",
description=f"Вы выбрали пополнение на {amount} рублей.",
prices=[LabeledPrice(label="XTR", amount=int(amount // RUB_TO_XTR))],
provider_token="",
payload=f"{amount}_stars",
currency="XTR",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
@router.pre_checkout_query()
async def on_pre_checkout_query(pre_checkout_query: PreCheckoutQuery):
await pre_checkout_query.answer(ok=True)
@router.message(F.successful_payment)
async def on_successful_payment(
message: types.Message,
):
try:
user_id = int(message.from_user.id)
amount = float(message.successful_payment.invoice_payload.split("_")[0])
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
-310
View File
@@ -1,310 +0,0 @@
import uuid
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from yookassa import Configuration, Payment
from bot import bot
from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
router = Router()
if YOOKASSA_ENABLE:
Configuration.account_id = YOOKASSA_SHOP_ID
Configuration.secret_key = YOOKASSA_SECRET_KEY
logger.debug(f"Account ID: {YOOKASSA_SHOP_ID}")
logger.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}")
class ReplenishBalanceState(StatesGroup):
choosing_amount_yookassa = State()
waiting_for_payment_confirmation_yookassa = State()
entering_custom_amount_yookassa = State()
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_yookassa")
async def process_callback_pay_yookassa(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'yookassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'yookassa_{PAYMENT_OPTIONS[i + 1]["callback_data"]},',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'yookassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_yookassa"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_yookassa)
await callback_query.answer()
@router.callback_query(F.data.startswith("yookassa_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
if len(data) != 2:
await send_message_with_deletion(
callback_query.from_user.id,
"Неверные данные для выбора суммы.",
state=state,
message_key="amount_error_message_id",
)
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await send_message_with_deletion(
callback_query.from_user.id,
"Некорректная сумма.",
state=state,
message_key="amount_error_message_id",
)
return
await state.update_data(amount=amount)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_yookassa
)
# state_data = await state.get_data()
customer_name = callback_query.from_user.full_name
customer_id = callback_query.from_user.id
customer_email = f"{customer_id}@solo.net"
payment = Payment.create(
{
"amount": {"value": str(amount), "currency": "RUB"},
"confirmation": {"type": "redirect", "return_url": "https://pocomacho.ru/"},
"capture": True,
"description": "Пополнение баланса",
"receipt": {
"customer": {
"full_name": customer_name,
"email": customer_email,
"phone": "79000000000",
},
"items": [
{
"description": "Пополнение баланса",
"quantity": "1.00",
"amount": {"value": str(amount), "currency": "RUB"},
"vat_code": 6,
}
],
},
"metadata": {"user_id": customer_id},
},
uuid.uuid4(),
)
if payment["status"] == "pending":
payment_url = payment["confirmation"]["confirmation_url"]
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Пополнить", url=payment_url)],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")],
]
)
await callback_query.message.edit_text(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard,
)
else:
await send_message_with_deletion(
callback_query.from_user.id, "Ошибка при создании платежа.", state=state
)
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def yookassa_webhook(request):
event = await request.json()
logger.debug(f"Webhook event received: {event}")
if event["event"] == "payment.succeeded":
user_id_str = event["object"]["metadata"]["user_id"]
amount_str = event["object"]["amount"]["value"]
try:
user_id = int(user_id_str)
amount = float(amount_str)
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
return web.Response(status=400)
return web.Response(status=200)
@router.callback_query(F.data == "enter_custom_amount_yookassa")
async def process_enter_custom_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount_yookassa)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_yookassa)
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount <= 0:
await message.answer(
"Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:"
)
return
await state.update_data(amount=amount)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_yookassa
)
try:
payment = Payment.create(
{
"amount": {"value": str(amount), "currency": "RUB"},
"confirmation": {
"type": "redirect",
"return_url": "https://pocomacho.ru/",
},
"capture": True,
"description": "Пополнение баланса",
"receipt": {
"customer": {
"full_name": message.from_user.full_name,
"email": f"{message.from_user.id}@solo.net",
"phone": "79000000000",
},
"items": [
{
"description": "Пополнение баланса",
"quantity": "1.00",
"amount": {"value": str(amount), "currency": "RUB"},
"vat_code": 6,
}
],
},
"metadata": {"user_id": message.from_user.id},
},
uuid.uuid4(),
)
if payment["status"] == "pending":
payment_url = payment["confirmation"]["confirmation_url"]
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Пополнить", url=payment_url)],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")],
]
)
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard,
)
else:
await message.answer("Ошибка при создании платежа.")
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
File diff suppressed because it is too large Load Diff
Binary file not shown.
+103 -95
View File
@@ -5,139 +5,147 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import CHANNEL_URL
from config import NEWS_MESSAGE, RENEWAL_PLANS
from database import get_balance, get_key_count, get_referral_stats
from handlers.buttons.profile import ADD_SUB, GIFTS, INSTRUCTIONS, INVITE, MAIN_MENU, MY_SUBS, PAYMENT, TARRIFS
from handlers.texts import get_referral_link, invite_message_send, profile_message_send
from logger import logger
router = Router()
@router.callback_query(F.data == "profile")
@router.message(F.text == "/profile")
async def process_callback_view_profile(
callback_query: types.CallbackQuery, state: FSMContext
callback_query_or_message: types.Message | types.CallbackQuery, state: FSMContext, admin: bool
):
chat_id = callback_query.from_user.id
username = callback_query.from_user.full_name
if isinstance(callback_query_or_message, types.CallbackQuery):
chat_id = callback_query_or_message.message.chat.id
username = callback_query_or_message.from_user.full_name
is_callback = True
elif isinstance(callback_query_or_message, types.Message):
chat_id = callback_query_or_message.chat.id
username = callback_query_or_message.from_user.full_name
is_callback = False
image_path = os.path.join(os.path.dirname(__file__), "pic.jpg")
image_path = os.path.join("img", "pic.jpg")
key_count = await get_key_count(chat_id)
balance = await get_balance(chat_id)
if balance is None:
balance = 0
try:
key_count = await get_key_count(chat_id)
balance = await get_balance(chat_id)
if balance is None:
balance = 0
profile_message = profile_message_send(username, chat_id, int(balance), key_count)
profile_message = profile_message_send(username, chat_id, balance, key_count)
if key_count == 0:
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i></pre>"
else:
profile_message += f"\n<pre> <i>{NEWS_MESSAGE}</i></pre>"
if key_count == 0:
profile_message += "\n🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i>"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL))
builder.row(
InlineKeyboardButton(text=" Устройство", callback_data="create_key"),
InlineKeyboardButton(text="📱 Мои устройства", callback_data="view_keys"),
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text=ADD_SUB, callback_data="create_key"),
InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys"),
)
builder.row(
InlineKeyboardButton(
text=PAYMENT,
callback_data="pay",
)
)
builder.row()
builder.row(
InlineKeyboardButton(text=INVITE, callback_data="invite"),
InlineKeyboardButton(text=GIFTS, callback_data="gifts"),
)
builder.row(
InlineKeyboardButton(text=TARRIFS, callback_data="view_tariffs"),
InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"),
)
if admin:
builder.row(
InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
)
builder.row(
InlineKeyboardButton(text="👥 Пригласить друзей", callback_data="invite"),
InlineKeyboardButton(text="📘 Инструкции", callback_data="instructions"),
)
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
)
builder.row(
InlineKeyboardButton(text="⬅️ Главное меню", callback_data="back_to_menu")
InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")
)
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start"))
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"❗ Ошибка при удалении сообщения: {e}")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=chat_id,
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
if is_callback:
await callback_query_or_message.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
caption=profile_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
else:
await callback_query_or_message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
caption=profile_message,
reply_markup=builder.as_markup(),
)
else:
if is_callback:
await callback_query_or_message.message.answer(
text=profile_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query_or_message.answer(
text=profile_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
except Exception as e:
await bot.send_message(
chat_id, f"❗️ Не удалось загрузить профиль. Техническая ошибка: {e}"
)
await callback_query.answer()
@router.callback_query(F.data == "view_tariffs")
async def view_tariffs_handler(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
image_path = os.path.join("img", "tariffs.jpg")
tariffs_message = "<b>🚀 Доступные тарифы VPN:</b>\n\n" + "\n".join(
[
f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: "
f"{RENEWAL_PLANS[months]['price']} "
f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей"
for months in sorted(RENEWAL_PLANS.keys(), key=int)
]
)
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="tariffs.jpg"),
caption=tariffs_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query.message.answer(
text=tariffs_message,
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "invite")
async def invite_handler(callback_query: types.CallbackQuery):
chat_id = callback_query.from_user.id
chat_id = callback_query.message.chat.id
referral_link = get_referral_link(chat_id)
referral_stats = await get_referral_stats(chat_id)
invite_message = invite_message_send(referral_link, referral_stats)
image_path = os.path.join(os.path.dirname(__file__), "pic_invite.jpg")
image_path = os.path.join("img", "pic_invite.jpg")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")
)
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
try:
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=chat_id,
photo=BufferedInputFile(
image_file.read(), filename="pic_invite.jpg"
),
caption=invite_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
text=invite_message,
parse_mode="HTML",
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic_invite.jpg"),
caption=invite_message,
reply_markup=builder.as_markup(),
)
except Exception as e:
await bot.send_message(
chat_id=chat_id,
text=f"❗️ Не удалось отправить сообщение. Техническая ошибка: {e}",
parse_mode="HTML",
else:
await callback_query.message.answer(
text=invite_message,
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "view_profile")
async def view_profile_handler(callback_query: types.CallbackQuery, state: FSMContext):
await state.clear()
await process_callback_view_profile(callback_query, state)
+183 -88
View File
@@ -1,13 +1,29 @@
import os
from typing import Any
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message
from aiogram.fsm.context import FSMContext
from aiogram.types import (
BufferedInputFile,
CallbackQuery,
InlineKeyboardButton,
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL
from database import add_connection, add_referral, check_connection_exists, get_trial
from config import (
CHANNEL_EXISTS,
CHANNEL_URL,
CONNECT_ANDROID,
CONNECT_IOS,
DONATIONS_ENABLE,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
SUPPORT_CHAT_URL,
)
from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial
from handlers.keys.key_management import create_key
from handlers.keys.trial_key import create_trial_key
from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn
from logger import logger
@@ -15,104 +31,179 @@ from logger import logger
router = Router()
async def send_welcome_message(chat_id: int, trial_status: int):
image_path = os.path.join(os.path.dirname(__file__), "pic.jpg")
@router.callback_query(F.data == "start")
async def handle_start_callback_query(
callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool
):
await start_command(callback_query.message, state, session, admin)
@router.message(Command("start"))
async def start_command(message: Message, state: FSMContext, session: Any, admin: bool):
"""Обрабатывает команду /start, включает логику рефералов и подарков."""
logger.info(f"Вызвана функция start_command для пользователя {message.chat.id}")
await state.clear()
if message.text:
try:
connection_exists = await check_connection_exists(message.chat.id)
logger.info(f"Проверка существования подключения: {connection_exists}")
if not connection_exists:
await add_connection(tg_id=message.chat.id, session=session)
logger.info(f"Пользователь {message.chat.id} успешно добавлен в базу данных.")
if "gift_" in message.text:
logger.info(f"Обнаружена ссылка на подарок: {message.text}")
parts = message.text.split("gift_")[1].split("_")
gift_id = parts[0]
recipient_tg_id = message.chat.id
gift_info = await session.fetchrow(
"SELECT * FROM gifts WHERE gift_id = $1 AND is_used = FALSE", gift_id
)
if gift_info is None:
logger.warning(f"Подарок с ID {gift_id} уже был использован или не существует.")
await message.answer("Этот подарок уже был использован или не существует.")
return await show_start_menu(message, admin, session)
if gift_info['sender_tg_id'] == recipient_tg_id:
logger.warning(
f"Пользователь {recipient_tg_id} попытался активировать подарок, который был отправлен им самим."
)
await message.answer("❌ Вы не можете получить подарок от самого себя.")
return await show_start_menu(message, admin, session)
selected_months = gift_info['selected_months']
expiry_time = gift_info['expiry_time']
expiry_time_naive = expiry_time.replace(tzinfo=None)
logger.info(f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}.")
await create_key(recipient_tg_id, expiry_time_naive, state, session, message)
logger.info(f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев.")
await session.execute(
"UPDATE gifts SET is_used = TRUE, recipient_tg_id = $1 WHERE gift_id = $2",
recipient_tg_id,
gift_id,
)
await message.answer(
f"🎉 Ваш подарок на {selected_months} {'месяц' if selected_months == 1 else 'месяца' if selected_months in [2, 3, 4] else 'месяцев'} активирован!"
)
logger.info(f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}.")
return
elif "referral_" in message.text:
try:
referrer_tg_id = int(message.text.split("referral_")[1])
if connection_exists:
logger.info(
f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом."
)
await message.answer("❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку.")
return await show_start_menu(message, admin, session)
if referrer_tg_id == message.chat.id:
logger.warning(f"Пользователь {message.chat.id} попытался стать рефералом самого себя.")
await message.answer("❌ Вы не можете быть рефералом самого себя.")
return await show_start_menu(message, admin, session)
existing_referral = await session.fetchrow(
"SELECT * FROM referrals WHERE referred_tg_id = $1", message.chat.id
)
if existing_referral:
logger.info(f"Реферал с ID {message.chat.id} уже существует.")
return await show_start_menu(message, admin, session)
await add_referral(message.chat.id, referrer_tg_id, session)
logger.info(f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}")
return await show_start_menu(message, admin, session)
except (ValueError, IndexError) as e:
logger.error(f"Ошибка при обработке реферальной ссылки: {e}")
return
else:
logger.info(f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка.")
await show_start_menu(message, admin, session)
except (ValueError, IndexError) as e:
logger.error(f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}")
await message.answer("❌ Произошла ошибка. Пожалуйста, попробуйте снова.")
else:
await show_start_menu(message, admin, session)
async def show_start_menu(message: Message, admin: bool, session: Any):
"""Функция для отображения стандартного меню"""
logger.info(f"Показываю главное меню для пользователя {message.chat.id}")
trial_status = await get_trial(message.chat.id, session)
image_path = os.path.join("img", "pic.jpg")
builder = InlineKeyboardBuilder()
if trial_status == 0:
builder.row(
InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")
)
builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
)
builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
)
builder.row(
InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL),
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL),
)
if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn"))
if os.path.isfile(image_path):
with open(image_path, "rb") as image_from_buffer:
await bot.send_photo(
chat_id=chat_id,
await message.answer_photo(
photo=BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"),
caption=WELCOME_TEXT,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
await message.answer(
text=WELCOME_TEXT,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
@router.message(Command("start"))
async def start_command(message: Message):
logger.info(f"Received start command with text: {message.text}")
if "referral_" in message.text:
referrer_tg_id = int(message.text.split("referral_")[1])
logger.info(f"Referral ID: {referrer_tg_id}")
if not await check_connection_exists(message.from_user.id):
await add_connection(message.from_user.id)
await add_referral(message.from_user.id, referrer_tg_id)
await message.answer("Вас пригласил друг, добро пожаловать!")
else:
await message.answer("Вы уже зарегистрированы в системе!")
trial_status = await get_trial(message.from_user.id)
await send_welcome_message(message.chat.id, trial_status)
@router.callback_query(F.data == "connect_vpn")
async def handle_connect_vpn(callback_query: CallbackQuery, session):
await callback_query.message.delete()
user_id = callback_query.from_user.id
async def handle_connect_vpn(callback_query: CallbackQuery, session: Any):
user_id = callback_query.message.chat.id
trial_key_info = await create_trial_key(user_id)
trial_key_info = await create_trial_key(user_id, session)
if "error" in trial_key_info:
await callback_query.message.answer(trial_key_info["error"])
else:
try:
result = await session.execute(
"""
UPDATE connections SET trial = 1 WHERE tg_id = $1
""",
user_id,
)
logger.info(f"Rows updated: {result}")
except Exception as e:
logger.error(f"Ошибка при обновлении trial: {e}")
await use_trial(user_id, session)
key_message = (
f"🔑 <b>Ваш персональный ключ доступа:</b>\n"
f"<pre>{trial_key_info['key']}</pre>\n\n"
f"<code>{trial_key_info['key']}</code>\n\n"
f"📋 <b>Быстрая инструкция по подключению:</b>\n{INSTRUCTIONS_TRIAL}"
)
email = trial_key_info["email"]
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS),
InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(
text="🍏 Скачать для iOS",
url=DOWNLOAD_IOS,
),
InlineKeyboardButton(
text="🤖 Скачать для Android",
url=DOWNLOAD_ANDROID,
),
)
builder.row(
InlineKeyboardButton(
text="🍏 Подключить на iOS",
@@ -123,35 +214,39 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session):
url=f'{CONNECT_ANDROID}{trial_key_info["key"]}',
),
)
await callback_query.message.answer(
key_message, parse_mode="HTML", reply_markup=builder.as_markup()
builder.row(
InlineKeyboardButton(
text="💻 Компьютеры", callback_data=f"connect_pc|{email}"
),
InlineKeyboardButton(
text="📺 Андроид TV", callback_data=f"connect_tv|{email}"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
await callback_query.answer()
await callback_query.message.answer(
key_message, reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "about_vpn")
async def handle_about_vpn(callback_query: CallbackQuery):
await callback_query.message.delete()
about_vpn_message = get_about_vpn("3.1.0_Stable")
builder = InlineKeyboardBuilder()
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu"))
if CHANNEL_EXISTS:
builder.row(
InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL),
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start"))
await callback_query.message.answer(
about_vpn_message, parse_mode="HTML", reply_markup=builder.as_markup()
get_about_vpn("3.2.3-minor"), reply_markup=builder.as_markup()
)
await callback_query.answer()
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
await send_welcome_message(callback_query.from_user.id, trial_status)
await callback_query.answer()
+60 -48
View File
@@ -4,76 +4,88 @@ import re
import asyncpg
from bot import bot
from config import CLUSTERS, DATABASE_URL
from config import DATABASE_URL
from database import get_servers_from_db
from logger import logger
def sanitize_key_name(key_name: str) -> str:
"""
Очищает название ключа, оставляя только допустимые символы.
Args:
key_name (str): Исходное название ключа.
Returns:
str: Очищенное название ключа в нижнем регистре.
"""
return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
def generate_random_email():
"""Генерирует случайный набор символов."""
random_string = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=6))
return random_string
async def get_least_loaded_cluster():
def generate_random_email(length: int = 6) -> str:
"""
Функция для получения кластера с наименьшей загрузкой (по количеству ключей).
Возвращает идентификатор кластера с наименьшей загрузкой или первый кластер из конфигурации,
если загруженность не определяется. В случае отсутствия кластеров с номером, возвращает 'cluster1'.
Генерирует случайный email с заданной длиной.
Args:
length (int, optional): Длина случайной строки. По умолчанию 6.
Returns:
str: Сгенерированная случайная строка.
"""
cluster_loads = {}
return "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=length))
conn = await asyncpg.connect(DATABASE_URL)
try:
keys = await conn.fetch("SELECT * FROM keys")
for key in keys:
cluster_id = key["server_id"]
if re.match(r"^cluster\d+$", cluster_id):
if cluster_id not in cluster_loads:
cluster_loads[cluster_id] = 0
cluster_loads[cluster_id] += 1
finally:
await conn.close()
async def get_least_loaded_cluster() -> str:
"""
Определяет кластер с наименьшей загрузкой.
logger.info(f"Cluster loads: {cluster_loads}")
Returns:
str: Идентификатор наименее загруженного кластера.
"""
servers = await get_servers_from_db()
cluster_loads: dict[str, int] = {cluster_id: 0 for cluster_id in servers.keys()}
async with asyncpg.create_pool(DATABASE_URL) as pool:
async with pool.acquire() as conn:
keys = await conn.fetch("SELECT server_id FROM keys")
for key in keys:
cluster_id = key["server_id"]
if cluster_id in cluster_loads:
cluster_loads[cluster_id] += 1
logger.info(f"Cluster loads after database query: {cluster_loads}")
if not cluster_loads:
available_clusters = [
cluster_id
for cluster_id in CLUSTERS.keys()
if re.match(r"^cluster\d+$", cluster_id)
]
logger.warning("No clusters found in database or configuration.")
return "cluster1"
logger.info(f"Available clusters from config: {available_clusters}")
if available_clusters:
logger.info(
f"Returning the first available cluster: {available_clusters[0]}"
)
return available_clusters[0]
else:
logger.warning("No valid clusters found in config, returning 'cluster1'.")
return "cluster1"
least_loaded_cluster = min(cluster_loads, key=cluster_loads.get)
least_loaded_cluster = min(cluster_loads, key=lambda k: (cluster_loads[k], k))
logger.info(f"Least loaded cluster selected: {least_loaded_cluster}")
return least_loaded_cluster
async def handle_error(tg_id, callback_query, message):
async def handle_error(
tg_id: int, callback_query: object | None = None, message: str = ""
) -> None:
"""
Обрабатывает ошибку, отправляя сообщение пользователю.
Args:
tg_id (int): Идентификатор пользователя в Telegram.
callback_query (Optional[object], optional): Объект запроса обратного вызова. По умолчанию None.
message (str, optional): Текст сообщения об ошибке. По умолчанию пустая строка.
"""
try:
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
if callback_query and hasattr(callback_query, "message"):
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as delete_error:
logger.warning(f"Не удалось удалить сообщение: {delete_error}")
await bot.send_message(tg_id, message, parse_mode="HTML")

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+11 -6
View File
@@ -1,10 +1,16 @@
import logging
import os
import sys
from datetime import timedelta
from loguru import logger
logger.remove()
log_folder = "logs"
if not os.path.exists(log_folder):
os.makedirs(log_folder)
logger.remove()
level_mapping = {
50: "CRITICAL",
@@ -27,7 +33,6 @@ logging.basicConfig(handlers=[InterceptHandler()], level=0)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
# Настройка вывода в консоль
logger.add(
sys.stderr,
level="INFO",
@@ -35,13 +40,13 @@ logger.add(
colorize=True,
)
# Настройка записи в файл
log_file_path = os.path.join(log_folder, "logging.log")
logger.add(
"logging.log",
log_file_path,
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} | {message}",
rotation="60 minute",
retention=24,
rotation=timedelta(minutes=60),
retention=timedelta(days=3),
)
logger = logger
+1 -107
View File
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
from collections.abc import Awaitable, Callable
from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import ADMIN_ID
class AdminMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
data["admin"] = self._check_admin_access(event)
return await handler(event, data)
def _check_admin_access(self, event: TelegramObject) -> bool:
try:
admin_ids: int | list[int] = ADMIN_ID
if isinstance(admin_ids, list):
return event.from_user.id in admin_ids
return event.from_user.id == admin_ids
except Exception:
return False
+7 -6
View File
@@ -1,4 +1,5 @@
from typing import Any, Awaitable, Callable, Dict
from collections.abc import Awaitable, Callable
from typing import Any
import asyncpg
from aiogram import BaseMiddleware
@@ -10,13 +11,13 @@ from config import DATABASE_URL
class DatabaseMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
data: dict[str, Any],
) -> Any:
session = await asyncpg.connect(DATABASE_URL)
data["session"] = session
conn = await asyncpg.connect(DATABASE_URL)
try:
data["session"] = conn
return await handler(event, data)
finally:
await session.close()
await conn.close()
+28
View File
@@ -0,0 +1,28 @@
from collections.abc import Awaitable, Callable
from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
class DeleteMessageMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
if isinstance(event, (Message, CallbackQuery)):
if isinstance(event, Message):
if not event.text or not event.text.startswith("/start"):
try:
await event.bot.delete_message(
event.chat.id, event.message_id - 1
)
except Exception:
pass
await event.delete()
elif isinstance(event, CallbackQuery):
await event.answer()
await event.message.delete()
return await handler(event, data)
+22 -14
View File
@@ -1,4 +1,5 @@
from typing import Any, Awaitable, Callable, Dict
from collections.abc import Awaitable, Callable
from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
@@ -9,27 +10,34 @@ from logger import logger
class LoggingMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
data: dict[str, Any],
) -> Any:
user_info = self._extract_user_info(event)
logger.info(
f"Активность пользователя - "
f"ID пользователя: {user_info['user_id']}, "
f"Имя пользователя: {user_info['username']}, "
f"Действие: {user_info['action']}"
)
return await handler(event, data)
def _extract_user_info(self, event: TelegramObject) -> dict[str, str | None]:
user_id = None
username = None
action = None
if isinstance(event, Message):
user_id = event.from_user.id
username = event.from_user.username
user = event.from_user
user_id = user.id
username = user.username
action = f"Сообщение: {event.text}"
elif isinstance(event, CallbackQuery):
user_id = event.from_user.id
username = event.from_user.username
user = event.from_user
user_id = user.id
username = user.username
action = f"Обратный вызов: {event.data}"
logger.info(
f"Активность пользователя - "
f"ID пользователя: {user_id}, "
f"Имя пользователя: {username}, "
f"Действие: {action}"
)
return await handler(event, data)
return {"user_id": user_id, "username": username, "action": action}
+29
View File
@@ -0,0 +1,29 @@
from collections.abc import Awaitable, Callable
from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, User
from database import upsert_user
class UserMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
if user := data.get("event_from_user"):
await self._process_user(user)
return await handler(event, data)
async def _process_user(self, user: User) -> None:
await upsert_user(
tg_id=user.id,
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
language_code=user.language_code,
is_bot=user.is_bot,
)
-70
View File
@@ -1,70 +0,0 @@
from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class Connection(Base):
"""
Модель для таблицы connections
"""
__tablename__ = "connections"
tg_id = Column(BigInteger, primary_key=True, nullable=False)
balance = Column(Float, nullable=False, default=0.0)
trial = Column(BigInteger, nullable=False, default=0)
# Связь с ключами и рефералами
keys = relationship("Key", back_populates="connection")
referrals_received = relationship(
"Referral", foreign_keys="Referral.referred_tg_id", back_populates="referred"
)
referrals_sent = relationship(
"Referral", foreign_keys="Referral.referrer_tg_id", back_populates="referrer"
)
class Key(Base):
"""
Модель для таблицы keys
"""
__tablename__ = "keys"
tg_id = Column(
BigInteger, ForeignKey("connections.tg_id"), primary_key=True, nullable=False
)
client_id = Column(Text, primary_key=True, nullable=False)
email = Column(Text, nullable=False)
created_at = Column(BigInteger, nullable=False)
expiry_time = Column(BigInteger, nullable=False)
key = Column(Text, nullable=False)
server_id = Column(Text, nullable=False, default="server1")
notified = Column(Boolean, nullable=False, default=False)
notified_24h = Column(Boolean, nullable=False, default=False)
# Связь с подключением
connection = relationship("Connection", back_populates="keys")
class Referral(Base):
"""
Модель для таблицы referrals
"""
__tablename__ = "referrals"
referred_tg_id = Column(
BigInteger, ForeignKey("connections.tg_id"), primary_key=True, nullable=False
)
referrer_tg_id = Column(BigInteger, ForeignKey("connections.tg_id"), nullable=False)
reward_issued = Column(Boolean, default=False)
# Связи с подключениями
referred = relationship(
"Connection", foreign_keys=[referred_tg_id], back_populates="referrals_received"
)
referrer = relationship(
"Connection", foreign_keys=[referrer_tg_id], back_populates="referrals_sent"
)
+17
View File
@@ -0,0 +1,17 @@
[tool.ruff]
line-length = 120
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "ANN", "ASYNC", "S", "BLE", "FBT", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE"]
ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001']
[tool.ruff.format]
quote-style = "single"
indent-style = "space"
[tool.darker]
src = ["."]
revision = "HEAD"
diff = false
check = false
+4 -1
View File
@@ -28,4 +28,7 @@ yookassa==3.3.0
loguru
aiocryptopay
py3xui
sqlalchemy
sqlalchemy
robokassa
ping3
ruff
-164
View File
@@ -1,164 +0,0 @@
#!/bin/bash
# Цвета для вывода
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}3x-ui Installation Script by izzzzzi${NC}"
echo "----------------------------------------"
# Запрос данных у пользователя
read -p "Enter your domain (e.g., example.com): " DOMAIN
read -p "Enter desired admin panel port (default: 2053): " PANEL_PORT
read -p "Enter desired sub panel port (default: 2054): " SUB_PANEL_PORT
# Использование значения по умолчанию для порта, если не указано
PANEL_PORT=${PANEL_PORT:-2053}
SUB_PANEL_PORT=${SUB_PANEL_PORT:-2054}
# Создание необходимых директорий
echo -e "${GREEN}Creating directories...${NC}"
mkdir -p nginx/conf.d db cert
# Установка certbot
echo -e "${GREEN}Installing certbot...${NC}"
apt-get update
apt-get install certbot -y
# Остановка nginx если он запущен
echo -e "${GREEN}Stopping nginx if running...${NC}"
docker-compose down 2>/dev/null
systemctl stop nginx 2>/dev/null
# Получение сертификата
echo -e "${GREEN}Obtaining SSL certificate...${NC}"
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d $DOMAIN
# Проверка успешности получения сертификата
if [ ! -d "/etc/letsencrypt/live/$DOMAIN" ]; then
echo "Failed to obtain SSL certificate. Please check your domain settings."
exit 1
fi
# Тестирование автообновления
echo -e "${GREEN}Testing certificate renewal...${NC}"
certbot renew --dry-run
# Создание docker-compose.yml
echo -e "${GREEN}Creating docker-compose.yml...${NC}"
cat > docker-compose.yml << EOL
version: "3"
services:
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- /etc/letsencrypt:/etc/letsencrypt
- /var/lib/letsencrypt:/var/lib/letsencrypt
networks:
- proxy-network
restart: unless-stopped
depends_on:
- 3x-ui
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: ${DOMAIN}
volumes:
- \$PWD/db/:/etc/x-ui/
- \$PWD/cert/:/root/cert/
- /etc/letsencrypt:/etc/letsencrypt:ro
environment:
XRAY_VMESS_AEAD_FORCED: "false"
tty: true
networks:
- proxy-network
restart: unless-stopped
networks:
proxy-network:
driver: bridge
EOL
# Создание конфигурации Nginx
echo -e "${GREEN}Creating Nginx configuration...${NC}"
cat > nginx/conf.d/default.conf << EOL
server {
listen 80;
server_name ${DOMAIN};
location / {
return 301 https://\$host\$request_uri;
}
}
server {
listen 443 ssl;
server_name ${DOMAIN};
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
location / {
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Range \$http_range;
proxy_set_header If-Range \$http_if_range;
proxy_redirect off;
proxy_pass http://3x-ui:${PANEL_PORT};
}
location /sub {
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Range \$http_range;
proxy_set_header If-Range \$http_if_range;
proxy_redirect off;
proxy_pass http://3x-ui:${SUB_PANEL_PORT};
}
}
EOL
# Проверка наличия Docker и Docker Compose
if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then
echo -e "${GREEN}Installing Docker and Docker Compose...${NC}"
curl -fsSL https://get.docker.com | sh
curl -L "https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
fi
# Настройка автообновления сертификатов
echo -e "${GREEN}Setting up certificate auto-renewal...${NC}"
cat > /etc/cron.d/certbot-renew << EOL
0 */12 * * * root certbot renew --quiet --deploy-hook "docker-compose -f $(pwd)/docker-compose.yml restart nginx"
EOL
# Запуск сервисов
echo -e "${GREEN}Starting all services...${NC}"
docker-compose up -d
echo -e "${GREEN}Installation completed!${NC}"
echo -e "${BLUE}You can access your 3x-ui panel at: https://${DOMAIN}${NC}"
echo -e "${YELLOW}Important: Default access to 3x-ui panel - ${NC}"
echo -e "${YELLOW}Login: admin${NC}"
echo -e "${YELLOW}Password: admin${NC}"
echo -e "${YELLOW}For security, it is recommended to change the password at: https://${DOMAIN}/panel/settings${NC}"
echo "Please wait a few minutes for all services to start properly."
echo "Default credentials can be found in the 3x-ui documentation."
EOL
-108
View File
@@ -1,108 +0,0 @@
#!/bin/bash
# Цвета для вывода
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${BLUE}PostgreSQL Installation and Setup Script by izzzzzi${NC}"
echo "----------------------------------------"
# Запрос данных у пользователя
read -p "Enter PostgreSQL port (default: 5432): " DB_PORT
read -p "Enter database name: " DB_NAME
read -p "Enter database user: " DB_USER
read -s -p "Enter database password: " DB_PASS
echo
read -s -p "Confirm database password: " DB_PASS_CONFIRM
echo
# Проверка паролей
if [ "$DB_PASS" != "$DB_PASS_CONFIRM" ]; then
echo -e "${RED}Passwords do not match!${NC}"
exit 1
fi
# Использование значения по умолчанию для порта
DB_PORT=${DB_PORT:-5432}
# Установка PostgreSQL
echo -e "${GREEN}Installing PostgreSQL...${NC}"
apt-get update
apt-get install -y postgresql postgresql-contrib
# Остановка PostgreSQL для изменения конфигурации
systemctl stop postgresql
# Настройка PostgreSQL для внешних подключений
echo -e "${GREEN}Configuring PostgreSQL...${NC}"
# Настройка postgresql.conf
PG_VERSION=$(ls /etc/postgresql/)
PG_CONF="/etc/postgresql/$PG_VERSION/main/postgresql.conf"
PG_HBA="/etc/postgresql/$PG_VERSION/main/pg_hba.conf"
# Backup конфигурационных файлов
cp $PG_CONF "${PG_CONF}.backup"
cp $PG_HBA "${PG_HBA}.backup"
# Изменение postgresql.conf
sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" $PG_CONF
sed -i "s/#port = 5432/port = $DB_PORT/" $PG_CONF
# Изменение pg_hba.conf
cat > $PG_HBA << EOL
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
local all all peer
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
host all all 0.0.0.0/0 md5
EOL
# Запуск PostgreSQL
systemctl start postgresql
systemctl enable postgresql
# Создание пользователя и базы данных
echo -e "${GREEN}Creating database and user...${NC}"
sudo -u postgres psql << EOF
CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';
CREATE DATABASE $DB_NAME OWNER $DB_USER;
ALTER USER $DB_USER WITH SUPERUSER;
EOF
# Настройка файрвола
echo -e "${GREEN}Configuring firewall...${NC}"
if command -v ufw >/dev/null; then
ufw allow $DB_PORT/tcp
ufw status
fi
# Проверка статуса PostgreSQL
systemctl status postgresql --no-pager
echo -e "${GREEN}Installation completed!${NC}"
echo -e "${BLUE}PostgreSQL is running on port: ${DB_PORT}${NC}"
echo -e "${BLUE}Database name: ${DB_NAME}${NC}"
echo -e "${BLUE}Database user: ${DB_USER}${NC}"
echo "You can now connect to your database using:"
echo "psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME"
echo
echo "For remote connections use:"
echo "psql -h YOUR_SERVER_IP -p $DB_PORT -U $DB_USER -d $DB_NAME"
echo
echo -e "${RED}Important: Make sure to save these credentials in a secure place!${NC}"
# Проверка подключения
echo -e "${GREEN}Testing connection...${NC}"
PGPASSWORD=$DB_PASS psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME -c "\conninfo"
# Добавление информации о создании бэкапов
echo
echo "To create a backup, use:"
echo "pg_dump -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME > backup.sql"
echo
echo "To restore from backup, use:"
echo "psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME < backup.sql"
+200
View File
@@ -0,0 +1,200 @@
import asyncio
import re
from datetime import datetime, timedelta
import asyncpg
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from ping3 import ping
from bot import bot
from config import ADMIN_ID, DATABASE_URL
from database import get_servers_from_db
from logger import logger
try:
from config import CLUSTERS
except ImportError:
CLUSTERS = None
logger.warning(
"Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!"
)
async def sync_servers_with_db():
"""
Синхронизирует сервера из конфигурации CLUSTERS с базой данных.
Если CLUSTERS не найден, синхронизация не будет выполнена.
"""
if CLUSTERS is None:
logger.info(
"Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена."
)
return
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info("Подключение к базе данных для синхронизации серверов успешно.")
for cluster_name, servers in CLUSTERS.items():
for server_key, server_info in servers.items():
exists = await conn.fetchval(
"""
SELECT 1 FROM servers
WHERE cluster_name = $1 AND server_name = $2
""",
cluster_name,
server_info["name"],
)
if not exists:
await conn.execute(
"""
INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id)
VALUES ($1, $2, $3, $4, $5)
""",
cluster_name,
server_info["name"],
server_info["API_URL"],
server_info["SUBSCRIPTION"],
server_info["INBOUND_ID"],
)
logger.info(
f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных."
)
else:
logger.info(
f"Сервер {server_info['name']} из кластера {cluster_name} уже существует."
)
except Exception as e:
logger.error(f"Ошибка при синхронизации серверов: {e}")
finally:
if "conn" in locals():
await conn.close()
last_ping_times = {}
last_notification_times = {}
async def ping_server(server_ip: str) -> bool:
"""
Функция пинга сервера.
Возвращает True, если сервер доступен, иначе False.
"""
try:
logger.debug(f"Пингуем сервер {server_ip}...")
response = ping(server_ip, timeout=3)
if response is False:
logger.warning(f"Сервер {server_ip} не отвечает.")
return False
return True
except Exception as e:
logger.error(f"Ошибка при пинге сервера {server_ip}: {e}")
return False
async def notify_admin(server_name: str):
"""
Отправляет уведомление всем администраторам о недоступности сервера.
Уведомления отправляются не чаще чем раз в 3 минуты.
"""
try:
current_time = datetime.now()
last_notification_time = last_notification_times.get(server_name)
if last_notification_time and current_time - last_notification_time < timedelta(
minutes=3
):
logger.info(
f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут."
)
return
logger.info(
f"Отправка уведомлений администратору о недоступности сервера {server_name}..."
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Управление сервером", callback_data=f"manage_server|{server_name}"
)
)
for admin_id in ADMIN_ID:
await bot.send_message(
admin_id,
(
f"❌ <b>Сервер '{server_name}'</b> не отвечает более 3 минут.\n\n"
"Проверьте соединение к серверу, подключение к панели или удалите его из таблицы серверов в боте, "
"чтобы не выдать подписку к неработающему серверу."
),
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
logger.info(
f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}."
)
last_notification_times[server_name] = current_time
except Exception as e:
logger.error(f"Ошибка при отправке уведомления администраторам: {e}")
async def check_servers():
"""
Периодическая проверка серверов с учетом извлечения хоста из `api_url`.
"""
while True:
servers = await get_servers_from_db()
current_time = datetime.now()
logger.info(f"Начинаю проверку серверов: {current_time}")
for cluster_name, cluster_servers in servers.items():
logger.debug(f"Проверка кластеров: {cluster_name}")
for server in cluster_servers:
original_api_url = server["api_url"]
server_name = server["server_name"]
server_host = extract_host(original_api_url)
logger.debug(
f"Проверка доступности сервера '{server_name}' с хостом {server_host}"
)
is_online = await ping_server(server_host)
if is_online:
last_ping_times[server_name] = current_time
else:
last_ping_time = last_ping_times.get(server_name)
if last_ping_time and current_time - last_ping_time > timedelta(
minutes=3
):
logger.warning(
f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление."
)
await notify_admin(server_name)
elif not last_ping_time:
last_ping_times[server_name] = current_time
logger.info(
f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован."
)
logger.info("Завершена проверка всех серверов.")
await asyncio.sleep(30)
def extract_host(api_url: str) -> str:
"""
Извлекает только хост из `api_url` (без путей, портов и параметров).
"""
match = re.match(r"(https?://)?([^:/]+)", api_url)
if match:
host = match.group(2)
logger.debug(f"Извлечён хост: {host} из URL: {api_url}")
return host
logger.error(f"Не удалось извлечь хост из URL: {api_url}")
return api_url