Compare commits

...

615 Commits

Author SHA1 Message Date
Egor 6c4aa8246c Update Dockerfile 2025-10-12 07:33:07 +03:00
Egor b08be14c6a Update docker-registry.yml 2025-10-12 07:32:48 +03:00
Egor 14d4166ac9 Update docker-hub.yml 2025-10-12 07:32:32 +03:00
Egor c2e91265b8 Merge pull request #1293 from Fr1ngg/dev4
Фикс кнопок в рассылке для режима MAIN_MENU_MODE=text
2025-10-12 07:19:27 +03:00
Egor edd2809f73 Merge pull request #1292 from Fr1ngg/8xskxx-bedolaga/fix-miniapp-access-in-main_menu_mode=text
Fix broadcast buttons opening miniapp in text menu mode
2025-10-12 07:15:58 +03:00
Egor b657b9e95c Fix broadcast buttons opening miniapp in text menu mode 2025-10-12 07:15:37 +03:00
Egor d112d769b7 Merge pull request #1291 from Fr1ngg/95i1mf-bedolaga/update-miniapp/index.html-for-accurate-traffic-data
Ensure mini app shows fresh subscription usage
2025-10-12 07:07:01 +03:00
Egor 28d26cee17 Sync subscription traffic when opening mini app 2025-10-12 07:06:33 +03:00
Egor e7115461db Merge pull request #1290 from Fr1ngg/dev4
Новый режим работы бота через ЛК MiniApp
2025-10-12 06:54:04 +03:00
Egor c8205bd882 Update README.md 2025-10-12 06:53:02 +03:00
Egor 1314d1c5cb Update .env.example 2025-10-12 06:52:25 +03:00
Egor 138a8920d9 Merge pull request #1289 from Fr1ngg/sdanmv-bedolaga/add-text-only-menu-mode
Route subscription reminders to miniapp in text mode
2025-10-12 06:41:26 +03:00
Egor ac3890598f Route subscription reminders to miniapp in text mode 2025-10-12 06:40:31 +03:00
Egor 2784920fbb Merge pull request #1288 from Fr1ngg/revert-1287-2jiako-bedolaga/add-text-only-menu-mode
Revert "Route promo offer CTA buttons to miniapp in text mode"
2025-10-12 06:20:01 +03:00
Egor 81e5731f34 Revert "Route promo offer CTA buttons to miniapp in text mode" 2025-10-12 06:19:53 +03:00
Egor d637f461f9 Merge pull request #1287 from Fr1ngg/2jiako-bedolaga/add-text-only-menu-mode
Route promo offer CTA buttons to miniapp in text mode
2025-10-12 05:49:54 +03:00
Egor 7fbe5a2bd8 Route promo offer CTAs to miniapp in text mode 2025-10-12 05:47:28 +03:00
Egor 9819897dbb Merge pull request #1286 from Fr1ngg/revert-1285-kluzxc-bedolaga/add-text-only-menu-mode
Revert "Route notification CTAs to miniapp in text mode"
2025-10-12 05:47:16 +03:00
Egor d20f264b59 Revert "Route notification CTAs to miniapp in text mode" 2025-10-12 05:47:08 +03:00
Egor 3d4e8b4d1b Merge pull request #1285 from Fr1ngg/kluzxc-bedolaga/add-text-only-menu-mode
Route notification CTAs to miniapp in text mode
2025-10-12 05:32:33 +03:00
Egor 253df6a60a Route notification CTAs to miniapp in text mode 2025-10-12 05:32:15 +03:00
Egor fae50b2155 Merge pull request #1284 from Fr1ngg/revert-1283-fwlbh2-bedolaga/add-text-only-menu-mode
Revert "Add text-only main menu mode with miniapp profile shortcut"
2025-10-12 05:31:56 +03:00
Egor 0df5fe9844 Revert "Add text-only main menu mode with miniapp profile shortcut" 2025-10-12 05:31:40 +03:00
Egor 522146c054 Merge pull request #1283 from Fr1ngg/fwlbh2-bedolaga/add-text-only-menu-mode
Add text-only main menu mode with miniapp profile shortcut
2025-10-12 05:05:08 +03:00
Egor 9a3301bc38 Add text-only main menu mode with miniapp profile button 2025-10-12 05:02:47 +03:00
Egor c169b8e69a Merge pull request #1282 from Fr1ngg/dev4
Обновление миниапки под актуальный шаблон app-config из ремны + режима guide в боте
2025-10-12 04:47:37 +03:00
Egor dbc9cbbb39 Merge pull request #1281 from Fr1ngg/main
w
2025-10-12 04:45:35 +03:00
Egor ec768d68ef Merge pull request #1280 from Fr1ngg/fozp9n-bedolaga/update-guide-mode-for-app-config.json
Add app selection list to connection guide
2025-10-12 04:32:03 +03:00
Egor 254255fe94 Add Apple TV option to connection guide 2025-10-12 04:31:49 +03:00
Egor c1aaa3acb0 Merge pull request #1279 from Fr1ngg/revert-1277-rsxc5b-bedolaga/update-guide-mode-for-app-config.json
Revert "Add app selection list to connection guide"
2025-10-12 04:30:44 +03:00
Egor 4c2262e0b0 Revert "Add app selection list to connection guide" 2025-10-12 04:30:36 +03:00
Egor 9c4445505b Merge pull request #1278 from Fr1ngg/i89vp3-bedolaga/move-subscription-button-to-instructions
Move connect button into subscription step
2025-10-12 04:28:54 +03:00
Egor b47944ef7b Move connect button into subscription step 2025-10-12 04:28:39 +03:00
Egor 2b268a0e06 Merge pull request #1277 from Fr1ngg/rsxc5b-bedolaga/update-guide-mode-for-app-config.json
Add app selection list to connection guide
2025-10-12 04:23:04 +03:00
Egor 3ba9bddfc5 Add multi-app selection to connection guide 2025-10-12 04:22:47 +03:00
Egor e19f18931f Merge pull request #1276 from Fr1ngg/revert-1275-ht68uj-bedolaga/update-guide-mode-for-app-config.json
Revert "Adapt guide mode to new app config and connect links"
2025-10-12 04:22:37 +03:00
Egor b8cbfc6065 Revert "Adapt guide mode to new app config and connect links" 2025-10-12 04:22:29 +03:00
Egor e7714dc33d Merge pull request #1275 from Fr1ngg/ht68uj-bedolaga/update-guide-mode-for-app-config.json
Adapt guide mode to new app config and connect links
2025-10-12 04:11:03 +03:00
Egor 847d04fa42 Adapt guide mode to new app config and connect links 2025-10-12 04:10:46 +03:00
Egor 164b4f4e33 Merge pull request #1274 from Fr1ngg/owmhgs-bedolaga/update-redirect-link-in-miniapp/index.html
Use configured Happ redirect template in miniapp
2025-10-12 03:09:40 +03:00
Egor e1b7af2990 Use configured Happ redirect template in miniapp 2025-10-12 03:09:25 +03:00
Egor da74c7841d Merge pull request #1273 from Fr1ngg/revert-1272-rkb5qf-bedolaga/update-redirect-link-in-miniapp/index.html
Revert "Use configured Happ redirect template fallback"
2025-10-12 03:08:35 +03:00
Egor 68d83be87a Revert "Use configured Happ redirect template fallback" 2025-10-12 03:08:28 +03:00
Egor 57f820dc35 Merge pull request #1272 from Fr1ngg/rkb5qf-bedolaga/update-redirect-link-in-miniapp/index.html
Use configured Happ redirect template fallback
2025-10-12 03:05:30 +03:00
Egor cd0aba5a6c Use configured Happ redirect template fallback 2025-10-12 03:05:11 +03:00
Egor a43e68846a Update app-config.json 2025-10-12 02:47:56 +03:00
Egor adadeb4b2a Update app-config.json 2025-10-12 02:47:46 +03:00
Egor ad81cff59a Update index.html 2025-10-12 02:45:16 +03:00
Egor bf0c1ce711 Update and rename bedolaga_app2.svg to bedolaga_app3.svg 2025-10-12 02:41:45 +03:00
Egor c71401cad4 Rename bedolaga_app.svg to bedolaga_app2.svg 2025-10-12 02:36:39 +03:00
Egor 81744d3de8 Update bedolaga_app.svg 2025-10-12 02:36:05 +03:00
Egor f49dbeaf3a Update bedolaga_app.svg 2025-10-12 02:35:21 +03:00
Egor 649bd32af3 Create bedolaga_app.svg 2025-10-12 02:18:37 +03:00
Egor 55c37d8860 Merge pull request #1271 from Fr1ngg/2dsocc-bedolaga/fix-scroll-issue-in-modal-instructions
Add platform filter controls to installation modal
2025-10-12 02:07:25 +03:00
Egor c953cc150d Add platform filter controls to installation modal 2025-10-12 02:07:06 +03:00
Egor 59a78f0823 Merge pull request #1270 from Fr1ngg/revert-1269-mbe8rg-bedolaga/fix-scroll-issue-in-modal-instructions
Revert "Fix installation modal truncation for PC app list"
2025-10-12 02:00:49 +03:00
Egor 8ac3d5e826 Revert "Fix installation modal truncation for PC app list" 2025-10-12 02:00:41 +03:00
Egor 744b7d532c Merge pull request #1269 from Fr1ngg/mbe8rg-bedolaga/fix-scroll-issue-in-modal-instructions
Fix installation modal truncation for PC app list
2025-10-12 01:59:05 +03:00
Egor 72ec8d565c Allow installation modal to show full PC app list 2025-10-12 01:58:31 +03:00
Egor 9350868830 Merge pull request #1268 from Fr1ngg/revert-1267-2fc8ey-bedolaga/fix-scroll-issue-in-modal-instructions
Revert "Improve installation modal scroll handling"
2025-10-12 01:58:20 +03:00
Egor effbede310 Revert "Improve installation modal scroll handling" 2025-10-12 01:58:12 +03:00
Egor 4b3be5725c Merge pull request #1267 from Fr1ngg/2fc8ey-bedolaga/fix-scroll-issue-in-modal-instructions
Improve installation modal scroll handling
2025-10-12 01:48:07 +03:00
Egor 22ed539b3a Fix installation modal scrolling 2025-10-12 01:47:50 +03:00
Egor fa46a5b96c Merge pull request #1265 from Fr1ngg/rz4sct-bedolaga/update-miniapp/index.html-for-new-json-format
Fix desktop redirect to use scheme subscription URLs
2025-10-12 01:32:21 +03:00
Egor 63402474d2 Fix PC connect redirect handling 2025-10-12 01:32:06 +03:00
Egor 93db300dc7 Merge pull request #1264 from Fr1ngg/revert-1263-kdbq2j-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Ensure PC connect links use redirect template scheme payloads"
2025-10-12 01:31:47 +03:00
Egor 76e29314b2 Revert "Ensure PC connect links use redirect template scheme payloads" 2025-10-12 01:31:37 +03:00
Egor 7e5c1d388e Merge pull request #1263 from Fr1ngg/kdbq2j-bedolaga/update-miniapp/index.html-for-new-json-format
Ensure PC connect links use redirect template scheme payloads
2025-10-12 01:28:12 +03:00
Egor 6b14ab70c6 Fix PC connect links to honor selected platform 2025-10-12 01:27:54 +03:00
Egor 5e3cff20b1 Merge pull request #1262 from Fr1ngg/revert-1261-z7k7de-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Fix desktop redirect to use scheme subscription URLs"
2025-10-12 01:27:21 +03:00
Egor 34c1c0b806 Revert "Fix desktop redirect to use scheme subscription URLs" 2025-10-12 01:27:13 +03:00
Egor e69dca013c Merge pull request #1261 from Fr1ngg/z7k7de-bedolaga/update-miniapp/index.html-for-new-json-format
Fix desktop redirect to use scheme subscription URLs
2025-10-12 01:06:23 +03:00
Egor d201e07385 Fix desktop redirect to use scheme subscription URLs 2025-10-12 01:06:07 +03:00
Egor 613e239691 Merge pull request #1260 from Fr1ngg/revert-1258-j5y403-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Ensure desktop redirect targets prefer scheme links"
2025-10-12 01:05:51 +03:00
Egor b72e00de9d Revert "Ensure desktop redirect targets prefer scheme links" 2025-10-12 01:05:43 +03:00
Egor 0830461398 Merge pull request #1258 from Fr1ngg/j5y403-bedolaga/update-miniapp/index.html-for-new-json-format
Ensure desktop redirect targets prefer scheme links
2025-10-12 01:01:28 +03:00
Egor 9abf0c1da9 Merge pull request #1259 from Fr1ngg/revert-1257-26yx6h-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Ensure desktop redirects use scheme subscription URLs"
2025-10-12 01:01:11 +03:00
Egor 74148d4e7d Revert "Ensure desktop redirects use scheme subscription URLs" 2025-10-12 01:01:03 +03:00
Egor 8901cad885 Ensure desktop redirect targets prefer scheme links 2025-10-12 01:00:44 +03:00
Egor a800bd4e3c Merge pull request #1257 from Fr1ngg/26yx6h-bedolaga/update-miniapp/index.html-for-new-json-format
Ensure desktop redirects use scheme subscription URLs
2025-10-12 00:50:50 +03:00
Egor 5c3c438fb7 Fix desktop redirect scheme target 2025-10-12 00:50:34 +03:00
Egor 65df7ff58a Merge pull request #1256 from Fr1ngg/revert-1255-qabbzf-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Fix desktop connect redirect target selection"
2025-10-12 00:50:14 +03:00
Egor 46855439e2 Revert "Fix desktop connect redirect target selection" 2025-10-12 00:49:37 +03:00
Egor 7b3b4a8fb3 Merge pull request #1255 from Fr1ngg/qabbzf-bedolaga/update-miniapp/index.html-for-new-json-format
Fix desktop connect redirect target selection
2025-10-12 00:45:54 +03:00
Egor 8a90893b40 Fix desktop connect redirect target selection 2025-10-12 00:45:32 +03:00
Egor edf1d3dc29 Merge pull request #1254 from Fr1ngg/revert-1253-qt32vw-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Adjust desktop connect redirects to wrap scheme links"
2025-10-12 00:44:41 +03:00
Egor d5bf7f2749 Revert "Adjust desktop connect redirects to wrap scheme links" 2025-10-12 00:44:32 +03:00
Egor 5939d2d69a Merge pull request #1253 from Fr1ngg/qt32vw-bedolaga/update-miniapp/index.html-for-new-json-format
Adjust desktop connect redirects to wrap scheme links
2025-10-12 00:42:56 +03:00
Egor 90a2fd9fdc Fix PC redirect scheme targeting 2025-10-12 00:42:35 +03:00
Egor 841b183a59 Merge pull request #1252 from Fr1ngg/revert-1251-revert-1248-revert-1247-4egmuh-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Revert "Revert "Fix desktop connect redirects for scheme subscriptions"""
2025-10-12 00:39:38 +03:00
Egor 9a73621ff4 Revert "Revert "Revert "Fix desktop connect redirects for scheme subscriptions""" 2025-10-12 00:39:29 +03:00
Egor 71e8ceace9 Merge pull request #1251 from Fr1ngg/revert-1248-revert-1247-4egmuh-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Revert "Fix desktop connect redirects for scheme subscriptions""
2025-10-12 00:27:47 +03:00
Egor 4e8e11fc7b Revert "Revert "Fix desktop connect redirects for scheme subscriptions"" 2025-10-12 00:27:38 +03:00
Egor b3a1a8ddb8 Merge pull request #1250 from Fr1ngg/revert-1249-lhhq28-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Prioritize scheme-based redirect targets on desktop"
2025-10-12 00:27:18 +03:00
Egor 029d162e44 Revert "Prioritize scheme-based redirect targets on desktop" 2025-10-12 00:27:10 +03:00
Egor 86ab3e544d Merge pull request #1249 from Fr1ngg/lhhq28-bedolaga/update-miniapp/index.html-for-new-json-format
Prioritize scheme-based redirect targets on desktop
2025-10-12 00:25:37 +03:00
Egor 587ca35049 Fix desktop redirect target selection 2025-10-12 00:25:20 +03:00
Egor 02c68e5983 Merge pull request #1248 from Fr1ngg/revert-1247-4egmuh-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Fix desktop connect redirects for scheme subscriptions"
2025-10-12 00:24:39 +03:00
Egor 9ddc6b8f0c Revert "Fix desktop connect redirects for scheme subscriptions" 2025-10-12 00:24:29 +03:00
Egor eb69b166fe Merge pull request #1247 from Fr1ngg/4egmuh-bedolaga/update-miniapp/index.html-for-new-json-format
Fix desktop connect redirects for scheme subscriptions
2025-10-12 00:22:45 +03:00
Egor 0ea6476834 Fix desktop connect redirects for non-Happ apps 2025-10-12 00:22:28 +03:00
Egor 6994735b0c Merge pull request #1246 from Fr1ngg/revert-1245-wklyjo-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Adjust desktop connect redirects to wrap scheme links"
2025-10-12 00:22:09 +03:00
Egor f504322afd Revert "Adjust desktop connect redirects to wrap scheme links" 2025-10-12 00:22:02 +03:00
Egor a60ff3bdb1 Merge pull request #1245 from Fr1ngg/wklyjo-bedolaga/update-miniapp/index.html-for-new-json-format
Adjust desktop connect redirects to wrap scheme links
2025-10-12 00:20:25 +03:00
Egor 1eec3b71f6 Adjust PC redirect targets for connect links 2025-10-12 00:20:04 +03:00
Egor 31945067c9 Merge pull request #1244 from Fr1ngg/revert-1243-gh1a1w-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Refine desktop connect redirects to use subscription URLs"
2025-10-12 00:19:29 +03:00
Egor fe0da51909 Revert "Refine desktop connect redirects to use subscription URLs" 2025-10-12 00:19:19 +03:00
Egor 9ddf7891aa Merge pull request #1243 from Fr1ngg/gh1a1w-bedolaga/update-miniapp/index.html-for-new-json-format
Refine desktop connect redirects to use subscription URLs
2025-10-12 00:03:50 +03:00
Egor ce4eb3341d Adjust connect link priorities for desktop 2025-10-12 00:03:32 +03:00
Egor f8e68de2bb Merge pull request #1242 from Fr1ngg/revert-1241-4g4jc1-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Adjust connect button links for PC redirect handling"
2025-10-12 00:03:07 +03:00
Egor 6613de548c Revert "Adjust connect button links for PC redirect handling" 2025-10-12 00:02:58 +03:00
Egor 1d3f02d26a Merge pull request #1241 from Fr1ngg/4g4jc1-bedolaga/update-miniapp/index.html-for-new-json-format
Adjust connect button links for PC redirect handling
2025-10-11 23:46:46 +03:00
Egor 77d571980f Refine connect link routing for device-specific flows 2025-10-11 23:46:30 +03:00
Egor db79c538f0 Merge pull request #1240 from Fr1ngg/revert-1239-yooawl-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Ensure desktop connect buttons use Happ redirect"
2025-10-11 23:46:10 +03:00
Egor c066b9929d Revert "Ensure desktop connect buttons use Happ redirect" 2025-10-11 23:46:01 +03:00
Egor ac39a22f61 Merge pull request #1239 from Fr1ngg/yooawl-bedolaga/update-miniapp/index.html-for-new-json-format
Ensure desktop connect buttons use Happ redirect
2025-10-11 23:42:48 +03:00
Egor 814617bd64 Adjust miniapp connect links for device redirects 2025-10-11 23:42:21 +03:00
Egor ba57848b60 Merge pull request #1238 from Fr1ngg/revert-1237-qau9xs-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Enhance installation modal for new config format"
2025-10-11 23:31:27 +03:00
Egor 269cbe6b2c Revert "Enhance installation modal for new config format" 2025-10-11 23:31:19 +03:00
Egor 07d3eff9a0 Merge pull request #1237 from Fr1ngg/qau9xs-bedolaga/update-miniapp/index.html-for-new-json-format
Enhance installation modal for new config format
2025-10-11 23:28:05 +03:00
Egor 4e63a99197 Fix connect links for Happ and platform schemes 2025-10-11 23:27:49 +03:00
Egor fe36737406 Merge pull request #1236 from Fr1ngg/revert-1235-kq2y5m-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Fix Happ connect link handling"
2025-10-11 23:27:12 +03:00
Egor 48917e47db Revert "Fix Happ connect link handling" 2025-10-11 23:27:04 +03:00
Egor 6f348b00d3 Merge pull request #1235 from Fr1ngg/kq2y5m-bedolaga/update-miniapp/index.html-for-new-json-format
Fix Happ connect link handling
2025-10-11 23:21:14 +03:00
Egor ea9da58e35 Fix Happ connect link handling 2025-10-11 23:20:53 +03:00
Egor 036e4d275d Merge pull request #1234 from Fr1ngg/revert-1233-8a5ts1-bedolaga/update-miniapp/index.html-for-new-json-format
Revert "Enhance installation modal for new config format"
2025-10-11 23:20:38 +03:00
Egor cff14989ae Revert "Enhance installation modal for new config format" 2025-10-11 23:20:29 +03:00
Egor 10da004e5d Merge pull request #1233 from Fr1ngg/8a5ts1-bedolaga/update-miniapp/index.html-for-new-json-format
Enhance installation modal for new config format
2025-10-11 21:13:36 +03:00
Egor ed0f62542f Enhance installation modal for new config format 2025-10-11 21:13:17 +03:00
Egor cc2b7c2036 Update app-config.json 2025-10-11 20:34:26 +03:00
Egor d3a87d62b4 Update app-config.json 2025-10-11 20:34:14 +03:00
Egor ef70f176dd Update Dockerfile 2025-10-11 09:06:35 +03:00
Egor 7a111bae6c Update docker-registry.yml 2025-10-11 09:06:18 +03:00
Egor 3132b32545 Update docker-hub.yml 2025-10-11 09:06:07 +03:00
Egor 10da3595b9 Merge pull request #1231 from Fr1ngg/dev4
Личный кабинет в миниапке
2025-10-11 08:47:20 +03:00
Egor f0f3f3508f Merge pull request #1230 from Fr1ngg/jb1s1w-bedolaga/fix-admin-notifications-for-miniapp-index
Add admin notifications to miniapp subscription flows
2025-10-11 08:39:43 +03:00
Egor 60b85911e1 Add admin notifications to miniapp subscription flows 2025-10-11 08:39:19 +03:00
Egor e78d75a440 Merge pull request #1229 from Fr1ngg/r8bqjd-bedolaga/fix-admin-notification-error
Fix admin topup notifications without DB session
2025-10-11 08:25:27 +03:00
Egor de57db14ea Delay first-topup flag until after referral processing 2025-10-11 08:25:11 +03:00
Egor b85d5083a9 Merge pull request #1228 from Fr1ngg/revert-1227-50t4gr-bedolaga/fix-admin-notification-error
Revert "Fix admin topup notifications without DB session"
2025-10-11 08:24:58 +03:00
Egor 862fcc53c4 Revert "Fix admin topup notifications without DB session" 2025-10-11 08:24:50 +03:00
Egor 42f1757508 Merge pull request #1227 from Fr1ngg/50t4gr-bedolaga/fix-admin-notification-error
Fix admin topup notifications without DB session
2025-10-11 08:02:43 +03:00
Egor 50279979ce Fix admin topup notifications without DB session 2025-10-11 08:02:25 +03:00
Egor ec00fe12a0 Merge pull request #1226 from Fr1ngg/0y4863-bedolaga/fix-mobile-modal-to-cover-full-screen
Make installation modal full screen on mobile
2025-10-11 07:46:06 +03:00
Egor 0806ed9f1e Make installation modal full screen on mobile 2025-10-11 07:45:53 +03:00
Egor 11b886d6ca Merge pull request #1225 from Fr1ngg/revert-1224-g7l4tv-bedolaga/fix-mobile-modal-to-cover-full-screen
Revert "Make subscription modal full-screen on mobile"
2025-10-11 07:45:23 +03:00
Egor 291695395f Revert "Make subscription modal full-screen on mobile" 2025-10-11 07:45:15 +03:00
Egor f787dfef04 Merge pull request #1224 from Fr1ngg/g7l4tv-bedolaga/fix-mobile-modal-to-cover-full-screen
Make subscription modal full-screen on mobile
2025-10-11 07:38:17 +03:00
Egor edc6b766cb Adjust mobile modal to full-screen 2025-10-11 07:38:03 +03:00
Egor da2d2c826a Merge pull request #1223 from Fr1ngg/revert-1222-ar94nr-bedolaga/fix-mobile-modal-to-cover-full-screen
Revert "Adjust subscription modal layout on mobile"
2025-10-11 07:37:44 +03:00
Egor 0ad1f58fd4 Revert "Adjust subscription modal layout on mobile" 2025-10-11 07:37:37 +03:00
Egor 9f398dbbe5 Merge pull request #1222 from Fr1ngg/ar94nr-bedolaga/fix-mobile-modal-to-cover-full-screen
Adjust subscription modal layout on mobile
2025-10-11 07:34:21 +03:00
Egor 3a33928c9e Adjust subscription modal layout on mobile 2025-10-11 07:31:34 +03:00
Egor cf77b38fe7 Merge pull request #1221 from Fr1ngg/ql23rs-bedolaga/fix-subscription-block-height-for-mobile
Adjust mini app subscription card height for promo discounts
2025-10-11 07:12:47 +03:00
Egor 3ab2dc0484 Adjust purchase card layout for promo discounts 2025-10-11 07:12:34 +03:00
Egor 27b5f70712 Merge pull request #1220 from Fr1ngg/revert-1219-dhoqjs-bedolaga/fix-layout-for-subscription-block-on-mobile
Revert "Adjust mobile spacing in mini app subscription purchase section"
2025-10-11 07:02:07 +03:00
Egor 05e47e3a1c Revert "Adjust mobile spacing in mini app subscription purchase section" 2025-10-11 07:01:59 +03:00
Egor e85a6dd9e0 Merge pull request #1219 from Fr1ngg/dhoqjs-bedolaga/fix-layout-for-subscription-block-on-mobile
Adjust mobile spacing in mini app subscription purchase section
2025-10-11 06:59:43 +03:00
Egor 595c7ce5bb Adjust mobile spacing for subscription actions 2025-10-11 06:59:27 +03:00
Egor 46e3433b55 Merge pull request #1218 from Fr1ngg/revert-1217-fm92lt-bedolaga/fix-layout-for-subscription-block-on-mobile
Revert "Fix subscription action spacing on narrow screens"
2025-10-11 06:58:55 +03:00
Egor f7577c1acc Revert "Fix subscription action spacing on narrow screens" 2025-10-11 06:58:46 +03:00
Egor 2e57522a58 Merge pull request #1217 from Fr1ngg/fm92lt-bedolaga/fix-layout-for-subscription-block-on-mobile
Fix subscription action spacing on narrow screens
2025-10-11 06:57:08 +03:00
Egor a3ca5725a6 Adjust purchase actions spacing on narrow screens 2025-10-11 06:56:53 +03:00
Egor 3a09240019 Merge pull request #1216 from Fr1ngg/revert-1215-uv8nkp-bedolaga/fix-layout-for-subscription-block-on-mobile
Revert "Adjust mobile spacing for subscription purchase actions"
2025-10-11 06:56:03 +03:00
Egor b3ad09f0a1 Revert "Adjust mobile spacing for subscription purchase actions" 2025-10-11 06:55:54 +03:00
Egor de515862f4 Merge pull request #1215 from Fr1ngg/uv8nkp-bedolaga/fix-layout-for-subscription-block-on-mobile
Adjust mobile spacing for subscription purchase actions
2025-10-11 06:54:18 +03:00
Egor c49e8d6c18 Adjust purchase summary spacing on small screens 2025-10-11 06:53:57 +03:00
Egor decafb1d0e Merge pull request #1214 from Fr1ngg/uw7hjl-bedolaga/update-subscription-block-design-on-miniapp/index.html
Align subscription period buttons with traffic cards
2025-10-11 06:44:09 +03:00
Egor 8f19a5e205 Align subscription period buttons with traffic cards 2025-10-11 06:43:52 +03:00
Egor fc75009e9b Merge pull request #1213 from Fr1ngg/aug6ak-bedolaga/redesign-subscription-section-for-mobile
Improve mobile subscription period layout and move copy link button
2025-10-11 06:20:15 +03:00
Egor c753f2f67e Improve subscription purchase mobile layout 2025-10-11 06:19:49 +03:00
Egor 5a260c00d0 Merge pull request #1212 from Fr1ngg/revert-1211-ddsp1p-bedolaga/redesign-subscription-section-for-mobile
Revert "Improve mobile subscription layout and relocate copy button"
2025-10-11 06:19:20 +03:00
Egor 6f690a672e Revert "Improve mobile subscription layout and relocate copy button" 2025-10-11 06:19:11 +03:00
Egor 3b2c560947 Merge pull request #1211 from Fr1ngg/ddsp1p-bedolaga/redesign-subscription-section-for-mobile
Improve mobile subscription layout and relocate copy button
2025-10-11 06:11:04 +03:00
Egor fb1641c5ba Improve mobile subscription layout and relocate copy button 2025-10-11 06:10:20 +03:00
Egor 91615ec4b9 Merge pull request #1210 from Fr1ngg/yh3i06-bedolaga/modify-auto-payment-feature-in-miniapp
Honor mini app autopay default day
2025-10-11 05:48:27 +03:00
Egor fe98d4391f Allow same-day autopay normalization 2025-10-11 05:48:04 +03:00
Egor 76f8ccdd85 Merge pull request #1209 from Fr1ngg/revert-1208-iyctz5-bedolaga/modify-auto-payment-feature-in-miniapp
Revert "Honor mini app autopay default day"
2025-10-11 05:47:44 +03:00
Egor 45bc3bc15c Revert "Honor mini app autopay default day" 2025-10-11 05:47:35 +03:00
Egor 0975fdea4d Merge pull request #1208 from Fr1ngg/iyctz5-bedolaga/modify-auto-payment-feature-in-miniapp
Honor mini app autopay default day
2025-10-11 05:34:45 +03:00
Egor 61bf48b7fa Honor mini app autopay default day 2025-10-11 05:34:05 +03:00
Egor 0ff4dd4321 Merge pull request #1207 from Fr1ngg/revert-1206-y71m00-bedolaga/modify-auto-payment-feature-in-miniapp
Revert "Ensure mini app autopay shows all day options"
2025-10-11 05:33:50 +03:00
Egor e0edc6bcdc Revert "Ensure mini app autopay shows all day options" 2025-10-11 05:33:42 +03:00
Egor 14e58421e4 Merge pull request #1206 from Fr1ngg/y71m00-bedolaga/modify-auto-payment-feature-in-miniapp
Ensure mini app autopay shows all day options
2025-10-11 05:19:16 +03:00
Egor b24ba2e75f Ensure mini app autopay shows all day options 2025-10-11 05:19:02 +03:00
Egor d9be872581 Merge pull request #1203 from Fr1ngg/revert-1202-1ji1v8-bedolaga/modify-auto-payment-feature-in-miniapp
Revert "Connect mini app autopay UI to backend"
2025-10-11 05:18:29 +03:00
Egor 30ab648784 Merge pull request #1205 from Fr1ngg/revert-1204-y7k7f9-bedolaga/modify-auto-payment-feature-in-miniapp
Revert "Fix autopay day parsing in mini app"
2025-10-11 05:18:09 +03:00
Egor b730de4175 Revert "Fix autopay day parsing in mini app" 2025-10-11 05:16:39 +03:00
Egor 6bfb484a9b Merge pull request #1204 from Fr1ngg/y7k7f9-bedolaga/modify-auto-payment-feature-in-miniapp
Fix autopay day parsing in mini app
2025-10-11 05:15:16 +03:00
Egor 0e6f6d0908 Handle autopay day labels when parsing options 2025-10-11 05:14:54 +03:00
Egor 90a495e942 Revert "Connect mini app autopay UI to backend" 2025-10-11 05:14:25 +03:00
Egor 0182e190a8 Merge pull request #1202 from Fr1ngg/1ji1v8-bedolaga/modify-auto-payment-feature-in-miniapp
Connect mini app autopay UI to backend
2025-10-11 05:05:57 +03:00
Egor 30274dce20 Connect mini app autopay UI to backend 2025-10-11 05:05:36 +03:00
Egor 0098cc724f Merge pull request #1201 from Fr1ngg/revert-1200-mt5flx-bedolaga/modify-auto-payment-feature-in-miniapp
Revert "Enable mini app autopay controls"
2025-10-11 05:05:23 +03:00
Egor d9cf4008fe Revert "Enable mini app autopay controls" 2025-10-11 05:05:15 +03:00
Egor ca83cf5349 Merge pull request #1200 from Fr1ngg/mt5flx-bedolaga/modify-auto-payment-feature-in-miniapp
Enable mini app autopay controls
2025-10-11 05:02:11 +03:00
Egor bf5e267822 Add mini app autopay endpoints and payloads 2025-10-11 05:01:55 +03:00
Egor b51a0c04bc Merge pull request #1199 from Fr1ngg/revert-1198-c5f5ag-bedolaga/modify-auto-payment-feature-in-miniapp
Revert "Revamp autopay settings UI in mini app"
2025-10-11 05:01:18 +03:00
Egor 42e19de19c Revert "Revamp autopay settings UI in mini app" 2025-10-11 05:01:09 +03:00
Egor 56c5e04c76 Merge pull request #1198 from Fr1ngg/c5f5ag-bedolaga/modify-auto-payment-feature-in-miniapp
Revamp autopay settings UI in mini app
2025-10-11 04:44:56 +03:00
Egor 556f9dbd45 Revamp mini-app autopay controls 2025-10-11 04:44:41 +03:00
Egor 562fe9dccb Merge pull request #1197 from Fr1ngg/revert-1196-qbc98z-bedolaga/redesign-autopayment-feature-in-miniapp/index.html
Revert "Add autopay toggle controls to miniapp UI"
2025-10-11 04:44:29 +03:00
Egor 960063fdca Revert "Add autopay toggle controls to miniapp UI" 2025-10-11 04:44:04 +03:00
Egor 4a4e7406c3 Merge pull request #1196 from Fr1ngg/qbc98z-bedolaga/redesign-autopayment-feature-in-miniapp/index.html
Add autopay toggle controls to miniapp UI
2025-10-11 04:39:33 +03:00
Egor fca88c5e08 feat: redesign autopay controls in miniapp 2025-10-11 04:39:13 +03:00
Egor 55dcb61798 Merge pull request #1195 from Fr1ngg/k66ujv-bedolaga/refactor-subscribe-button-in-miniapp/index.html
fix: restore connect fallback when instructions missing
2025-10-11 04:08:28 +03:00
Egor 3d9cc65336 fix: keep connect action without instructions 2025-10-11 04:08:08 +03:00
Egor 61c69e6827 Merge pull request #1194 from Fr1ngg/revert-1193-2s0o2x-bedolaga/refactor-subscribe-button-in-miniapp/index.html
Revert "fix: hide connect button for inactive users"
2025-10-11 04:07:30 +03:00
Egor 246c9d3935 Revert "fix: hide connect button for inactive users" 2025-10-11 04:07:21 +03:00
Egor 7f6b0f4190 Merge pull request #1193 from Fr1ngg/2s0o2x-bedolaga/refactor-subscribe-button-in-miniapp/index.html
fix: hide connect button for inactive users
2025-10-11 03:53:31 +03:00
Egor 9e495e9f64 fix: hide connect button for inactive users 2025-10-11 03:53:15 +03:00
Egor 58bc8e97ee Merge pull request #1192 from Fr1ngg/revert-1191-revert-1186-revert-1185-tl4a1p-bedolaga/refactor-subscribe-button-in-miniapp/index.html
Revert "Revert "Revert "feat: redesign miniapp connect flow"""
2025-10-11 03:52:43 +03:00
Egor f491a6bf00 Revert "Revert "Revert "feat: redesign miniapp connect flow""" 2025-10-11 03:51:48 +03:00
Egor 8abcd0707d Merge pull request #1191 from Fr1ngg/revert-1186-revert-1185-tl4a1p-bedolaga/refactor-subscribe-button-in-miniapp/index.html
Revert "Revert "feat: redesign miniapp connect flow""
2025-10-11 03:30:25 +03:00
Egor b4ac9cd2bc Revert "Revert "feat: redesign miniapp connect flow"" 2025-10-11 03:30:15 +03:00
Egor b18759b69f Merge pull request #1190 from Fr1ngg/revert-1187-revert-1184-ulw2na-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Hide connection lists until subscription is active""
2025-10-11 03:29:57 +03:00
Egor d327ad9379 Revert "Revert "Hide connection lists until subscription is active"" 2025-10-11 03:29:49 +03:00
Egor cfe86bd8d3 Merge pull request #1189 from Fr1ngg/revert-1188-84cuhz-bedolaga/update-miniapp/index.html-logic
Revert "Add floating connect button for installation guide"
2025-10-11 03:29:42 +03:00
Egor 5cb4479a70 Revert "Add floating connect button for installation guide" 2025-10-11 03:29:35 +03:00
Egor 64664fa30f Merge pull request #1188 from Fr1ngg/84cuhz-bedolaga/update-miniapp/index.html-logic
Add floating connect button for installation guide
2025-10-11 03:27:20 +03:00
Egor 15ed087142 Refine mini app connect entry point 2025-10-11 03:27:06 +03:00
Egor be6d358ed0 Merge pull request #1187 from Fr1ngg/revert-1184-ulw2na-bedolaga/update-miniapp/index.html-logic
Revert "Hide connection lists until subscription is active"
2025-10-11 03:26:20 +03:00
Egor dd77ecae9d Revert "Hide connection lists until subscription is active" 2025-10-11 03:26:12 +03:00
Egor edfb325ccd Merge pull request #1186 from Fr1ngg/revert-1185-tl4a1p-bedolaga/refactor-subscribe-button-in-miniapp/index.html
Revert "feat: redesign miniapp connect flow"
2025-10-11 03:25:40 +03:00
Egor a4a6685dbb Revert "feat: redesign miniapp connect flow" 2025-10-11 03:25:32 +03:00
Egor ae9d32ceb4 Merge pull request #1185 from Fr1ngg/tl4a1p-bedolaga/refactor-subscribe-button-in-miniapp/index.html
feat: redesign miniapp connect flow
2025-10-11 03:22:14 +03:00
Egor fa12448068 feat: redesign connect button and installation guide 2025-10-11 03:21:58 +03:00
Egor a72b7ce3c8 Merge pull request #1184 from Fr1ngg/ulw2na-bedolaga/update-miniapp/index.html-logic
Hide connection lists until subscription is active
2025-10-11 03:07:02 +03:00
Egor 1bf150f45a Hide connected resources without subscription 2025-10-11 03:06:43 +03:00
Egor e0633c409d Merge pull request #1183 from Fr1ngg/revert-1182-8soheo-bedolaga/update-miniapp/index.html-logic
Revert "Remove purchase action from missing subscription card"
2025-10-11 03:06:30 +03:00
Egor 2621b2f8be Revert "Remove purchase action from missing subscription card" 2025-10-11 03:06:22 +03:00
Egor ba1394c35f Merge pull request #1182 from Fr1ngg/8soheo-bedolaga/update-miniapp/index.html-logic
Remove purchase action from missing subscription card
2025-10-11 02:32:33 +03:00
Egor 1072b64af8 Remove duplicate purchase button from missing subscription card 2025-10-11 02:32:18 +03:00
Egor d3584d52e8 Merge pull request #1181 from Fr1ngg/revert-1180-m51uu7-bedolaga/update-miniapp/index.html-logic
Revert "Restore mini app trial activation and purchase flow"
2025-10-11 02:31:20 +03:00
Egor f32b9242ba Revert "Restore mini app trial activation and purchase flow" 2025-10-11 02:31:12 +03:00
Egor 516a1f3e52 Merge pull request #1180 from Fr1ngg/m51uu7-bedolaga/update-miniapp/index.html-logic
Restore mini app trial activation and purchase flow
2025-10-11 02:21:38 +03:00
Egor 57906b1df9 Fix mini app trial activation and purchase flow 2025-10-11 02:21:22 +03:00
Egor 83865dd0b9 Merge pull request #1179 from Fr1ngg/revert-1178-s7vtov-bedolaga/update-miniapp/index.html-logic
Revert "Fix mini app trial activation and purchase options"
2025-10-11 02:21:04 +03:00
Egor 69553d2551 Revert "Fix mini app trial activation and purchase options" 2025-10-11 02:20:55 +03:00
Egor 02a8b932a3 Merge pull request #1178 from Fr1ngg/s7vtov-bedolaga/update-miniapp/index.html-logic
Fix mini app trial activation and purchase options
2025-10-11 02:14:05 +03:00
Egor b27624021f Fix mini app trial activation and purchase options 2025-10-11 02:13:48 +03:00
Egor 4fde44eefd Merge pull request #1177 from Fr1ngg/revert-1176-revert-1171-revert-1170-2xx781-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Revert "Polish mini app subscription state handling"""
2025-10-11 02:13:20 +03:00
Egor 37f5977efd Revert "Revert "Revert "Polish mini app subscription state handling""" 2025-10-11 02:13:13 +03:00
Egor a9cfbb4a4d Merge pull request #1176 from Fr1ngg/revert-1171-revert-1170-2xx781-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Polish mini app subscription state handling""
2025-10-11 02:00:50 +03:00
Egor c7beec1df0 Revert "Revert "Polish mini app subscription state handling"" 2025-10-11 02:00:39 +03:00
Egor 172c3426b9 Merge pull request #1175 from Fr1ngg/revert-1174-cvpz16-bedolaga/update-miniapp/index.html-logic
Revert "Improve miniapp onboarding for unsubscribed users"
2025-10-11 01:58:32 +03:00
Egor a616987eab Revert "Improve miniapp onboarding for unsubscribed users" 2025-10-11 01:58:25 +03:00
Egor 0742d7cdc9 Merge pull request #1174 from Fr1ngg/cvpz16-bedolaga/update-miniapp/index.html-logic
Improve miniapp onboarding for unsubscribed users
2025-10-11 01:57:16 +03:00
Egor f10a84399b Improve miniapp onboarding for unsubscribed users 2025-10-11 01:56:56 +03:00
Egor 6d2eb30704 Merge pull request #1173 from Fr1ngg/revert-1172-3gh1f0-bedolaga/update-miniapp/index.html-logic
Revert "Handle miniapp flows for unregistered and inactive users"
2025-10-11 01:56:37 +03:00
Egor 618a5153f6 Revert "Handle miniapp flows for unregistered and inactive users" 2025-10-11 01:56:28 +03:00
Egor 292378ebbc Merge pull request #1172 from Fr1ngg/3gh1f0-bedolaga/update-miniapp/index.html-logic
Handle miniapp flows for unregistered and inactive users
2025-10-11 01:55:12 +03:00
Egor 645cca088a Handle miniapp flows for unregistered and inactive users 2025-10-11 01:54:46 +03:00
Egor 4159689a4e Merge pull request #1171 from Fr1ngg/revert-1170-2xx781-bedolaga/update-miniapp/index.html-logic
Revert "Polish mini app subscription state handling"
2025-10-11 01:53:55 +03:00
Egor dd2ec37bec Revert "Polish mini app subscription state handling" 2025-10-11 01:53:47 +03:00
Egor 125131c8e6 Merge pull request #1170 from Fr1ngg/2xx781-bedolaga/update-miniapp/index.html-logic
Polish mini app subscription state handling
2025-10-11 01:52:14 +03:00
Egor 3b895b73f9 Polish mini app subscription state handling 2025-10-11 01:51:57 +03:00
Egor 39c915acc4 Merge pull request #1169 from Fr1ngg/revert-1168-74stxg-bedolaga/update-miniapp/index.html-logic
Revert "Update mini app onboarding for unregistered and inactive users"
2025-10-11 01:10:29 +03:00
Egor 8f08489939 Revert "Update mini app onboarding for unregistered and inactive users" 2025-10-11 01:10:22 +03:00
Egor baf69495d6 Merge pull request #1168 from Fr1ngg/74stxg-bedolaga/update-miniapp/index.html-logic
Update mini app onboarding for unregistered and inactive users
2025-10-11 01:08:29 +03:00
Egor 7e9c1bd784 Update mini app states for unregistered and inactive users 2025-10-11 01:08:11 +03:00
Egor 509e7ffe14 Merge pull request #1167 from Fr1ngg/revert-1166-revert-1157-revert-1156-t3tlcj-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Revert "Handle registration and missing subscription states in mini app"""
2025-10-11 01:07:39 +03:00
Egor e4547b3ca0 Revert "Revert "Revert "Handle registration and missing subscription states in mini app""" 2025-10-11 01:07:31 +03:00
Egor 5d638dbba9 Merge pull request #1166 from Fr1ngg/revert-1157-revert-1156-t3tlcj-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Handle registration and missing subscription states in mini app""
2025-10-11 01:06:12 +03:00
Egor d0390bb432 Revert "Revert "Handle registration and missing subscription states in mini app"" 2025-10-11 01:06:00 +03:00
Egor 66eaadd628 Merge pull request #1165 from Fr1ngg/revert-1164-revert-1153-revert-1152-7gq87l-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Revert "Improve miniapp onboarding states for unregistered users"""
2025-10-11 01:05:02 +03:00
Egor 8a5b5599cc Revert "Revert "Revert "Improve miniapp onboarding states for unregistered users""" 2025-10-11 01:04:48 +03:00
Egor a7935f3e88 Merge pull request #1164 from Fr1ngg/revert-1153-revert-1152-7gq87l-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Improve miniapp onboarding states for unregistered users""
2025-10-11 01:02:37 +03:00
Egor bbb89633c3 Revert "Revert "Improve miniapp onboarding states for unregistered users"" 2025-10-11 01:02:29 +03:00
Egor 40a690b4bd Merge pull request #1163 from Fr1ngg/revert-1162-revert-1155-revert-1154-c3rvdj-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Revert "Handle registration and empty subscription states in mini app"""
2025-10-11 01:01:55 +03:00
Egor 4f06a183be Revert "Revert "Revert "Handle registration and empty subscription states in mini app""" 2025-10-11 01:01:47 +03:00
Egor 6701a63a0a Merge pull request #1162 from Fr1ngg/revert-1155-revert-1154-c3rvdj-bedolaga/update-miniapp/index.html-logic
Revert "Revert "Handle registration and empty subscription states in mini app""
2025-10-11 01:00:52 +03:00
Egor 905e1f1861 Revert "Revert "Handle registration and empty subscription states in mini app"" 2025-10-11 01:00:44 +03:00
Egor fe91fc1c29 Merge pull request #1161 from Fr1ngg/revert-1160-u5iv6j-bedolaga/update-miniapp/index.html-logic
Revert "Handle registration and empty subscription states in miniapp"
2025-10-11 00:59:41 +03:00
Egor df8b85a179 Revert "Handle registration and empty subscription states in miniapp" 2025-10-11 00:59:34 +03:00
Egor 8d08b23d0f Merge pull request #1160 from Fr1ngg/u5iv6j-bedolaga/update-miniapp/index.html-logic
Handle registration and empty subscription states in miniapp
2025-10-11 00:57:30 +03:00
Egor ce087beea6 Add tests for miniapp subscription responses 2025-10-11 00:57:12 +03:00
Egor e63d2158c3 Merge pull request #1159 from Fr1ngg/revert-1158-adv2s5-bedolaga/update-miniapp/index.html-logic
Revert "feat: improve miniapp onboarding states"
2025-10-11 00:56:46 +03:00
Egor eb64d8ed3a Revert "feat: improve miniapp onboarding states" 2025-10-11 00:56:37 +03:00
Egor fc4e17d995 Merge pull request #1158 from Fr1ngg/adv2s5-bedolaga/update-miniapp/index.html-logic
feat: improve miniapp onboarding states
2025-10-11 00:53:54 +03:00
Egor f22fd67b44 feat: improve miniapp onboarding states 2025-10-11 00:53:37 +03:00
Egor e9ba19011a Merge pull request #1157 from Fr1ngg/revert-1156-t3tlcj-bedolaga/update-miniapp/index.html-logic
Revert "Handle registration and missing subscription states in mini app"
2025-10-11 00:53:03 +03:00
Egor 6569e45d9c Revert "Handle registration and missing subscription states in mini app" 2025-10-11 00:52:55 +03:00
Egor bc12bf47d6 Merge pull request #1156 from Fr1ngg/t3tlcj-bedolaga/update-miniapp/index.html-logic
Handle registration and missing subscription states in mini app
2025-10-11 00:51:54 +03:00
Egor e731ccc055 Add mini app states for registration and subscription purchase 2025-10-11 00:51:36 +03:00
Egor c648e78e34 Merge pull request #1155 from Fr1ngg/revert-1154-c3rvdj-bedolaga/update-miniapp/index.html-logic
Revert "Handle registration and empty subscription states in mini app"
2025-10-11 00:51:17 +03:00
Egor ef073bd307 Revert "Handle registration and empty subscription states in mini app" 2025-10-11 00:51:09 +03:00
Egor dde44a8afd Merge pull request #1154 from Fr1ngg/c3rvdj-bedolaga/update-miniapp/index.html-logic
Handle registration and empty subscription states in mini app
2025-10-11 00:48:58 +03:00
Egor 0981aa6efa Adjust mini app states for unregistered and inactive users 2025-10-11 00:47:22 +03:00
Egor 2b71e1701c Merge pull request #1153 from Fr1ngg/revert-1152-7gq87l-bedolaga/update-miniapp/index.html-logic
Revert "Improve miniapp onboarding states for unregistered users"
2025-10-11 00:47:04 +03:00
Egor 2bd5c3e061 Revert "Improve miniapp onboarding states for unregistered users" 2025-10-11 00:46:56 +03:00
Egor 8f39c69f22 Merge pull request #1152 from Fr1ngg/7gq87l-bedolaga/update-miniapp/index.html-logic
Improve miniapp onboarding states for unregistered users
2025-10-11 00:45:42 +03:00
Egor b4301265ec Improve miniapp states for unregistered users 2025-10-11 00:45:24 +03:00
Egor 7d793eff6a Merge pull request #1151 from Fr1ngg/revert-1150-k49afg-bedolaga/update-miniapp/index.html-logic
Revert "feat: improve miniapp onboarding states"
2025-10-11 00:44:49 +03:00
Egor 48e1485cac Revert "feat: improve miniapp onboarding states" 2025-10-11 00:44:41 +03:00
Egor f548f186de Merge pull request #1150 from Fr1ngg/k49afg-bedolaga/update-miniapp/index.html-logic
feat: improve miniapp onboarding states
2025-10-11 00:42:36 +03:00
Egor 97ae7b6b1b feat: improve miniapp onboarding states 2025-10-11 00:42:21 +03:00
Egor ac06b039a9 Merge pull request #1149 from Fr1ngg/9kilqp-bedolaga/fix-layout-for-subscription-button
Stop clipping top-up button in subscription purchase card
2025-10-11 00:24:16 +03:00
Egor 8514f8604a Ensure top-up button stays visible in subscription card 2025-10-11 00:23:59 +03:00
Egor a22b04a91e Merge pull request #1148 from Fr1ngg/revert-1147-hehyjo-bedolaga/fix-layout-for-subscription-button
Revert "Ensure subscription top-up button remains visible"
2025-10-11 00:23:47 +03:00
Egor 11f5289f2d Revert "Ensure subscription top-up button remains visible" 2025-10-11 00:23:29 +03:00
Egor c72886d45a Merge pull request #1147 from Fr1ngg/hehyjo-bedolaga/fix-layout-for-subscription-button
Ensure subscription top-up button remains visible
2025-10-11 00:20:21 +03:00
Egor fbfd59ca83 Prevent purchase top-up button clipping 2025-10-11 00:20:03 +03:00
Egor 1737791659 Merge pull request #1146 from Fr1ngg/revert-1143-t4unz2-bedolaga/fix-layout-for-subscription-button
Revert "Refine subscription period option layout"
2025-10-11 00:19:04 +03:00
Egor 3a8112b4ec Revert "Refine subscription period option layout" 2025-10-11 00:18:56 +03:00
Egor cfdb7b6bdd Merge pull request #1145 from Fr1ngg/revert-1144-q2di6m-bedolaga/fix-button-cropping-in-subscription-section
Revert "Fix subscription top-up button clipping"
2025-10-11 00:18:49 +03:00
Egor 223a112d88 Revert "Fix subscription top-up button clipping" 2025-10-11 00:18:42 +03:00
Egor b72ad3e090 Merge pull request #1144 from Fr1ngg/q2di6m-bedolaga/fix-button-cropping-in-subscription-section
Fix subscription top-up button clipping
2025-10-11 00:16:22 +03:00
Egor e81b555e43 Fix subscription top-up button clipping 2025-10-11 00:16:03 +03:00
Egor 8214775ded Merge pull request #1143 from Fr1ngg/t4unz2-bedolaga/fix-layout-for-subscription-button
Refine subscription period option layout
2025-10-11 00:03:04 +03:00
Egor 3bb5727089 Improve subscription period card layout 2025-10-11 00:02:35 +03:00
Egor 39677eb82e Merge pull request #1141 from Fr1ngg/revert-1140-7tajud-bedolaga/fix-layout-for-subscription-button
Revert "Revamp subscription purchase traffic and device UI"
2025-10-11 00:01:58 +03:00
Egor c56cdf2383 Revert "Revamp subscription purchase traffic and device UI" 2025-10-11 00:01:48 +03:00
Egor a40c4dd7e9 Merge pull request #1140 from Fr1ngg/7tajud-bedolaga/fix-layout-for-subscription-button
Revamp subscription purchase traffic and device UI
2025-10-10 23:48:55 +03:00
Egor a09fd825bf Refine purchase selections layout 2025-10-10 23:48:39 +03:00
Egor 03cf74656e Merge pull request #1139 from Fr1ngg/revert-1137-8hdvnv-bedolaga/fix-layout-for-subscription-button
Revert "Redesign subscription purchase miniapp layout"
2025-10-10 23:48:20 +03:00
Egor 98a301c1b5 Revert "Redesign subscription purchase miniapp layout" 2025-10-10 23:48:12 +03:00
Egor 1c865e3ae8 Merge pull request #1137 from Fr1ngg/8hdvnv-bedolaga/fix-layout-for-subscription-button
Redesign subscription purchase miniapp layout
2025-10-10 23:39:56 +03:00
Egor 01388442b7 Merge pull request #1138 from Fr1ngg/revert-1134-hccafi-bedolaga/fix-layout-for-subscription-button
Revert "Improve subscription purchase CTA spacing on narrow viewports"
2025-10-10 23:39:43 +03:00
Egor 251a940be2 Revamp subscription purchase flow layout 2025-10-10 23:39:36 +03:00
Egor fc725ba9e1 Revert "Improve subscription purchase CTA spacing on narrow viewports" 2025-10-10 23:39:36 +03:00
Egor 9f7e73ae2e Merge pull request #1136 from Fr1ngg/revert-1135-2rmc9c-bedolaga/improve-subscription-purchase-block-design
Revert "Improve subscription purchase UI styling"
2025-10-10 23:38:51 +03:00
Egor d576801676 Revert "Improve subscription purchase UI styling" 2025-10-10 23:38:43 +03:00
Egor 8b82d56635 Merge pull request #1135 from Fr1ngg/2rmc9c-bedolaga/improve-subscription-purchase-block-design
Improve subscription purchase UI styling
2025-10-10 23:37:04 +03:00
Egor b165fafc4d Refine subscription purchase UI 2025-10-10 23:36:47 +03:00
Egor bb9a0b305c Merge pull request #1134 from Fr1ngg/hccafi-bedolaga/fix-layout-for-subscription-button
Improve subscription purchase CTA spacing on narrow viewports
2025-10-10 21:56:05 +03:00
Egor 99b219a307 Improve narrow layout for purchase card 2025-10-10 21:55:49 +03:00
Egor ad2a9b9d14 Merge pull request #1133 from Fr1ngg/revert-1132-mxndh2-bedolaga/fix-layout-for-subscription-button
Revert "Adjust subscription purchase card spacing"
2025-10-10 21:55:27 +03:00
Egor e03bf2a393 Revert "Adjust subscription purchase card spacing" 2025-10-10 21:55:19 +03:00
Egor b6ead34e6c Merge pull request #1132 from Fr1ngg/mxndh2-bedolaga/fix-layout-for-subscription-button
Adjust subscription purchase card spacing
2025-10-10 21:48:42 +03:00
Egor cb180d9c0e Adjust subscription purchase card spacing 2025-10-10 21:48:27 +03:00
Egor 791e26f56a Merge pull request #1131 from Fr1ngg/revert-1130-9a3g3t-bedolaga/fix-layout-for-subscription-button
Revert "Fix spacing around subscription purchase actions"
2025-10-10 21:47:46 +03:00
Egor b0403dab83 Revert "Fix spacing around subscription purchase actions" 2025-10-10 21:47:38 +03:00
Egor e6c3d45991 Merge pull request #1130 from Fr1ngg/9a3g3t-bedolaga/fix-layout-for-subscription-button
Fix spacing around subscription purchase actions
2025-10-10 21:45:13 +03:00
Egor d43597ab2a Improve subscription purchase spacing 2025-10-10 21:44:58 +03:00
Egor b2bf5024a0 Merge pull request #1129 from Fr1ngg/dd0l4x-bedolaga/add-subscription-renewal-section
Ensure subscription purchase actions remain visible
2025-10-10 21:36:59 +03:00
Egor 001f35ed89 Adjust subscription purchase card spacing 2025-10-10 21:36:44 +03:00
Egor 9def85e8e2 Merge pull request #1128 from Fr1ngg/revert-1127-obl5mk-bedolaga/add-subscription-renewal-section
Revert "Improve subscription purchase CTA spacing"
2025-10-10 21:36:27 +03:00
Egor ed92e0cd8e Revert "Improve subscription purchase CTA spacing" 2025-10-10 21:36:19 +03:00
Egor 336cfe440a Merge pull request #1127 from Fr1ngg/obl5mk-bedolaga/add-subscription-renewal-section
Improve subscription purchase CTA spacing
2025-10-10 21:32:36 +03:00
Egor 3c312e089b Improve subscription purchase CTA spacing 2025-10-10 21:32:22 +03:00
Egor d54e42612e Merge pull request #1126 from Fr1ngg/revert-1125-vuqd4o-bedolaga/add-subscription-renewal-section
Revert "Adjust subscription purchase CTA spacing on mobile"
2025-10-10 21:31:50 +03:00
Egor 233520a656 Revert "Adjust subscription purchase CTA spacing on mobile" 2025-10-10 21:31:41 +03:00
Egor aeff150feb Merge pull request #1125 from Fr1ngg/vuqd4o-bedolaga/add-subscription-renewal-section
Adjust subscription purchase CTA spacing on mobile
2025-10-10 21:24:59 +03:00
Egor f0d67e2428 Increase purchase CTA spacing for mobile 2025-10-10 21:24:43 +03:00
Egor 72df1d91d5 Merge pull request #1124 from Fr1ngg/revert-1123-jub1oo-bedolaga/add-subscription-renewal-section
Revert "Handle zero-cost renewals and adjust purchase CTA spacing"
2025-10-10 21:24:22 +03:00
Egor 26811cdcff Revert "Handle zero-cost renewals and adjust purchase CTA spacing" 2025-10-10 21:24:14 +03:00
Egor bc26d8352b Merge pull request #1123 from Fr1ngg/jub1oo-bedolaga/add-subscription-renewal-section
Handle zero-cost renewals and adjust purchase CTA spacing
2025-10-10 21:23:03 +03:00
Egor 4fe2e029bc Tighten subscription purchase CTA spacing 2025-10-10 21:22:48 +03:00
Egor f54bf16ef4 Merge pull request #1122 from Fr1ngg/revert-1121-gy8gds-bedolaga/add-subscription-renewal-section
Revert "Adjust purchase CTA spacing on mobile"
2025-10-10 21:22:14 +03:00
Egor 7dfa5d5a6d Revert "Adjust purchase CTA spacing on mobile" 2025-10-10 21:22:05 +03:00
Egor ac1f6120ab Merge pull request #1121 from Fr1ngg/gy8gds-bedolaga/add-subscription-renewal-section
Adjust purchase CTA spacing on mobile
2025-10-10 21:18:45 +03:00
Egor 26c9896b51 Adjust purchase CTA spacing on mobile 2025-10-10 21:18:22 +03:00
Egor 3b7323c6f4 Merge pull request #1120 from Fr1ngg/revert-1119-byeh3b-bedolaga/add-subscription-renewal-section
Revert "Handle zero-cost renewals and adjust purchase CTA spacing"
2025-10-10 21:18:06 +03:00
Egor 956936185a Revert "Handle zero-cost renewals and adjust purchase CTA spacing" 2025-10-10 21:17:57 +03:00
Egor 435c2d98f1 Merge pull request #1119 from Fr1ngg/byeh3b-bedolaga/add-subscription-renewal-section
Handle zero-cost renewals and adjust purchase CTA spacing
2025-10-10 21:06:22 +03:00
Egor 7f24bd7c8c Tighten renewal promo consumption and polish purchase CTA 2025-10-10 21:06:08 +03:00
Egor b15781ad3b Merge pull request #1118 from Fr1ngg/revert-1117-revert-1114-revert-1113-52037z-bedolaga/add-subscription-renewal-section
Revert "Revert "Revert "Handle zero-cost renewals and polish purchase button layout"""
2025-10-10 21:05:31 +03:00
Egor c598d66ecc Revert "Revert "Revert "Handle zero-cost renewals and polish purchase button layout""" 2025-10-10 21:05:23 +03:00
Egor ffc969daaa Merge pull request #1117 from Fr1ngg/revert-1114-revert-1113-52037z-bedolaga/add-subscription-renewal-section
Revert "Revert "Handle zero-cost renewals and polish purchase button layout""
2025-10-10 21:05:00 +03:00
Egor 116201cca3 Revert "Revert "Handle zero-cost renewals and polish purchase button layout"" 2025-10-10 21:04:52 +03:00
Egor a7592836da Merge pull request #1116 from Fr1ngg/revert-1115-hhoosa-bedolaga/add-subscription-renewal-section
Revert "Ensure free renewal promos are consumed and tweak mobile CTA"
2025-10-10 21:04:44 +03:00
Egor 734c3679ce Revert "Ensure free renewal promos are consumed and tweak mobile CTA" 2025-10-10 21:04:35 +03:00
Egor d350b627fc Merge pull request #1115 from Fr1ngg/hhoosa-bedolaga/add-subscription-renewal-section
Ensure free renewal promos are consumed and tweak mobile CTA
2025-10-10 21:02:55 +03:00
Egor 4fe483da56 Fix free renewal promo consumption and mobile submit layout 2025-10-10 21:02:29 +03:00
Egor 8b4e1c830b Merge pull request #1114 from Fr1ngg/revert-1113-52037z-bedolaga/add-subscription-renewal-section
Revert "Handle zero-cost renewals and polish purchase button layout"
2025-10-10 21:02:13 +03:00
Egor ebfb653d8b Revert "Handle zero-cost renewals and polish purchase button layout" 2025-10-10 21:02:05 +03:00
Egor 8b4f1672dd Merge pull request #1113 from Fr1ngg/52037z-bedolaga/add-subscription-renewal-section
Handle zero-cost renewals and polish purchase button layout
2025-10-10 20:58:47 +03:00
Egor e48a3f178e Fix promo offer consumption and mobile purchase button 2025-10-10 20:56:23 +03:00
Egor a72ed1c2a1 Merge pull request #1112 from Fr1ngg/revert-1111-wti6kr-bedolaga/add-subscription-renewal-section
Revert "Polish subscription purchase configurator"
2025-10-10 20:55:26 +03:00
Egor 15db2d975f Revert "Polish subscription purchase configurator" 2025-10-10 20:55:17 +03:00
Egor 99ff5f8a9d Merge pull request #1111 from Fr1ngg/wti6kr-bedolaga/add-subscription-renewal-section
Polish subscription purchase configurator
2025-10-10 20:07:44 +03:00
Egor 742093e68c Polish subscription purchase configurator 2025-10-10 20:07:27 +03:00
Egor a72f6caf80 Merge pull request #1110 from Fr1ngg/revert-1109-1q0t4b-bedolaga/add-subscription-renewal-section
Revert "Refine subscription purchase configurator UX"
2025-10-10 20:07:12 +03:00
Egor 605d33053d Revert "Refine subscription purchase configurator UX" 2025-10-10 20:07:03 +03:00
Egor 7a2e83f3d1 Merge pull request #1109 from Fr1ngg/1q0t4b-bedolaga/add-subscription-renewal-section
Refine subscription purchase configurator UX
2025-10-10 20:04:57 +03:00
Egor e7fc1b8402 Polish subscription purchase flow 2025-10-10 20:04:42 +03:00
Egor 5fcdcc5878 Merge pull request #1108 from Fr1ngg/revert-1107-ppvgbo-bedolaga/add-subscription-renewal-section
Revert "Refine subscription purchase presentation"
2025-10-10 20:04:28 +03:00
Egor 4212fc0be3 Revert "Refine subscription purchase presentation" 2025-10-10 20:04:19 +03:00
Egor df4ce40739 Merge pull request #1107 from Fr1ngg/ppvgbo-bedolaga/add-subscription-renewal-section
Refine subscription purchase presentation
2025-10-10 20:02:18 +03:00
Egor b761630505 Improve subscription purchase layout and summary 2025-10-10 20:02:02 +03:00
Egor d1c3358dbf Merge pull request #1106 from Fr1ngg/revert-1105-63c4w3-bedolaga/add-subscription-renewal-section
Revert "Improve subscription purchase layout and discount summary"
2025-10-10 20:01:33 +03:00
Egor 1d1fc7dbfb Revert "Improve subscription purchase layout and discount summary" 2025-10-10 20:01:23 +03:00
Egor 25aaa703a6 Merge pull request #1105 from Fr1ngg/63c4w3-bedolaga/add-subscription-renewal-section
Improve subscription purchase layout and discount summary
2025-10-10 19:59:23 +03:00
Egor 46f3ef23ed Refine subscription purchase configurator 2025-10-10 19:59:08 +03:00
Egor 80517f0c53 Merge pull request #1104 from Fr1ngg/revert-1103-u6qc6m-bedolaga/add-subscription-renewal-section
Revert "Update renewal balance warning per selected period"
2025-10-10 19:58:40 +03:00
Egor 66d1f96e32 Revert "Update renewal balance warning per selected period" 2025-10-10 19:58:31 +03:00
Egor a93c5808c0 Merge pull request #1103 from Fr1ngg/u6qc6m-bedolaga/add-subscription-renewal-section
Update renewal balance warning per selected period
2025-10-10 19:40:56 +03:00
Egor f80818c18a Fix renewal balance warning for period switches 2025-10-10 19:40:38 +03:00
Egor 6199716c09 Merge pull request #1102 from Fr1ngg/revert-1101-revert-1097-revert-1096-h2ud8b-bedolaga/add-subscription-renewal-section
Revert "Revert "Revert "Prevent renewal options from showing phantom discounts"""
2025-10-10 19:39:46 +03:00
Egor ab131ed399 Revert "Revert "Revert "Prevent renewal options from showing phantom discounts""" 2025-10-10 19:39:34 +03:00
Egor 43203bfeb3 Merge pull request #1101 from Fr1ngg/revert-1097-revert-1096-h2ud8b-bedolaga/add-subscription-renewal-section
Revert "Revert "Prevent renewal options from showing phantom discounts""
2025-10-10 12:47:12 +03:00
Egor 4873a26880 Revert "Revert "Prevent renewal options from showing phantom discounts"" 2025-10-10 12:47:03 +03:00
Egor 974b049ee0 Merge pull request #1100 from Fr1ngg/revert-1099-mtpzes-bedolaga/add-subscription-renewal-section
Revert "Prevent renewal prices from showing strike-through without discounts"
2025-10-10 12:46:45 +03:00
Egor 3656ec5df5 Revert "Prevent renewal prices from showing strike-through without discounts" 2025-10-10 12:46:37 +03:00
Egor ef4d343e16 Merge pull request #1099 from Fr1ngg/mtpzes-bedolaga/add-subscription-renewal-section
Prevent renewal prices from showing strike-through without discounts
2025-10-10 12:43:07 +03:00
Egor 71171b24b3 Fix renewal option original price display when no discount 2025-10-10 12:42:46 +03:00
Egor 5ce9fa9fac Merge pull request #1098 from Fr1ngg/revert-1095-e369z3-bedolaga/add-subscription-renewal-section
Revert "Fix renewal original price display without discounts"
2025-10-10 12:42:19 +03:00
Egor 85a702b00a Revert "Fix renewal original price display without discounts" 2025-10-10 12:42:10 +03:00
Egor aab8727de7 Merge pull request #1095 from Fr1ngg/e369z3-bedolaga/add-subscription-renewal-section
Fix renewal original price display without discounts
2025-10-10 12:41:02 +03:00
Egor 813bb45cf9 Merge pull request #1097 from Fr1ngg/revert-1096-h2ud8b-bedolaga/add-subscription-renewal-section
Revert "Prevent renewal options from showing phantom discounts"
2025-10-10 12:40:50 +03:00
Egor 3d731e6b06 Revert "Prevent renewal options from showing phantom discounts" 2025-10-10 12:40:41 +03:00
Egor 8e6174ea13 Merge pull request #1096 from Fr1ngg/h2ud8b-bedolaga/add-subscription-renewal-section
Prevent renewal options from showing phantom discounts
2025-10-10 12:37:53 +03:00
Egor 7a5a1fa8b4 Normalize renewal price comparison when no discount 2025-10-10 12:37:28 +03:00
Egor 1a4c8eff19 Fix renewal price display when no discount 2025-10-10 12:36:39 +03:00
Egor b345a4ce25 Merge pull request #1094 from Fr1ngg/revert-1093-ifww7g-bedolaga/add-subscription-renewal-section
Revert "Fix renewal mini app spacing and discount presentation"
2025-10-10 12:36:04 +03:00
Egor d8c37349ca Revert "Fix renewal mini app spacing and discount presentation" 2025-10-10 12:35:53 +03:00
Egor 4df3b2ea8f Merge pull request #1093 from Fr1ngg/ifww7g-bedolaga/add-subscription-renewal-section
Fix renewal mini app spacing and discount presentation
2025-10-10 12:28:19 +03:00
Egor 8bc683e03a Fix renewal discount display in mini app 2025-10-10 12:27:59 +03:00
Egor c176a9abf2 Merge pull request #1092 from Fr1ngg/revert-1091-revert-1088-revert-1087-bfz03a-bedolaga/add-subscription-renewal-section
Revert "Revert "Revert "Add renewal API for mini app subscription extensions"""
2025-10-10 12:27:32 +03:00
Egor 72553e5f8b Revert "Revert "Revert "Add renewal API for mini app subscription extensions""" 2025-10-10 12:27:22 +03:00
Egor de09965876 Merge pull request #1091 from Fr1ngg/revert-1088-revert-1087-bfz03a-bedolaga/add-subscription-renewal-section
Revert "Revert "Add renewal API for mini app subscription extensions""
2025-10-10 11:58:39 +03:00
Egor 7eb14ad036 Revert "Revert "Add renewal API for mini app subscription extensions"" 2025-10-10 11:58:29 +03:00
Egor 0e0c917fdd Merge pull request #1090 from Fr1ngg/revert-1089-64afzg-bedolaga/fix-subscription-renewal-section-in-miniapp
Revert "Implement mini app subscription renewal flow"
2025-10-10 11:58:25 +03:00
Egor 7eb58d1add Revert "Implement mini app subscription renewal flow" 2025-10-10 11:58:05 +03:00
Egor 405cba645a Merge pull request #1089 from Fr1ngg/64afzg-bedolaga/fix-subscription-renewal-section-in-miniapp
Implement mini app subscription renewal flow
2025-10-10 11:57:18 +03:00
Egor 359b7c807c Implement mini app subscription renewal API 2025-10-10 11:56:51 +03:00
Egor dbed5d9e5d Merge pull request #1088 from Fr1ngg/revert-1087-bfz03a-bedolaga/add-subscription-renewal-section
Revert "Add renewal API for mini app subscription extensions"
2025-10-10 11:56:39 +03:00
Egor f999a31664 Revert "Add renewal API for mini app subscription extensions" 2025-10-10 11:56:29 +03:00
Egor 96c795c789 Merge pull request #1087 from Fr1ngg/bfz03a-bedolaga/add-subscription-renewal-section
Add renewal API for mini app subscription extensions
2025-10-10 11:52:08 +03:00
Egor 874c5d8017 Add renewal API for mini app subscription extensions 2025-10-10 11:51:48 +03:00
Egor 2c76751c63 Merge pull request #1086 from Fr1ngg/revert-1083-8qdgqk-bedolaga/add-subscription-renewal-section
Revert "Add subscription renewal UI to mini app"
2025-10-10 11:51:19 +03:00
Egor 483b99e557 Revert "Add subscription renewal UI to mini app" 2025-10-10 11:51:08 +03:00
Egor b653c45320 Merge pull request #1085 from Fr1ngg/revert-1084-nqyfy2-bedolaga/fix-subscription-renewal-section-in-miniapp
Revert "Implement miniapp subscription renewal API"
2025-10-10 11:51:02 +03:00
Egor 3f0f9c3f4b Revert "Implement miniapp subscription renewal API" 2025-10-10 11:50:52 +03:00
Egor d899dc7d86 Merge pull request #1084 from Fr1ngg/nqyfy2-bedolaga/fix-subscription-renewal-section-in-miniapp
Implement miniapp subscription renewal API
2025-10-10 11:49:03 +03:00
Egor 21567088a1 Add miniapp subscription renewal endpoints 2025-10-10 11:48:47 +03:00
Egor 5425210cb0 Merge pull request #1083 from Fr1ngg/8qdgqk-bedolaga/add-subscription-renewal-section
Add subscription renewal UI to mini app
2025-10-10 11:27:28 +03:00
Egor 0484d69538 Guard renewal summary elements when data is missing 2025-10-10 11:27:10 +03:00
Egor ac5c764daa Merge pull request #1082 from Fr1ngg/revert-1081-uiwlsk-bedolaga/add-subscription-renewal-section
Revert "Add subscription renewal section to miniapp"
2025-10-10 11:26:57 +03:00
Egor 15df6b802c Revert "Add subscription renewal section to miniapp" 2025-10-10 11:26:48 +03:00
Egor a1e1267239 Merge pull request #1081 from Fr1ngg/uiwlsk-bedolaga/add-subscription-renewal-section
Add subscription renewal section to miniapp
2025-10-10 11:20:15 +03:00
Egor 5fcaa73d20 Add subscription renewal section to miniapp 2025-10-10 11:19:58 +03:00
Egor c57d273c7e Merge pull request #1080 from Fr1ngg/zros0m-bedolaga/-api
Update miniapp purchase UI to reflect promo pricing and hide inactive servers
2025-10-10 10:52:31 +03:00
Egor 51a1db0680 Hide inactive miniapp servers during purchase 2025-10-10 10:52:11 +03:00
Egor a3d46cd293 Merge pull request #1079 from Fr1ngg/revert-1078-19ztpa-bedolaga/-api
Revert "Update miniapp purchase UI to reflect promo pricing"
2025-10-10 10:50:29 +03:00
Egor 18ec1c1f5f Revert "Update miniapp purchase UI to reflect promo pricing" 2025-10-10 10:50:20 +03:00
Egor a9d4bea9de Merge pull request #1078 from Fr1ngg/19ztpa-bedolaga/-api
Update miniapp purchase UI to reflect promo pricing
2025-10-10 10:26:18 +03:00
Egor 10b05f95ab Enhance miniapp purchase UI for new pricing data 2025-10-10 10:26:00 +03:00
Egor b5be8e5237 Merge pull request #1077 from Fr1ngg/revert-1076-9jagyo-bedolaga/expand-api-for-subscription-purchases
Revert "Add subscription purchase flow for the mini app"
2025-10-10 10:25:04 +03:00
Egor 4117dcb97f Revert "Add subscription purchase flow for the mini app" 2025-10-10 10:24:55 +03:00
Egor 0cb422d33e Merge pull request #1076 from Fr1ngg/9jagyo-bedolaga/expand-api-for-subscription-purchases
Add subscription purchase flow for the mini app
2025-10-10 10:23:20 +03:00
Egor da1ed55fd7 Add subscription purchase flow for mini app 2025-10-10 10:22:50 +03:00
Egor bb17f1c456 Merge pull request #1075 from Fr1ngg/revert-1074-vty20t-bedolaga/expand-api-for-subscription-purchase
Revert "Harden miniapp purchase period resolution"
2025-10-10 09:59:31 +03:00
Egor b2f88e75d5 Revert "Harden miniapp purchase period resolution" 2025-10-10 09:59:22 +03:00
Egor 767e7b653e Merge pull request #1074 from Fr1ngg/vty20t-bedolaga/expand-api-for-subscription-purchase
Harden miniapp purchase period resolution
2025-10-10 09:57:56 +03:00
Egor 2a7969716a Harden miniapp purchase period resolution 2025-10-10 09:57:37 +03:00
Egor 3e10bceb43 Merge pull request #1073 from Fr1ngg/revert-1072-026xmf-bedolaga/expand-api-for-subscription-purchase
Revert "Handle missing subscription period metadata in preview"
2025-10-10 09:57:14 +03:00
Egor 6f293615bb Revert "Handle missing subscription period metadata in preview" 2025-10-10 09:57:06 +03:00
Egor 69ce111281 Merge pull request #1072 from Fr1ngg/026xmf-bedolaga/expand-api-for-subscription-purchase
Handle missing subscription period metadata in preview
2025-10-10 09:55:38 +03:00
Egor 85c8ad018a Handle missing period selections in preview 2025-10-10 09:55:20 +03:00
Egor 385ebd1d39 Merge pull request #1070 from Fr1ngg/revert-1069-h3hzso-bedolaga/expand-api-for-subscription-purchase
Revert "Improve subscription purchase period handling"
2025-10-10 09:54:23 +03:00
Egor 4227a8613e Revert "Improve subscription purchase period handling" 2025-10-10 09:54:15 +03:00
Egor 2dc8947b00 Merge pull request #1069 from Fr1ngg/h3hzso-bedolaga/expand-api-for-subscription-purchase
Improve subscription purchase period handling
2025-10-10 09:53:13 +03:00
Egor 851f4dc58c Handle missing period selections in miniapp preview 2025-10-10 09:52:50 +03:00
Egor 366fc28ec5 Merge pull request #1068 from Fr1ngg/revert-1067-qor8f9-bedolaga/expand-api-for-subscription-purchase
Revert "Fix subscription purchase selection handling"
2025-10-10 09:52:22 +03:00
Egor 277d690686 Revert "Fix subscription purchase selection handling" 2025-10-10 09:52:13 +03:00
Egor 077584fb57 Merge pull request #1067 from Fr1ngg/qor8f9-bedolaga/expand-api-for-subscription-purchase
Fix subscription purchase selection handling
2025-10-10 09:40:17 +03:00
Egor 1bd05de773 Fix subscription purchase selection handling 2025-10-10 09:39:45 +03:00
Egor 1ae84a73dc Merge pull request #1066 from Fr1ngg/revert-1065-a0y28m-bedolaga/expand-api-for-subscription-purchase
Revert "Improve miniapp subscription configurator responsiveness"
2025-10-10 09:39:19 +03:00
Egor f5dad62ab0 Revert "Improve miniapp subscription configurator responsiveness" 2025-10-10 09:39:10 +03:00
Egor 2d90b9783a Merge pull request #1065 from Fr1ngg/a0y28m-bedolaga/expand-api-for-subscription-purchase
Improve miniapp subscription configurator responsiveness
2025-10-10 09:37:21 +03:00
Egor dd174bf6a5 Fix miniapp subscription configurator interactions 2025-10-10 09:37:04 +03:00
Egor fcd20b3aaa Merge pull request #1064 from Fr1ngg/revert-1063-ny2u3l-bedolaga/expand-api-for-subscription-purchase
Revert "Improve miniapp subscription purchase configurator"
2025-10-10 09:36:41 +03:00
Egor d97a71d96a Revert "Improve miniapp subscription purchase configurator" 2025-10-10 09:36:32 +03:00
Egor e7b631a991 Merge pull request #1063 from Fr1ngg/ny2u3l-bedolaga/expand-api-for-subscription-purchase
Improve miniapp subscription purchase configurator
2025-10-10 09:35:12 +03:00
Egor 948e36478b Improve miniapp subscription purchase configurator 2025-10-10 09:34:56 +03:00
Egor ec52741cf8 Merge pull request #1062 from Fr1ngg/revert-1061-revert-1056-revert-1053-9vdvf5-bedolaga/expand-api-for-subscription-purchase
Revert "Revert "Revert "Add mini app subscription purchase endpoints and schemas"""
2025-10-10 09:34:38 +03:00
Egor 08c3a54e6d Revert "Revert "Revert "Add mini app subscription purchase endpoints and schemas""" 2025-10-10 09:34:27 +03:00
Egor 75af19ab83 Merge pull request #1061 from Fr1ngg/revert-1056-revert-1053-9vdvf5-bedolaga/expand-api-for-subscription-purchase
Revert "Revert "Add mini app subscription purchase endpoints and schemas""
2025-10-10 09:13:05 +03:00
Egor 13c89c1d6f Revert "Revert "Add mini app subscription purchase endpoints and schemas"" 2025-10-10 09:12:57 +03:00
Egor c091cb95c8 Merge pull request #1060 from Fr1ngg/revert-1059-0idrid-bedolaga/expand-api-for-subscription-purchase
Revert "Add mini-app subscription purchase endpoints and UI handling"
2025-10-10 09:12:12 +03:00
Egor 65aa0a7c08 Revert "Add mini-app subscription purchase endpoints and UI handling" 2025-10-10 09:12:03 +03:00
Egor 080c019684 Merge pull request #1059 from Fr1ngg/0idrid-bedolaga/expand-api-for-subscription-purchase
Add mini-app subscription purchase endpoints and UI handling
2025-10-10 09:11:03 +03:00
Egor 0e1962c291 Add mini-app subscription purchase endpoints and UI handling 2025-10-10 09:10:44 +03:00
Egor e0aea226a4 Merge pull request #1058 from Fr1ngg/revert-1057-80ize0-bedolaga/extend-api-for-subscription-purchasing
Revert "Add miniapp subscription purchase flow"
2025-10-10 09:09:51 +03:00
Egor 687ea7d1d6 Revert "Add miniapp subscription purchase flow" 2025-10-10 09:09:41 +03:00
Egor b7146268c2 Merge pull request #1057 from Fr1ngg/80ize0-bedolaga/extend-api-for-subscription-purchasing
Add miniapp subscription purchase flow
2025-10-10 09:08:37 +03:00
Egor 0952059c60 Add miniapp subscription purchase flow 2025-10-10 09:08:18 +03:00
Egor bbf5575b49 Merge pull request #1056 from Fr1ngg/revert-1053-9vdvf5-bedolaga/expand-api-for-subscription-purchase
Revert "Add mini app subscription purchase endpoints and schemas"
2025-10-10 09:07:29 +03:00
Egor 6b8e913f91 Revert "Add mini app subscription purchase endpoints and schemas" 2025-10-10 09:07:18 +03:00
Egor 51e5f1c715 Merge pull request #1053 from Fr1ngg/9vdvf5-bedolaga/expand-api-for-subscription-purchase
Add mini app subscription purchase endpoints and schemas
2025-10-10 09:04:08 +03:00
Egor 910f48bff0 Merge pull request #1055 from Fr1ngg/revert-1054-fze3ek-bedolaga/expand-api-for-subscription-purchase
Revert "Add mini app subscription purchase flow"
2025-10-10 09:03:27 +03:00
Egor 62325d7a21 Revert "Add mini app subscription purchase flow" 2025-10-10 09:03:19 +03:00
Egor dc07de9957 Merge pull request #1054 from Fr1ngg/fze3ek-bedolaga/expand-api-for-subscription-purchase
Add mini app subscription purchase flow
2025-10-10 09:02:02 +03:00
Egor d65524d349 Add mini app subscription purchase flow 2025-10-10 09:01:37 +03:00
Egor fd989d0186 Add subscription purchase API for mini app 2025-10-10 09:00:27 +03:00
Egor 390874565e Merge pull request #1052 from Fr1ngg/oz7h91-bedolaga/add-subscription-purchase-configurator-to-miniapp
Implement subscription purchase configurator
2025-10-10 08:21:56 +03:00
Egor ae64dc48d2 Implement subscription purchase configurator 2025-10-10 08:21:31 +03:00
Egor 8a68a0d4b1 Merge pull request #1051 from Fr1ngg/revert-1050-hpiari-bedolaga/add-subscription-purchase-configurator-to-miniapp
Revert "Add mini-app subscription purchase configurator"
2025-10-10 08:21:17 +03:00
Egor 82cdd4442e Revert "Add mini-app subscription purchase configurator" 2025-10-10 08:21:09 +03:00
Egor 112ce528d9 Merge pull request #1050 from Fr1ngg/hpiari-bedolaga/add-subscription-purchase-configurator-to-miniapp
Add mini-app subscription purchase configurator
2025-10-10 08:13:32 +03:00
Egor 950f84d012 Add subscription purchase configurator logic 2025-10-10 08:13:10 +03:00
Egor 025a57a325 Merge pull request #1049 from Fr1ngg/revert-1048-84983m-bedolaga/add-subscription-purchase-feature-to-miniapp
Revert "Add interactive purchase configurator flow to mini app"
2025-10-10 08:12:18 +03:00
Egor 9d374f3ba1 Revert "Add interactive purchase configurator flow to mini app" 2025-10-10 08:12:02 +03:00
Egor d4aec3b8aa Merge pull request #1048 from Fr1ngg/84983m-bedolaga/add-subscription-purchase-feature-to-miniapp
Add interactive purchase configurator flow to mini app
2025-10-10 08:10:59 +03:00
Egor ffe8f70946 Add purchase configurator actions for mini app 2025-10-10 08:10:43 +03:00
Egor febf9ec669 Merge pull request #1047 from Fr1ngg/revert-1046-7hfdjj-bedolaga/add-subscription-purchase-configurator-to-miniapp
Revert "Wire subscription purchase configurator actions"
2025-10-10 08:10:03 +03:00
Egor 6bff990d59 Revert "Wire subscription purchase configurator actions" 2025-10-10 08:09:53 +03:00
Egor d50afd94eb Merge pull request #1046 from Fr1ngg/7hfdjj-bedolaga/add-subscription-purchase-configurator-to-miniapp
Wire subscription purchase configurator actions
2025-10-10 08:06:23 +03:00
Egor d43bab9c88 Wire subscription purchase configurator actions 2025-10-10 08:06:02 +03:00
Egor b6b63cd82d Merge pull request #1045 from Fr1ngg/revert-1044-17u0o4-bedolaga/add-subscription-purchase-feature-to-miniapp
Revert "Add subscription configurator to mini app"
2025-10-10 08:05:25 +03:00
Egor d7534e098c Revert "Add subscription configurator to mini app" 2025-10-10 08:05:17 +03:00
Egor 1af55ec86e Merge pull request #1044 from Fr1ngg/17u0o4-bedolaga/add-subscription-purchase-feature-to-miniapp
Add subscription configurator to mini app
2025-10-10 08:02:30 +03:00
Egor 3b72707fd9 Safely render subscription configurator summary 2025-10-10 08:02:14 +03:00
Egor 55ee06402e Merge pull request #1043 from Fr1ngg/revert-1042-pt7wcl-bedolaga/add-subscription-purchase-feature-to-miniapp
Revert "feat: add subscription purchase configurator to miniapp"
2025-10-10 07:59:04 +03:00
Egor bd19b570e5 Revert "feat: add subscription purchase configurator to miniapp" 2025-10-10 07:58:53 +03:00
Egor d0735aad31 Merge pull request #1042 from Fr1ngg/pt7wcl-bedolaga/add-subscription-purchase-feature-to-miniapp
feat: add subscription purchase configurator to miniapp
2025-10-10 07:57:09 +03:00
Egor 73594b9088 feat: add subscription purchase configurator to miniapp 2025-10-10 07:56:55 +03:00
Egor 6075ed5410 Merge pull request #1041 from Fr1ngg/dev4
Редактирование платной подписки в миниапке
2025-10-10 07:46:30 +03:00
Egor 07ff14c6b1 Merge pull request #1040 from Fr1ngg/5ulvyc-bedolaga/add-subscription-settings-endpoints
Handle Telegram popup confirmations and validate traffic selections
2025-10-10 07:19:56 +03:00
Egor 36ac94468a Fix miniapp confirmations and guard traffic updates 2025-10-10 07:19:38 +03:00
Egor 3c3bd139e3 Merge pull request #1039 from Fr1ngg/revert-1038-bblv1f-bedolaga/add-subscription-settings-endpoints
Revert "Require active subscription and confirm add-on charges"
2025-10-10 07:19:04 +03:00
Egor 82b2127e55 Revert "Require active subscription and confirm add-on charges" 2025-10-10 07:18:41 +03:00
Egor 5f2bb415de Merge pull request #1038 from Fr1ngg/bblv1f-bedolaga/add-subscription-settings-endpoints
Require active subscription and confirm add-on charges
2025-10-10 07:03:46 +03:00
Egor c2a6dfb3b4 Improve subscription settings updates 2025-10-10 07:03:31 +03:00
Egor 04bcc53012 Merge pull request #1037 from Fr1ngg/revert-1036-kn2p4i-bedolaga/add-subscription-settings-endpoints
Revert "Require active subscription for miniapp changes and add confirmations"
2025-10-10 07:03:14 +03:00
Egor d9330bfb7a Revert "Require active subscription for miniapp changes and add confirmations" 2025-10-10 07:03:06 +03:00
Egor 938f96bef8 Merge pull request #1036 from Fr1ngg/kn2p4i-bedolaga/add-subscription-settings-endpoints
Require active subscription for miniapp changes and add confirmations
2025-10-10 06:59:25 +03:00
Egor 4ea0868f6a Ensure active subscription updates and add confirmations 2025-10-10 06:59:07 +03:00
Egor d032b9fcb1 Merge pull request #1035 from Fr1ngg/revert-1034-nlxsql-bedolaga/add-subscription-settings-endpoints
Revert "Require confirmation before paid miniapp subscription updates"
2025-10-10 06:58:53 +03:00
Egor a0efbb105e Revert "Require confirmation before paid miniapp subscription updates" 2025-10-10 06:58:45 +03:00
Egor 6cfd239217 Merge pull request #1034 from Fr1ngg/nlxsql-bedolaga/add-subscription-settings-endpoints
Require confirmation before paid miniapp subscription updates
2025-10-10 06:55:10 +03:00
Egor 8378a2c555 Require active subscription and confirm paid miniapp updates 2025-10-10 06:54:54 +03:00
Egor fd8ced4f1a Merge pull request #1033 from Fr1ngg/revert-1032-keclgm-bedolaga/add-subscription-settings-endpoints
Revert "Add miniapp subscription settings management endpoints"
2025-10-10 06:54:33 +03:00
Egor 261b5b8d14 Revert "Add miniapp subscription settings management endpoints" 2025-10-10 06:54:24 +03:00
Egor 0fb684544f Merge pull request #1032 from Fr1ngg/keclgm-bedolaga/add-subscription-settings-endpoints
Add miniapp subscription settings management endpoints
2025-10-10 06:32:30 +03:00
Egor 22f9c9a48d Add miniapp subscription settings management endpoints 2025-10-10 06:32:12 +03:00
Egor b6c3acfa13 Merge pull request #1031 from Fr1ngg/revert-1030-cy8m6c-bedolaga/add-subscription-settings-endpoints
Revert "Add miniapp subscription settings management endpoints"
2025-10-10 06:31:53 +03:00
Egor 8b94ac7e1d Revert "Add miniapp subscription settings management endpoints" 2025-10-10 06:31:45 +03:00
Egor a676e2818a Merge pull request #1030 from Fr1ngg/cy8m6c-bedolaga/add-subscription-settings-endpoints
Add miniapp subscription settings management endpoints
2025-10-10 06:30:54 +03:00
Egor b5c2ad4c07 Add miniapp subscription settings management endpoints 2025-10-10 06:30:38 +03:00
Egor d3100f6996 Merge pull request #1029 from Fr1ngg/revert-1028-7c9pa7-bedolaga/add-subscription-settings-endpoints
Revert "Add miniapp subscription settings management endpoints"
2025-10-10 06:29:31 +03:00
Egor 3a13c5225b Revert "Add miniapp subscription settings management endpoints" 2025-10-10 06:29:23 +03:00
Egor eab3626848 Merge pull request #1028 from Fr1ngg/7c9pa7-bedolaga/add-subscription-settings-endpoints
Add miniapp subscription settings management endpoints
2025-10-10 06:27:44 +03:00
Egor 7a34a9d370 Add miniapp subscription settings management endpoints 2025-10-10 06:27:27 +03:00
Egor 7045412fe6 Merge pull request #1027 from Fr1ngg/revert-1026-n4b27b-bedolaga/fix-subscription-settings-endpoints-issues
Revert "Add miniapp subscription settings API endpoints"
2025-10-10 06:12:57 +03:00
Egor 7027fa91c3 Revert "Add miniapp subscription settings API endpoints" 2025-10-10 06:12:48 +03:00
Egor 8e9f7f91ef Merge pull request #1026 from Fr1ngg/n4b27b-bedolaga/fix-subscription-settings-endpoints-issues
Add miniapp subscription settings API endpoints
2025-10-10 06:08:34 +03:00
Egor e6923a0d0a Add miniapp subscription settings API endpoints 2025-10-10 06:07:53 +03:00
Egor 3250976709 Merge pull request #1025 from Fr1ngg/revert-1024-h9chhq-bedolaga/fix-subscription-settings-on-page
Revert "Implement miniapp subscription settings endpoints"
2025-10-10 06:07:40 +03:00
Egor cb5b731e92 Revert "Implement miniapp subscription settings endpoints" 2025-10-10 06:07:32 +03:00
Egor 34a7c0dfc6 Merge pull request #1024 from Fr1ngg/h9chhq-bedolaga/fix-subscription-settings-on-page
Implement miniapp subscription settings endpoints
2025-10-10 06:06:33 +03:00
Egor 009b6bf71d Add miniapp subscription settings endpoints 2025-10-10 06:06:17 +03:00
Egor d01da00809 Merge pull request #1023 from Fr1ngg/da8kxk-bedolaga/add-subscription-settings-panel
Add subscription settings management to miniapp
2025-10-10 05:24:10 +03:00
Egor a76ca27f22 feat: add subscription subscription settings management 2025-10-10 05:23:53 +03:00
Egor 1a73b13d50 Merge pull request #1022 from Fr1ngg/dev4
Пополнение баланса на странице миниапки
2025-10-10 04:55:28 +03:00
Egor 073af762c5 Merge pull request #1021 from Fr1ngg/bedolaga-yhudb1
Normalize Stars invoices to matched amounts
2025-10-10 04:49:07 +03:00
Egor b5c8b4218c Preserve normalized Stars invoice amounts 2025-10-10 04:48:50 +03:00
Egor 00605a37ab Merge pull request #1020 from Fr1ngg/revert-1019-bedolaga-8f5lx7
Revert "Normalize Stars invoices to matched amounts"
2025-10-10 04:48:11 +03:00
Egor 0f61df80e2 Revert "Normalize Stars invoices to matched amounts" 2025-10-10 04:48:01 +03:00
Egor d1777b48ea Merge pull request #1019 from Fr1ngg/bedolaga-8f5lx7
Normalize Stars invoices to matched amounts
2025-10-10 04:31:39 +03:00
Egor fae4036670 Normalize Stars invoices to matched amounts 2025-10-10 04:31:21 +03:00
Egor 57a32ea52e Merge pull request #1018 from Fr1ngg/revert-1017-bedolaga-6dyjgo
Revert "Fix miniapp stars status reuse"
2025-10-10 04:30:13 +03:00
Egor 5412ad4202 Revert "Fix miniapp stars status reuse" 2025-10-10 04:30:05 +03:00
Egor ab9bc8b9e1 Merge pull request #1017 from Fr1ngg/bedolaga-6dyjgo
Fix miniapp stars status reuse
2025-10-10 04:06:36 +03:00
Egor 2c4090b63e Fix miniapp stars status reuse 2025-10-10 04:06:17 +03:00
Egor 61287d5dbc Merge pull request #1016 from Fr1ngg/revert-1015-bedolaga-22m19x
Revert "Prevent duplicate miniapp stars payment confirmations"
2025-10-10 04:05:57 +03:00
Egor 0cc37c1a9e Revert "Prevent duplicate miniapp stars payment confirmations" 2025-10-10 04:05:48 +03:00
Egor c877b327a5 Merge pull request #1015 from Fr1ngg/bedolaga-22m19x
Prevent duplicate miniapp stars payment confirmations
2025-10-10 04:04:01 +03:00
Egor a27c9a7463 Ensure stars payments use unique payloads 2025-10-10 04:03:44 +03:00
Egor 16c99bed06 Merge pull request #1014 from Fr1ngg/revert-1013-revert-1010-revert-1009-bedolaga-y96yta
Revert "Revert "Revert "Handle miniapp payment status refresh"""
2025-10-10 04:01:13 +03:00
Egor 045bc32798 Revert "Revert "Revert "Handle miniapp payment status refresh""" 2025-10-10 04:01:03 +03:00
Egor f1dbd7c784 Merge pull request #1013 from Fr1ngg/revert-1010-revert-1009-bedolaga-y96yta
Revert "Revert "Handle miniapp payment status refresh""
2025-10-10 03:46:22 +03:00
Egor cb9c673af1 Revert "Revert "Handle miniapp payment status refresh"" 2025-10-10 03:46:14 +03:00
Egor 53ed64afa3 Merge pull request #1012 from Fr1ngg/revert-1011-revert-1006-revert-1005-bedolaga-gg86kp
Revert "Revert "Revert "Align miniapp payment limits and status UX"""
2025-10-10 03:45:59 +03:00
Egor 23d313e12d Revert "Revert "Revert "Align miniapp payment limits and status UX""" 2025-10-10 03:45:50 +03:00
Egor 02313fc924 Merge pull request #1011 from Fr1ngg/revert-1006-revert-1005-bedolaga-gg86kp
Revert "Revert "Align miniapp payment limits and status UX""
2025-10-10 03:42:08 +03:00
Egor 860937591d Revert "Revert "Align miniapp payment limits and status UX"" 2025-10-10 03:41:59 +03:00
Egor 9388f1c81a Merge pull request #1010 from Fr1ngg/revert-1009-bedolaga-y96yta
Revert "Handle miniapp payment status refresh"
2025-10-10 03:41:34 +03:00
Egor d7c4f439b9 Revert "Handle miniapp payment status refresh" 2025-10-10 03:41:26 +03:00
Egor 4273d2354c Merge pull request #1009 from Fr1ngg/bedolaga-y96yta
Handle miniapp payment status refresh
2025-10-10 03:37:47 +03:00
Egor ee1c215278 Enhance miniapp payment flows 2025-10-10 03:37:16 +03:00
Egor da2260f55c Merge pull request #1008 from Fr1ngg/revert-1007-bedolaga-kj65b7
Revert "Improve miniapp payment polling for Stars"
2025-10-10 03:36:57 +03:00
Egor bdeff1dbad Revert "Improve miniapp payment polling for Stars" 2025-10-10 03:36:48 +03:00
Egor bd421f23da Merge pull request #1007 from Fr1ngg/bedolaga-kj65b7
Improve miniapp payment polling for Stars
2025-10-10 03:32:00 +03:00
Egor c63b439722 Improve miniapp payment polling for Stars 2025-10-10 03:31:39 +03:00
Egor b501e937de Merge pull request #1006 from Fr1ngg/revert-1005-bedolaga-gg86kp
Revert "Align miniapp payment limits and status UX"
2025-10-10 03:31:15 +03:00
Egor afa65e0fbd Revert "Align miniapp payment limits and status UX" 2025-10-10 03:31:06 +03:00
Egor 16945492e8 Merge pull request #1005 from Fr1ngg/bedolaga-gg86kp
Align miniapp payment limits and status UX
2025-10-10 03:28:15 +03:00
Egor 3c879914ca Align miniapp payment limits and status UI 2025-10-10 03:27:54 +03:00
Egor d4f8d9b94d Merge pull request #1004 from Fr1ngg/revert-1003-bedolaga-d7n0dr
Revert "Implement mini app balance top-up flow"
2025-10-10 02:56:31 +03:00
Egor f44e6aca6f Revert "Implement mini app balance top-up flow" 2025-10-10 02:56:21 +03:00
Egor af273c5762 Merge pull request #1003 from Fr1ngg/bedolaga-d7n0dr
Implement mini app balance top-up flow
2025-10-10 02:54:56 +03:00
Egor c24ba07751 Add balance top-up flow to mini app 2025-10-10 02:54:33 +03:00
Egor 1fb1dadd9d Merge pull request #1002 from Fr1ngg/revert-1001-bedolaga-qilzdq
Revert "Add miniapp balance top up flow"
2025-10-10 02:54:07 +03:00
Egor 83536d7f3d Revert "Add miniapp balance top up flow" 2025-10-10 02:53:56 +03:00
Egor c2be32783f Merge pull request #1001 from Fr1ngg/bedolaga-qilzdq
Add miniapp balance top up flow
2025-10-10 02:39:26 +03:00
Egor 6654608aba feat: add miniapp balance top up flow 2025-10-10 02:39:10 +03:00
Egor 6f29265913 Merge pull request #1000 from Fr1ngg/revert-999-bedolaga-r7wllt
Revert "feat: enable balance top up flow in miniapp"
2025-10-10 02:38:31 +03:00
Egor 95b42d6e9b Revert "feat: enable balance top up flow in miniapp" 2025-10-10 02:38:23 +03:00
Egor e76975388e Merge pull request #999 from Fr1ngg/bedolaga-r7wllt
feat: enable balance top up flow in miniapp
2025-10-10 02:36:16 +03:00
Egor 43ebb11793 feat: enable balance top up flow in miniapp 2025-10-10 02:35:46 +03:00
Egor e6d373d9bd Merge pull request #998 from Fr1ngg/ywf16j-bedolaga/add-squad-migration-feature-to-bot
Add RemnaWave squad migration API endpoints
2025-10-10 02:10:50 +03:00
Egor bea83a3635 Add RemnaWave squad migration API endpoints 2025-10-10 02:10:34 +03:00
Egor ba73236ca1 Merge pull request #997 from Fr1ngg/revert-996-9r5j29-bedolaga/add-squad-migration-feature-to-bot
Revert "Add Remnawave squad migration admin workflow"
2025-10-10 02:09:43 +03:00
Egor 4120d516b4 Revert "Add Remnawave squad migration admin workflow" 2025-10-10 02:09:35 +03:00
Egor 09a46fccad Merge pull request #996 from Fr1ngg/9r5j29-bedolaga/add-squad-migration-feature-to-bot
Add Remnawave squad migration admin workflow
2025-10-10 01:41:41 +03:00
Egor 7b27f39f74 feat: add remnawave squad migration flow 2025-10-10 01:41:20 +03:00
Egor e4b1754e8b Merge pull request #995 from Fr1ngg/dev4
Dev4
2025-10-10 00:47:48 +03:00
Egor 3114bf59de Update config.py 2025-10-10 00:46:57 +03:00
Egor 30b87e13c6 Merge pull request #994 from Fr1ngg/7oguuk-bedolaga/extend-ban-words-filter-in-nicknames
feat: make display name banned keywords configurable
2025-10-10 00:42:37 +03:00
Egor 45d2ca6e25 feat: make display name banned keywords configurable 2025-10-10 00:42:17 +03:00
Egor 42e3fae980 Merge pull request #993 from Fr1ngg/dev4
Dev4
2025-10-09 19:30:54 +03:00
Egor 07c4ab264c Merge pull request #992 from Fr1ngg/b4gp89-bedolaga/fix-rounding-error-for-star-conversion
Fix Telegram Stars rounding and amount formatting
2025-10-09 19:21:28 +03:00
Egor bdaa3b5875 Fix Telegram Stars rounding and formatting 2025-10-09 19:20:44 +03:00
Egor 42696d0564 Merge pull request #991 from Fr1ngg/u9tr9v-bedolaga/fix-balance-top-up-error
Fix handling of Telegram Stars payment callbacks without message text
2025-10-09 19:10:50 +03:00
Egor 3c0c5a3494 Fix topup amount handler for non-text messages 2025-10-09 19:10:33 +03:00
Egor 743c3dabee Merge pull request #990 from Fr1ngg/dev4
w
2025-10-09 19:02:00 +03:00
Egor 3b8708f8e6 Merge pull request #989 from Fr1ngg/v59gno-bedolaga/update-ticket-system-to-use-tg-id
Fix ticket reply notifications to use Telegram IDs
2025-10-09 18:57:08 +03:00
Egor 977042e66c Fix ticket reply notifications to use Telegram IDs 2025-10-09 18:56:43 +03:00
Egor a91f42e9f0 Merge pull request #988 from Fr1ngg/dev4
Dev4
2025-10-09 18:49:42 +03:00
Egor c4d5388081 Merge pull request #987 from Fr1ngg/5ygzlg-bedolaga/update-bot-middleware-to-restrict-users
Ensure display name middleware catches obfuscated keywords
2025-10-09 18:41:55 +03:00
Egor f73a0fcfe8 Tighten keyword detection in display name middleware 2025-10-09 18:41:35 +03:00
Egor 7ff6e42f5b Merge pull request #986 from Fr1ngg/main
w
2025-10-09 18:32:06 +03:00
46 changed files with 20758 additions and 675 deletions
+3
View File
@@ -280,6 +280,9 @@ PAL24_REQUEST_TIMEOUT=30
ENABLE_LOGO_MODE=true
LOGO_FILE=vpn_logo.png
# Режим главного меню (default - классический режим работы бота, text - режим работы с активным ЛК MiniApp, отключает покупку/управление подпиской в меню, заменяет все кнопками открытия в MiniApp ЛК)
MAIN_MENU_MODE=default
# Скрыть блок с ссылкой подключения в разделе с информацией о подписке
HIDE_SUBSCRIPTION_LINK=false
+3 -3
View File
@@ -36,15 +36,15 @@ jobs:
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🏷️ Собираем релизную версию: $VERSION"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
VERSION="v2.4.7-$(git rev-parse --short HEAD)"
VERSION="v2.5.0-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🚀 Собираем версию из main: $VERSION"
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
VERSION="v2.4.7-dev-$(git rev-parse --short HEAD)"
VERSION="v2.5.0-dev-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
echo "🧪 Собираем dev версию: $VERSION"
else
VERSION="v2.4.7-pr-$(git rev-parse --short HEAD)"
VERSION="v2.5.0-pr-$(git rev-parse --short HEAD)"
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)"
echo "🔀 Собираем PR версию: $VERSION"
fi
+3 -3
View File
@@ -49,13 +49,13 @@ jobs:
VERSION=${GITHUB_REF#refs/tags/}
echo "🏷️ Building release version: $VERSION"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
VERSION="v2.4.7-$(git rev-parse --short HEAD)"
VERSION="v2.5.0-$(git rev-parse --short HEAD)"
echo "🚀 Building main version: $VERSION"
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
VERSION="v2.4.7-dev-$(git rev-parse --short HEAD)"
VERSION="v2.5.0-dev-$(git rev-parse --short HEAD)"
echo "🧪 Building dev version: $VERSION"
else
VERSION="v2.4.7-pr-$(git rev-parse --short HEAD)"
VERSION="v2.5.0-pr-$(git rev-parse --short HEAD)"
echo "🔀 Building PR version: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
+1 -1
View File
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
FROM python:3.13-slim
ARG VERSION="v2.4.7"
ARG VERSION="v2.5.0"
ARG BUILD_DATE
ARG VCS_REF
+3
View File
@@ -829,6 +829,9 @@ PAL24_REQUEST_TIMEOUT=30
ENABLE_LOGO_MODE=true
LOGO_FILE=vpn_logo.png
# Режим главного меню (default - классический режим работы бота, text - режим работы с активным ЛК MiniApp, отключает покупку/управление подпиской в меню, заменяет все кнопками открытия в MiniApp ЛК)
MAIN_MENU_MODE=default
# Скрыть блок с ссылкой подключения в разделе с информацией о подписке
HIDE_SUBSCRIPTION_LINK=false
+486 -61
View File
@@ -1,5 +1,16 @@
{
"config": {},
"config": {
"additionalLocales": [
"ru",
"zh",
"fa"
],
"branding": {
"name": "Subscription",
"logoUrl": "https://raw.githubusercontent.com/Fr1ngg/remnawave-bedolaga-telegram-bot/bf0c1ce711a26fa2f24559e7e4443820e68d758b/assets/bedolaga_app3.svg",
"supportUrl": "https://t.me"
}
},
"platforms": {
"ios": [
{
@@ -12,19 +23,102 @@
{
"buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215",
"buttonText": {
"en": "App Store [EU]",
"fa": "App Store [EU]",
"ru": "App Store [EU]",
"zh": "App Store [EU]"
"en": "Open in App Store [EU]",
"fa": "باز کردن در App Store [EU]",
"ru": "Открыть в App Store [EU]",
"zh": "App Store 中打开 [EU]"
}
},
{
"buttonLink": "https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973",
"buttonText": {
"en": "App Store [RU]",
"fa": "App Store [RU]",
"ru": "App Store [RU]",
"zh": "App Store [RU]"
"en": "Open in App Store [RU]",
"fa": "باز کردن در App Store [RU]",
"ru": "Открыть в App Store [RU]",
"zh": "App Store 中打开 [RU]"
}
}
],
"description": {
"en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
"fa": "صفحه را در App Store باز کنید و برنامه را نصب کنید. آن را اجرا کنید، در پنجره مجوز پیکربندی VPN روی Allow کلیک کنید و رمز عبور خود را وارد کنید.",
"ru": "Откройте страницу в App Store и установите приложение. Запустите его, в окне разрешения VPN-конфигурации нажмите Allow и введите свой пароль.",
"zh": "在 App Store 中打开页面并安装应用。启动应用后,在 VPN 配置权限窗口中点击\"允许\"并输入您的密码。"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below — the app will open and the subscription will be added automatically",
"fa": "برای افزودن خودکار اشتراک روی دکمه زیر کلیک کنید - برنامه باز خواهد شد",
"ru": "Нажмите кнопку ниже — приложение откроется, и подписка добавится автоматически.",
"zh": "点击下方按钮 — 应用将打开并自动添加订阅"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
"zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
}
}
},
{
"id": "streisand",
"name": "Streisand",
"isFeatured": false,
"urlScheme": "streisand://import/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://apps.apple.com/ru/app/streisand/id6450534064",
"buttonText": {
"en": "Open in App Store",
"fa": "باز کردن در App Store",
"ru": "Открыть в App Store",
"zh": "在 App Store 中打开"
}
}
],
"description": {
"en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
"fa": "صفحه را در App Store باز کنید و برنامه را نصب کنید. آن را اجرا کنید، در پنجره مجوز پیکربندی VPN روی Allow کلیک کنید و رمز عبور خود را وارد کنید.",
"ru": "Откройте страницу в App Store и установите приложение. Запустите его, в окне разрешения VPN-конфигурации нажмите Allow и введите свой пароль.",
"zh": "在 App Store 中打开页面并安装应用。启动应用后,在 VPN 配置权限窗口中点击\"允许\"并输入您的密码。"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below — the app will open and the subscription will be added automatically",
"fa": "برای افزودن خودکار اشتراک روی دکمه زیر کلیک کنید - برنامه باز خواهد شد",
"ru": "Нажмите кнопку ниже — приложение откроется, и подписка добавится автоматически.",
"zh": "点击下方按钮 — 应用将打开并自动添加订阅"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
"zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
}
}
},
{
"id": "shadowrocket",
"name": "Shadowrocket",
"isFeatured": false,
"urlScheme": "sub://",
"isNeedBase64Encoding": true,
"installationStep": {
"buttons": [
{
"buttonLink": "https://apps.apple.com/ru/app/shadowrocket/id932747118",
"buttonText": {
"en": "Open in App Store",
"fa": "باز کردن در App Store",
"ru": "Открыть в App Store",
"zh": "在 App Store 中打开"
}
}
],
@@ -64,10 +158,10 @@
{
"buttonLink": "https://play.google.com/store/apps/details?id=com.happproxy",
"buttonText": {
"en": "Google Play",
"fa": "Google Play",
"ru": "Google Play",
"zh": "Google Play"
"en": "Open in Google Play",
"fa": "باز کردن در Google Play",
"ru": "Открыть в Google Play",
"zh": "Google Play 中打开"
}
},
{
@@ -103,53 +197,210 @@
"zh": "打开应用并连接到服务器"
}
}
}
],
"macos": [
},
{
"id": "happ",
"name": "Happ",
"isFeatured": true,
"urlScheme": "happ://add/",
"id": "clash-meta",
"name": "Clash Meta",
"isFeatured": false,
"urlScheme": "clash://install-config?url=",
"installationStep": {
"buttons": [
{
"buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215",
"buttonLink": "https://github.com/MetaCubeX/ClashMetaForAndroid/releases/download/v2.11.7/cmfa-2.11.7-meta-universal-release.apk",
"buttonText": {
"en": "App Store [EU]",
"fa": "App Store [EU]",
"ru": "App Store [EU]",
"zh": "App Store [EU]"
"en": "Download APK",
"fa": "دانلود APK",
"ru": "Скачать APK",
"zh": "下载 APK"
}
},
{
"buttonLink": "https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973",
"buttonLink": "https://f-droid.org/packages/com.github.metacubex.clash.meta/",
"buttonText": {
"en": "App Store [RU]",
"fa": "App Store [RU]",
"ru": "App Store [RU]",
"zh": "App Store [RU]"
"en": "Open in F-Droid",
"fa": "در F-Droid باز کنید",
"ru": "Открыть в F-Droid",
"zh": "在 F-Droid 中打开"
}
}
],
"description": {
"en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
"fa": "صفحه را در App Store باز کنید و برنامه را نصب کنید. آن را اجرا کنید، در پنجره مجوز پیکربندی VPN روی Allow کلیک کنید و رمز عبور خود را وارد کنید.",
"ru": "Откройте страницу в App Store и установите приложение. Запустите его, в окне разрешения VPN-конфигурации нажмите Allow и введите свой пароль.",
"zh": "在 App Store 中打开页面并安装应用。启动应用后,在 VPN 配置权限窗口中点击\"允许\"并输入您的密码。"
"en": "Download and install Clash Meta APK",
"fa": "دانلود و نصب Clash Meta APK",
"ru": "Скачайте и установите Clash Meta APK",
"zh": "下载并安装 Clash Meta APK"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below — the app will open and the subscription will be added automatically",
"fa": "برای افزودن خودکار اشتراک روی دکمه زیر کلیک کنید - برنامه باز خواهد شد",
"ru": "Нажмите кнопку ниже — приложение откроется, и подписка добавится автоматически.",
"zh": "点击下方按钮 — 应用将打开并自动添加订阅"
"en": "Tap the button to import configuration",
"fa": "برای وارد کردن پیکربندی روی دکمه ضربه بزنید",
"ru": "Нажмите кнопку, чтобы импортировать конфигурацию",
"zh": "点击按钮导入配置"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
"en": "Open Clash Meta and tap on Connect",
"fa": "Clash Meta را باز کنید و روی اتصال ضربه بزنید",
"ru": "Откройте Clash Meta и нажмите Подключиться",
"zh": "打开 Clash Meta 并点击连接"
}
}
}
],
"macos": [
{
"id": "clash-verge",
"name": "Clash Verge",
"isFeatured": true,
"urlScheme": "clash://install-config?url=",
"installationStep": {
"buttons": [
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_x64-setup.exe",
"buttonText": {
"en": "Windows",
"fa": "ویندوز",
"ru": "Windows",
"zh": "Windows"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_x64.dmg",
"buttonText": {
"en": "macOS (Intel)",
"fa": "مک (اینتل)",
"ru": "macOS (Intel)",
"zh": "macOS (Intel)"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_aarch64.dmg",
"buttonText": {
"en": "macOS (Apple Silicon)",
"fa": "مک (Apple Silicon)",
"ru": "macOS (Apple Silicon)",
"zh": "macOS (Apple Silicon)"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases",
"buttonText": {
"en": "Linux",
"fa": "لینوکس",
"ru": "Linux",
"zh": "Linux"
}
}
],
"description": {
"en": "Choose the version for your device, click the button below and install the app.",
"fa": "نسخه مناسب برای دستگاه خود را انتخاب کنید، دکمه زیر را فشار دهید و برنامه را نصب کنید",
"ru": "Выберите подходящую версию для вашего устройства, нажмите на кнопку ниже и установите приложение.",
"zh": "选择适合您设备的版本,点击下方按钮并安装应用。"
}
},
"additionalBeforeAddSubscriptionStep": {
"buttons": [],
"description": {
"en": "After launching the app, you can change the language in settings. In the left panel, find the gear icon, then navigate to Verge 设置 and select 语言设置.",
"fa": "پس از راه‌اندازی برنامه، می‌توانید زبان را در تنظیمات تغییر دهید. در پنل سمت چپ، نماد چرخ دنده را پیدا کنید، سپس به Verge 设置 بروید و 语言设置 را انتخاب کنید.",
"ru": "После запуска приложения вы можете сменить язык в настройках. В левой панели найдите иконку шестеренки, далее ориентируйтесь на Verge 设置 и выберите пункт 语言设置.",
"zh": "启动应用后,您可以在设置中更改语言。在左侧面板找到齿轮图标,然后导航到 Verge 设置并选择语言设置。"
},
"title": {
"en": "Change language",
"fa": "تغییر زبان",
"ru": "Смена языка",
"zh": "更改语言"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
"ru": "Нажмите кнопку ниже, чтобы добавить подписку",
"zh": "点击下方按钮添加订阅"
}
},
"additionalAfterAddSubscriptionStep": {
"buttons": [],
"title": {
"en": "If the subscription is not added",
"fa": "اگر اشتراک در برنامه نصب نشده است",
"ru": "Если подписка не добавилась",
"zh": "如果订阅未添加"
},
"description": {
"en": "If nothing happens after clicking the button, add the subscription manually. Click the Get Link button in the top right corner of this page, copy the link. In Clash Verge, go to the Profiles section and paste the link in the text field, then click the Import button.",
"fa": "اگر پس از کلیک روی دکمه اتفاقی نیفتاد، اشتراک را به صورت دستی اضافه کنید. در گوشه بالا سمت راست این صفحه روی دکمه دریافت لینک کلیک کنید، لینک را کپی کنید. در Clash Verge به بخش پروفایل‌ها بروید و لینک را در فیلد متنی وارد کنید، سپس روی دکمه وارد کردن کلیک کنید.",
"ru": "Если после нажатия на кнопку ничего не произошло, добавьте подписку вручную. Нажмите на этой страницу кнопку Получить ссылку в правом верхнем углу, скопируйте ссылку. В Clash Verge перейдите в раздел Профили и вставьте ссылку в текстовое поле, затем нажмите на кнопку Импорт.",
"zh": "如果点击按钮后没有反应,请手动添加订阅。点击此页面右上角的获取链接按钮,复制链接。在 Clash Verge 中,转到配置文件部分,将链接粘贴到文本字段中,然后点击导入按钮。"
}
},
"connectAndUseStep": {
"description": {
"en": "You can select a server in the Proxy section, and enable VPN in the Settings section. Set the TUN Mode switch to ON.",
"fa": "می‌توانید در بخش پروکسی سرور را انتخاب کنید و در بخش تنظیمات VPN را فعال کنید. کلید TUN Mode را در حالت روشن قرار دهید.",
"ru": "Выбрать сервер можно в разделе Прокси, включить VPN можно в разделе Настройки. Установите переключатель TUN Mode в положение ВКЛ.",
"zh": "您可以在代理部分选择服务器,在设置部分启用 VPN。将 TUN 模式开关设置为开启。"
}
}
},
{
"id": "hiddify",
"name": "Hiddify",
"isFeatured": false,
"urlScheme": "hiddify://import/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-Windows-Setup-x64.exe",
"buttonText": {
"en": "Windows",
"fa": "ویندوز",
"ru": "Windows",
"zh": "Windows"
}
},
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-MacOS.dmg",
"buttonText": {
"en": "macOS",
"fa": "مک",
"ru": "macOS",
"zh": "macOS"
}
},
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-Linux-x64.AppImage",
"buttonText": {
"en": "Linux",
"fa": "لینوکس",
"ru": "Linux",
"zh": "Linux"
}
}
],
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. If needed, select a different server in the Proxy section",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. در صورت نیاز، سرور دیگری را در بخش پروکسی انتخاب کنید",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. При необходимости выберите другой сервер в разделе Прокси.",
"zh": "在主界面中,点击中央的大电源按钮连接 VPN。如有需要,可在代理部分选择不同的服务器"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
"ru": "Нажмите кнопку ниже, чтобы добавить подписку",
"zh": "点击下方按钮添加订阅"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, select a different server from the server list.",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
"zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
@@ -159,75 +410,249 @@
],
"windows": [
{
"id": "happ",
"name": "Happ",
"isFeatured": false,
"urlScheme": "happ://add/",
"id": "clash-verge",
"name": "Clash Verge",
"isFeatured": true,
"urlScheme": "clash://install-config?url=",
"installationStep": {
"buttons": [
{
"buttonLink": "https://github.com/Happ-proxy/happ-desktop/releases/latest/download/setup-Happ.x86.exe",
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_x64-setup.exe",
"buttonText": {
"en": "Download",
"ru": "Скачать"
"en": "Windows",
"fa": "ویندوز",
"ru": "Windows",
"zh": "Windows"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_x64.dmg",
"buttonText": {
"en": "macOS (Intel)",
"fa": "مک (اینتل)",
"ru": "macOS (Intel)",
"zh": "macOS (Intel)"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_aarch64.dmg",
"buttonText": {
"en": "macOS (Apple Silicon)",
"fa": "مک (Apple Silicon)",
"ru": "macOS (Apple Silicon)",
"zh": "macOS (Apple Silicon)"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases",
"buttonText": {
"en": "Linux",
"fa": "لینوکس",
"ru": "Linux",
"zh": "Linux"
}
}
],
"description": {
"en": "Download and install Happ.",
"ru": "Скачайте и установите Happ"
"en": "Choose the version for your device, click the button below and install the app.",
"fa": "نسخه مناسب برای دستگاه خود را انتخاب کنید، دکمه زیر را فشار دهید و برنامه را نصب کنید",
"ru": "Выберите подходящую версию для вашего устройства, нажмите на кнопку ниже и установите приложение.",
"zh": "选择适合您设备的版本,点击下方按钮并安装应用。"
}
},
"additionalBeforeAddSubscriptionStep": {
"buttons": [],
"description": {
"en": "After launching the app, you can change the language in settings. In the left panel, find the gear icon, then navigate to Verge 设置 and select 语言设置.",
"fa": "پس از راه‌اندازی برنامه، می‌توانید زبان را در تنظیمات تغییر دهید. در پنل سمت چپ، نماد چرخ دنده را پیدا کنید، سپس به Verge 设置 بروید و 语言设置 را انتخاب کنید.",
"ru": "После запуска приложения вы можете сменить язык в настройках. В левой панели найдите иконку шестеренки, далее ориентируйтесь на Verge 设置 и выберите пункт 语言设置.",
"zh": "启动应用后,您可以在设置中更改语言。在左侧面板找到齿轮图标,然后导航到 Verge 设置并选择语言设置。"
},
"title": {
"en": "Change language",
"fa": "تغییر زبان",
"ru": "Смена языка",
"zh": "更改语言"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
"fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
"ru": "Нажмите кнопку ниже, чтобы добавить подписку",
"zh": "点击下方按钮添加订阅"
}
},
"additionalAfterAddSubscriptionStep": {
"buttons": [],
"title": {
"en": "If the subscription is not added",
"fa": "اگر اشتراک در برنامه نصب نشده است",
"ru": "Если подписка не добавилась",
"zh": "如果订阅未添加"
},
"description": {
"en": "If nothing happens after clicking the button, add the subscription manually. Click the Get Link button in the top right corner of this page, copy the link. In Clash Verge, go to the Profiles section and paste the link in the text field, then click the Import button.",
"fa": "اگر پس از کلیک روی دکمه اتفاقی نیفتاد، اشتراک را به صورت دستی اضافه کنید. در گوشه بالا سمت راست این صفحه روی دکمه دریافت لینک کلیک کنید، لینک را کپی کنید. در Clash Verge به بخش پروفایل‌ها بروید و لینک را در فیلد متنی وارد کنید، سپس روی دکمه وارد کردن کلیک کنید.",
"ru": "Если после нажатия на кнопку ничего не произошло, добавьте подписку вручную. Нажмите на этой страницу кнопку Получить ссылку в правом верхнем углу, скопируйте ссылку. В Clash Verge перейдите в раздел Профили и вставьте ссылку в текстовое поле, затем нажмите на кнопку Импорт.",
"zh": "如果点击按钮后没有反应,请手动添加订阅。点击此页面右上角的获取链接按钮,复制链接。在 Clash Verge 中,转到配置文件部分,将链接粘贴到文本字段中,然后点击导入按钮。"
}
},
"connectAndUseStep": {
"description": {
"en": "You can select a server",
"ru": "Выберете локацию, включить VPN"
"en": "You can select a server in the Proxy section, and enable VPN in the Settings section. Set the TUN Mode switch to ON.",
"fa": "می‌توانید در بخش پروکسی سرور را انتخاب کنید و در بخش تنظیمات VPN را فعال کنید. کلید TUN Mode را در حالت روشن قرار دهید.",
"ru": "Выبрать сервер можно в разделе Прокси, включить VPN можно в разделе Настройки. Установите переключатель TUN Mode в положение ВКЛ.",
"zh": "您可以在代理部分选择服务器,在设置部分启用 VPN。将 TUN 模式开关设置为开启。"
}
}
},
{
"id": "hiddify",
"name": "Hiddify",
"isFeatured": false,
"urlScheme": "hiddify://import/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-Windows-Setup-x64.exe",
"buttonText": {
"en": "Windows",
"fa": "ویندوز",
"ru": "Windows",
"zh": "Windows"
}
},
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-MacOS.dmg",
"buttonText": {
"en": "macOS",
"fa": "مک",
"ru": "macOS",
"zh": "macOS"
}
},
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-Linux-x64.AppImage",
"buttonText": {
"en": "Linux",
"fa": "لینوکس",
"ru": "Linux",
"zh": "Linux"
}
}
],
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. If needed, select a different server in the Proxy section",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. در صورت نیاز، سرور دیگری را در بخش پروکسی انتخاب کنید",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. При необходимости выберите другой сервер в разделе Прокси.",
"zh": "在主界面中,点击中央的大电源按钮连接 VPN。如有需要,可在代理部分选择不同的服务器"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
"ru": "Нажмите кнопку ниже, чтобы добавить подписку",
"zh": "点击下方按钮添加订阅"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, select a different server from the server list.",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
"zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
}
}
}
],
"linux": [],
"androidTV": [
{
"id": "vpn4tv",
"name": "VPN4TV",
"id": "new-app-androidtv-1760203310792",
"name": "Happ",
"isFeatured": true,
"urlScheme": "",
"urlScheme": "happ://add/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://play.google.com/store/apps/details?id=com.vpn4tv.hiddify",
"buttonText": {
"en": "Google Play",
"ru": "Google Play"
"ru": "Button TextGoogle Play",
"zh": "Button Text",
"fa": "Button Text"
}
}
],
"description": {
"en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
"ru": "Откройте страницу в Google Play и установите приложение"
"ru": "Откройте страницу в Google Play и установите приложение",
"zh": "-",
"fa": "-"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал"
"ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал",
"zh": "-",
"fa": "-"
}
},
"connectAndUseStep": {
"description": {
"en": "Open the app and connect to the server",
"ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN"
"ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN",
"zh": "-",
"fa": "-"
}
}
}
],
"linux": [],
"appleTV": []
"appleTV": [
{
"id": "new-app-appletv-1760203488851",
"name": "Happ",
"isFeatured": true,
"urlScheme": "happ://add/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://play.google.com/store/apps/details?id=com.vpn4tv.hiddify",
"buttonText": {
"en": "Google Play",
"ru": "Google Play",
"zh": "Button Text",
"fa": "Button Text"
}
}
],
"description": {
"en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
"ru": "Откройте страницу в Google Play и установите приложение",
"zh": "-",
"fa": "-"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал",
"zh": "-",
"fa": "-"
}
},
"connectAndUseStep": {
"description": {
"en": "Open the app and connect to the server",
"ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN",
"zh": "-",
"fa": "-"
}
}
}
]
}
}
}
+6 -1
View File
@@ -10,7 +10,8 @@ from app.middlewares.auth import AuthMiddleware
from app.middlewares.logging import LoggingMiddleware
from app.middlewares.throttling import ThrottlingMiddleware
from app.middlewares.subscription_checker import SubscriptionStatusMiddleware
from app.middlewares.maintenance import MaintenanceMiddleware
from app.middlewares.maintenance import MaintenanceMiddleware
from app.middlewares.display_name_restriction import DisplayNameRestrictionMiddleware
from app.services.maintenance_service import maintenance_service
from app.utils.cache import cache
@@ -102,6 +103,10 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
dp.callback_query.middleware(LoggingMiddleware())
dp.message.middleware(MaintenanceMiddleware())
dp.callback_query.middleware(MaintenanceMiddleware())
display_name_middleware = DisplayNameRestrictionMiddleware()
dp.message.middleware(display_name_middleware)
dp.callback_query.middleware(display_name_middleware)
dp.pre_checkout_query.middleware(display_name_middleware)
dp.message.middleware(ThrottlingMiddleware())
dp.callback_query.middleware(ThrottlingMiddleware())
+78 -2
View File
@@ -12,6 +12,12 @@ from pydantic import field_validator, Field
from pathlib import Path
DEFAULT_DISPLAY_NAME_BANNED_KEYWORDS = [
"tme",
"joingroup",
]
class Settings(BaseSettings):
BOT_TOKEN: str
@@ -198,6 +204,10 @@ class Settings(BaseSettings):
MULENPAY_DESCRIPTION: str = "Пополнение баланса"
MULENPAY_LANGUAGE: str = "ru"
MULENPAY_VAT_CODE: int = 0
DISPLAY_NAME_BANNED_KEYWORDS: str = "\n".join(
DEFAULT_DISPLAY_NAME_BANNED_KEYWORDS
)
MULENPAY_PAYMENT_SUBJECT: int = 4
MULENPAY_PAYMENT_MODE: int = 4
MULENPAY_MIN_AMOUNT_KOPEKS: int = 10000
@@ -217,6 +227,7 @@ class Settings(BaseSettings):
PAL24_SBP_BUTTON_TEXT: Optional[str] = None
PAL24_CARD_BUTTON_TEXT: Optional[str] = None
MAIN_MENU_MODE: str = "default"
CONNECT_BUTTON_MODE: str = "guide"
MINIAPP_CUSTOM_URL: str = ""
MINIAPP_PURCHASE_URL: str = ""
@@ -283,6 +294,29 @@ class Settings(BaseSettings):
EXTERNAL_ADMIN_TOKEN: Optional[str] = None
EXTERNAL_ADMIN_TOKEN_BOT_ID: Optional[int] = None
@field_validator('MAIN_MENU_MODE', mode='before')
@classmethod
def normalize_main_menu_mode(cls, value: Optional[str]) -> str:
if not value:
return "default"
normalized = str(value).strip().lower()
aliases = {
"classic": "default",
"default": "default",
"full": "default",
"standard": "default",
"text": "text",
"text_only": "text",
"textual": "text",
"minimal": "text",
}
mode = aliases.get(normalized, normalized)
if mode not in {"default", "text"}:
raise ValueError("MAIN_MENU_MODE must be one of: default, text")
return mode
@field_validator('SERVER_STATUS_MODE', mode='before')
@classmethod
def normalize_server_status_mode(cls, value: Optional[str]) -> str:
@@ -455,6 +489,29 @@ class Settings(BaseSettings):
description = re.sub(r'\s+', ' ', description).strip()
return description
def get_display_name_banned_keywords(self) -> List[str]:
raw_value = self.DISPLAY_NAME_BANNED_KEYWORDS
if raw_value is None:
return []
if isinstance(raw_value, str):
candidates = re.split(r"[\n,]+", raw_value)
else:
candidates = list(raw_value)
unique: List[str] = []
seen: set[str] = set()
for candidate in candidates:
normalized = str(candidate).strip().lower()
if not normalized:
continue
if normalized in seen:
continue
seen.add(normalized)
unique.append(normalized)
return unique
def get_autopay_warning_days(self) -> List[int]:
try:
@@ -491,8 +548,14 @@ class Settings(BaseSettings):
return bool(getattr(self, "LANGUAGE_SELECTION_ENABLED", True))
def format_price(self, price_kopeks: int) -> str:
rubles = price_kopeks // 100
return f"{rubles}"
sign = "-" if price_kopeks < 0 else ""
rubles, kopeks = divmod(abs(price_kopeks), 100)
if kopeks:
value = f"{sign}{rubles}.{kopeks:02d}".rstrip("0").rstrip(".")
return f"{value}"
return f"{sign}{rubles}"
def get_reports_chat_id(self) -> Optional[str]:
if self.ADMIN_REPORTS_CHAT_ID:
@@ -538,6 +601,19 @@ class Settings(BaseSettings):
def is_notifications_enabled(self) -> bool:
return self.ENABLE_NOTIFICATIONS
def get_main_menu_mode(self) -> str:
return getattr(self, "MAIN_MENU_MODE", "default")
def is_text_main_menu_mode(self) -> bool:
return self.get_main_menu_mode() == "text"
def get_main_menu_miniapp_url(self) -> Optional[str]:
for candidate in [self.MINIAPP_CUSTOM_URL, self.MINIAPP_PURCHASE_URL]:
value = (candidate or "").strip()
if value:
return value
return None
def get_app_config_path(self) -> str:
if os.path.isabs(self.APP_CONFIG_PATH):
+29 -3
View File
@@ -17,7 +17,14 @@ from sqlalchemy import (
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database.models import PromoGroup, ServerSquad, SubscriptionServer, Subscription, User
from app.database.models import (
PromoGroup,
ServerSquad,
SubscriptionServer,
Subscription,
SubscriptionStatus,
User,
)
logger = logging.getLogger(__name__)
@@ -495,10 +502,10 @@ def _extract_country_code(original_name: str) -> Optional[str]:
async def get_server_statistics(db: AsyncSession) -> dict:
total_result = await db.execute(select(func.count(ServerSquad.id)))
total_servers = total_result.scalar()
available_result = await db.execute(
select(func.count(ServerSquad.id))
.where(ServerSquad.is_available == True)
@@ -537,6 +544,25 @@ async def get_server_statistics(db: AsyncSession) -> dict:
'total_revenue_rubles': total_revenue_kopeks / 100
}
async def count_active_users_for_squad(db: AsyncSession, squad_uuid: str) -> int:
"""Возвращает количество активных подписок, подключенных к указанному скваду."""
result = await db.execute(
select(func.count(Subscription.id)).where(
Subscription.status.in_(
[
SubscriptionStatus.ACTIVE.value,
SubscriptionStatus.TRIAL.value,
]
),
cast(Subscription.connected_squads, String).like(f'%"{squad_uuid}"%'),
)
)
return result.scalar() or 0
async def add_user_to_servers(
db: AsyncSession,
server_squad_ids: List[int]
+2
View File
@@ -39,6 +39,7 @@ async def get_user_by_id(db: AsyncSession, user_id: int) -> Optional[User]:
.options(
selectinload(User.subscription),
selectinload(User.promo_group),
selectinload(User.referrer),
)
.where(User.id == user_id)
)
@@ -56,6 +57,7 @@ async def get_user_by_telegram_id(db: AsyncSession, telegram_id: int) -> Optiona
.options(
selectinload(User.subscription),
selectinload(User.promo_group),
selectinload(User.referrer),
)
.where(User.telegram_id == telegram_id)
)
+12 -10
View File
@@ -1,4 +1,5 @@
import logging
from decimal import Decimal, ROUND_HALF_UP
from typing import Optional, Dict, Any
from aiogram import Bot
from aiogram.types import LabeledPrice, InlineKeyboardMarkup, InlineKeyboardButton
@@ -18,8 +19,9 @@ class TelegramStarsService:
return settings.rubles_to_stars(rubles)
@staticmethod
def calculate_rubles_from_stars(stars: int) -> float:
return settings.stars_to_rubles(stars)
def calculate_rubles_from_stars(stars: int) -> Decimal:
rate = Decimal(str(settings.get_stars_rate()))
return (Decimal(stars) * rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
async def create_invoice(
self,
@@ -31,10 +33,10 @@ class TelegramStarsService:
start_parameter: Optional[str] = None
) -> Optional[str]:
try:
amount_rubles = amount_kopeks / 100
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
amount_rubles = Decimal(amount_kopeks) / Decimal(100)
stars_amount = self.calculate_stars_from_rubles(float(amount_rubles))
stars_rate = settings.get_stars_rate()
invoice_link = await self.bot.create_invoice_link(
title=title,
description=description,
@@ -46,7 +48,7 @@ class TelegramStarsService:
)
logger.info(
f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}) "
f"Создан Stars invoice на {stars_amount} звезд (~{settings.format_price(amount_kopeks)}) "
f"для {chat_id}, курс: {stars_rate}₽/⭐"
)
return invoice_link
@@ -65,8 +67,8 @@ class TelegramStarsService:
keyboard: Optional[InlineKeyboardMarkup] = None
) -> Optional[Dict[str, Any]]:
try:
amount_rubles = amount_kopeks / 100
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
amount_rubles = Decimal(amount_kopeks) / Decimal(100)
stars_amount = self.calculate_stars_from_rubles(float(amount_rubles))
stars_rate = settings.get_stars_rate()
message = await self.bot.send_invoice(
@@ -82,12 +84,12 @@ class TelegramStarsService:
logger.info(
f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд "
f"(~{int(amount_rubles)}), курс: {stars_rate}₽/⭐"
f"(~{settings.format_price(amount_kopeks)}), курс: {stars_rate}₽/⭐"
)
return {
"message_id": message.message_id,
"stars_amount": stars_amount,
"rubles_amount": amount_rubles,
"rubles_amount": float(amount_rubles),
"payload": payload
}
+22 -5
View File
@@ -28,12 +28,21 @@ from app.localization.texts import get_texts
from app.database.crud.user import get_users_list
from app.database.crud.subscription import get_expiring_subscriptions
from app.utils.decorators import admin_required, error_handler
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
logger = logging.getLogger(__name__)
BUTTON_ROWS = BROADCAST_BUTTON_ROWS
DEFAULT_SELECTED_BUTTONS = DEFAULT_BROADCAST_BUTTONS
TEXT_MENU_MINIAPP_BUTTON_KEYS = {
"balance",
"referrals",
"promocode",
"connect",
"subscription",
}
def get_message_buttons_selector_keyboard(language: str = "ru") -> types.InlineKeyboardMarkup:
return get_updated_message_buttons_selector_keyboard(list(DEFAULT_SELECTED_BUTTONS), language)
@@ -54,12 +63,20 @@ def create_broadcast_keyboard(selected_buttons: list, language: str = "ru") -> O
if button_key not in selected_buttons:
continue
button_config = button_config_map[button_key]
row_buttons.append(
types.InlineKeyboardButton(
text=button_config["text"],
callback_data=button_config["callback"]
if settings.is_text_main_menu_mode() and button_key in TEXT_MENU_MINIAPP_BUTTON_KEYS:
row_buttons.append(
build_miniapp_or_callback_button(
text=button_config["text"],
callback_data=button_config["callback"],
)
)
else:
row_buttons.append(
types.InlineKeyboardButton(
text=button_config["text"],
callback_data=button_config["callback"]
)
)
)
if row_buttons:
keyboard.append(row_buttons)
+7 -1
View File
@@ -44,6 +44,7 @@ from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
from app.utils.subscription_utils import get_display_subscription_link
from app.utils.formatters import format_datetime, format_duration
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
logger = logging.getLogger(__name__)
@@ -1920,7 +1921,12 @@ async def _send_offer_to_users(
user_texts = get_texts(user.language or db_user.language)
keyboard_rows: List[List[InlineKeyboardButton]] = [
[InlineKeyboardButton(text=template.button_text, callback_data=f"claim_discount_{offer_record.id}")]
[
build_miniapp_or_callback_button(
text=template.button_text,
callback_data=f"claim_discount_{offer_record.id}",
)
]
]
keyboard_rows.append([
+780 -2
View File
@@ -1,18 +1,24 @@
import logging
import math
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from app.states import SquadRenameStates, SquadCreateStates
from app.states import SquadRenameStates, SquadCreateStates, SquadMigrationStates
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.database.crud.server_squad import (
count_active_users_for_squad,
get_all_server_squads,
get_server_squad_by_uuid,
)
from app.keyboards.admin import (
get_admin_remnawave_keyboard, get_sync_options_keyboard,
get_node_management_keyboard, get_confirmation_keyboard,
get_squad_management_keyboard, get_squad_edit_keyboard
)
from app.localization.texts import get_texts
from app.services.remnawave_service import RemnaWaveService
from app.services.remnawave_service import RemnaWaveService, RemnaWaveConfigurationError
from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_bytes, format_datetime
@@ -21,6 +27,769 @@ logger = logging.getLogger(__name__)
squad_inbound_selections = {}
squad_create_data = {}
MIGRATION_PAGE_SIZE = 8
def _format_migration_server_label(texts, server) -> str:
status = (
texts.t("ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE", "✅ Доступен")
if getattr(server, "is_available", True)
else texts.t("ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE", "🚫 Недоступен")
)
return texts.t(
"ADMIN_SQUAD_MIGRATION_SERVER_LABEL",
"{name} — 👥 {users} ({status})",
).format(name=server.display_name, users=server.current_users, status=status)
def _build_migration_keyboard(
texts,
squads,
page: int,
total_pages: int,
stage: str,
*,
exclude_uuid: str = None,
):
prefix = "admin_migration_source" if stage == "source" else "admin_migration_target"
rows = []
has_items = False
button_template = texts.t(
"ADMIN_SQUAD_MIGRATION_SQUAD_BUTTON",
"🌍 {name} — 👥 {users} ({status})",
)
for squad in squads:
if exclude_uuid and squad.squad_uuid == exclude_uuid:
continue
has_items = True
status = (
texts.t("ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE_SHORT", "")
if getattr(squad, "is_available", True)
else texts.t("ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE_SHORT", "🚫")
)
rows.append(
[
types.InlineKeyboardButton(
text=button_template.format(
name=squad.display_name,
users=squad.current_users,
status=status,
),
callback_data=f"{prefix}_{squad.squad_uuid}",
)
]
)
if total_pages > 1:
nav_buttons = []
if page > 1:
nav_buttons.append(
types.InlineKeyboardButton(
text="⬅️",
callback_data=f"{prefix}_page_{page - 1}",
)
)
nav_buttons.append(
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_PAGE",
"Стр. {page}/{pages}",
).format(page=page, pages=total_pages),
callback_data="admin_migration_page_info",
)
)
if page < total_pages:
nav_buttons.append(
types.InlineKeyboardButton(
text="➡️",
callback_data=f"{prefix}_page_{page + 1}",
)
)
rows.append(nav_buttons)
rows.append(
[
types.InlineKeyboardButton(
text=texts.CANCEL,
callback_data="admin_migration_cancel",
)
]
)
return types.InlineKeyboardMarkup(inline_keyboard=rows), has_items
async def _fetch_migration_page(
db: AsyncSession,
page: int,
):
squads, total = await get_all_server_squads(
db,
page=max(1, page),
limit=MIGRATION_PAGE_SIZE,
)
total_pages = max(1, math.ceil(total / MIGRATION_PAGE_SIZE))
if page < 1:
page = 1
if page > total_pages:
page = total_pages
squads, total = await get_all_server_squads(
db,
page=page,
limit=MIGRATION_PAGE_SIZE,
)
total_pages = max(1, math.ceil(total / MIGRATION_PAGE_SIZE))
return squads, page, total_pages
@admin_required
@error_handler
async def show_squad_migration_menu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
texts = get_texts(db_user.language)
await state.clear()
squads, page, total_pages = await _fetch_migration_page(db, page=1)
keyboard, has_items = _build_migration_keyboard(
texts,
squads,
page,
total_pages,
"source",
)
message = (
texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 <b>Переезд сквадов</b>")
+ "\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_SELECT_SOURCE",
"Выберите сквад, из которого нужно переехать:",
)
)
if not has_items:
message += (
"\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_NO_OPTIONS",
"Нет доступных сквадов. Добавьте новые или отмените операцию.",
)
)
await state.set_state(SquadMigrationStates.selecting_source)
await callback.message.edit_text(
message,
reply_markup=keyboard,
disable_web_page_preview=True,
)
await callback.answer()
@admin_required
@error_handler
async def paginate_migration_source(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
if await state.get_state() != SquadMigrationStates.selecting_source:
await callback.answer()
return
try:
page = int(callback.data.split("_page_")[-1])
except (ValueError, IndexError):
await callback.answer()
return
squads, page, total_pages = await _fetch_migration_page(db, page=page)
texts = get_texts(db_user.language)
keyboard, has_items = _build_migration_keyboard(
texts,
squads,
page,
total_pages,
"source",
)
message = (
texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 <b>Переезд сквадов</b>")
+ "\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_SELECT_SOURCE",
"Выберите сквад, из которого нужно переехать:",
)
)
if not has_items:
message += (
"\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_NO_OPTIONS",
"Нет доступных сквадов. Добавьте новые или отмените операцию.",
)
)
await callback.message.edit_text(
message,
reply_markup=keyboard,
disable_web_page_preview=True,
)
await callback.answer()
@admin_required
@error_handler
async def handle_migration_source_selection(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
if await state.get_state() != SquadMigrationStates.selecting_source:
await callback.answer()
return
if "_page_" in callback.data:
await callback.answer()
return
source_uuid = callback.data.replace("admin_migration_source_", "", 1)
texts = get_texts(db_user.language)
server = await get_server_squad_by_uuid(db, source_uuid)
if not server:
await callback.answer(
texts.t(
"ADMIN_SQUAD_MIGRATION_SQUAD_NOT_FOUND",
"Сквад не найден или недоступен.",
),
show_alert=True,
)
return
await state.update_data(
source_uuid=server.squad_uuid,
source_display=_format_migration_server_label(texts, server),
)
squads, page, total_pages = await _fetch_migration_page(db, page=1)
keyboard, has_items = _build_migration_keyboard(
texts,
squads,
page,
total_pages,
"target",
exclude_uuid=server.squad_uuid,
)
message = (
texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 <b>Переезд сквадов</b>")
+ "\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE",
"Источник: {source}",
).format(source=_format_migration_server_label(texts, server))
+ "\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_SELECT_TARGET",
"Выберите сквад, в который нужно переехать:",
)
)
if not has_items:
message += (
"\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_TARGET_EMPTY",
"Нет других сквадов для переезда. Отмените операцию или создайте новые сквады.",
)
)
await state.set_state(SquadMigrationStates.selecting_target)
await callback.message.edit_text(
message,
reply_markup=keyboard,
disable_web_page_preview=True,
)
await callback.answer()
@admin_required
@error_handler
async def paginate_migration_target(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
if await state.get_state() != SquadMigrationStates.selecting_target:
await callback.answer()
return
try:
page = int(callback.data.split("_page_")[-1])
except (ValueError, IndexError):
await callback.answer()
return
data = await state.get_data()
source_uuid = data.get("source_uuid")
if not source_uuid:
await callback.answer()
return
texts = get_texts(db_user.language)
squads, page, total_pages = await _fetch_migration_page(db, page=page)
keyboard, has_items = _build_migration_keyboard(
texts,
squads,
page,
total_pages,
"target",
exclude_uuid=source_uuid,
)
source_display = data.get("source_display") or source_uuid
message = (
texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 <b>Переезд сквадов</b>")
+ "\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE",
"Источник: {source}",
).format(source=source_display)
+ "\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_SELECT_TARGET",
"Выберите сквад, в который нужно переехать:",
)
)
if not has_items:
message += (
"\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_TARGET_EMPTY",
"Нет других сквадов для переезда. Отмените операцию или создайте новые сквады.",
)
)
await callback.message.edit_text(
message,
reply_markup=keyboard,
disable_web_page_preview=True,
)
await callback.answer()
@admin_required
@error_handler
async def handle_migration_target_selection(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
current_state = await state.get_state()
if current_state != SquadMigrationStates.selecting_target:
await callback.answer()
return
if "_page_" in callback.data:
await callback.answer()
return
data = await state.get_data()
source_uuid = data.get("source_uuid")
if not source_uuid:
await callback.answer()
return
target_uuid = callback.data.replace("admin_migration_target_", "", 1)
texts = get_texts(db_user.language)
if target_uuid == source_uuid:
await callback.answer(
texts.t(
"ADMIN_SQUAD_MIGRATION_SAME_SQUAD",
"Нельзя выбрать тот же сквад.",
),
show_alert=True,
)
return
target_server = await get_server_squad_by_uuid(db, target_uuid)
if not target_server:
await callback.answer(
texts.t(
"ADMIN_SQUAD_MIGRATION_SQUAD_NOT_FOUND",
"Сквад не найден или недоступен.",
),
show_alert=True,
)
return
source_display = data.get("source_display") or source_uuid
users_to_move = await count_active_users_for_squad(db, source_uuid)
await state.update_data(
target_uuid=target_server.squad_uuid,
target_display=_format_migration_server_label(texts, target_server),
migration_count=users_to_move,
)
await state.set_state(SquadMigrationStates.confirming)
message_lines = [
texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 <b>Переезд сквадов</b>"),
"",
texts.t(
"ADMIN_SQUAD_MIGRATION_CONFIRM_DETAILS",
"Проверьте параметры переезда:",
),
texts.t(
"ADMIN_SQUAD_MIGRATION_CONFIRM_SOURCE",
"• Из: {source}",
).format(source=source_display),
texts.t(
"ADMIN_SQUAD_MIGRATION_CONFIRM_TARGET",
"В: {target}",
).format(target=_format_migration_server_label(texts, target_server)),
texts.t(
"ADMIN_SQUAD_MIGRATION_CONFIRM_COUNT",
"• Пользователей к переносу: {count}",
).format(count=users_to_move),
"",
texts.t(
"ADMIN_SQUAD_MIGRATION_CONFIRM_PROMPT",
"Подтвердите выполнение операции.",
),
]
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_CONFIRM_BUTTON",
"✅ Подтвердить",
),
callback_data="admin_migration_confirm",
)
],
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_CHANGE_TARGET",
"🔄 Изменить сервер назначения",
),
callback_data="admin_migration_change_target",
)
],
[
types.InlineKeyboardButton(
text=texts.CANCEL,
callback_data="admin_migration_cancel",
)
],
]
)
await callback.message.edit_text(
"\n".join(message_lines),
reply_markup=keyboard,
disable_web_page_preview=True,
)
await callback.answer()
@admin_required
@error_handler
async def change_migration_target(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
data = await state.get_data()
source_uuid = data.get("source_uuid")
if not source_uuid:
await callback.answer()
return
await state.set_state(SquadMigrationStates.selecting_target)
texts = get_texts(db_user.language)
squads, page, total_pages = await _fetch_migration_page(db, page=1)
keyboard, has_items = _build_migration_keyboard(
texts,
squads,
page,
total_pages,
"target",
exclude_uuid=source_uuid,
)
source_display = data.get("source_display") or source_uuid
message = (
texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 <b>Переезд сквадов</b>")
+ "\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE",
"Источник: {source}",
).format(source=source_display)
+ "\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_SELECT_TARGET",
"Выберите сквад, в который нужно переехать:",
)
)
if not has_items:
message += (
"\n\n"
+ texts.t(
"ADMIN_SQUAD_MIGRATION_TARGET_EMPTY",
"Нет других сквадов для переезда. Отмените операцию или создайте новые сквады.",
)
)
await callback.message.edit_text(
message,
reply_markup=keyboard,
disable_web_page_preview=True,
)
await callback.answer()
@admin_required
@error_handler
async def confirm_squad_migration(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
current_state = await state.get_state()
if current_state != SquadMigrationStates.confirming:
await callback.answer()
return
data = await state.get_data()
source_uuid = data.get("source_uuid")
target_uuid = data.get("target_uuid")
if not source_uuid or not target_uuid:
await callback.answer()
return
texts = get_texts(db_user.language)
remnawave_service = RemnaWaveService()
await callback.answer(texts.t("ADMIN_SQUAD_MIGRATION_IN_PROGRESS", "Запускаю переезд..."))
try:
result = await remnawave_service.migrate_squad_users(
db,
source_uuid=source_uuid,
target_uuid=target_uuid,
)
except RemnaWaveConfigurationError as error:
message = texts.t(
"ADMIN_SQUAD_MIGRATION_API_ERROR",
"❌ RemnaWave API не настроен: {error}",
).format(error=str(error))
reply_markup = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_BACK_BUTTON",
"⬅️ В Remnawave",
),
callback_data="admin_remnawave",
)
]
]
)
await callback.message.edit_text(message, reply_markup=reply_markup)
await state.clear()
return
source_display = data.get("source_display") or source_uuid
target_display = data.get("target_display") or target_uuid
if not result.get("success"):
error_message = result.get("message") or ""
error_code = result.get("error") or "unexpected"
message = texts.t(
"ADMIN_SQUAD_MIGRATION_ERROR",
"❌ Не удалось выполнить переезд (код: {code}). {details}",
).format(code=error_code, details=error_message)
reply_markup = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_BACK_BUTTON",
"⬅️ В Remnawave",
),
callback_data="admin_remnawave",
)
],
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_NEW_BUTTON",
"🔁 Новый переезд",
),
callback_data="admin_rw_migration",
)
],
]
)
await callback.message.edit_text(message, reply_markup=reply_markup)
await state.clear()
return
message_lines = [
texts.t("ADMIN_SQUAD_MIGRATION_SUCCESS_TITLE", "✅ Переезд завершен"),
"",
texts.t("ADMIN_SQUAD_MIGRATION_CONFIRM_SOURCE", "• Из: {source}").format(
source=source_display
),
texts.t("ADMIN_SQUAD_MIGRATION_CONFIRM_TARGET", "В: {target}").format(
target=target_display
),
"",
texts.t(
"ADMIN_SQUAD_MIGRATION_RESULT_TOTAL",
"Найдено подписок: {count}",
).format(count=result.get("total", 0)),
texts.t(
"ADMIN_SQUAD_MIGRATION_RESULT_UPDATED",
"Перенесено: {count}",
).format(count=result.get("updated", 0)),
]
panel_updated = result.get("panel_updated", 0)
panel_failed = result.get("panel_failed", 0)
if panel_updated:
message_lines.append(
texts.t(
"ADMIN_SQUAD_MIGRATION_RESULT_PANEL_UPDATED",
"Обновлено в панели: {count}",
).format(count=panel_updated)
)
if panel_failed:
message_lines.append(
texts.t(
"ADMIN_SQUAD_MIGRATION_RESULT_PANEL_FAILED",
"Не удалось обновить в панели: {count}",
).format(count=panel_failed)
)
reply_markup = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_NEW_BUTTON",
"🔁 Новый переезд",
),
callback_data="admin_rw_migration",
)
],
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_BACK_BUTTON",
"⬅️ В Remnawave",
),
callback_data="admin_remnawave",
)
],
]
)
await callback.message.edit_text(
"\n".join(message_lines),
reply_markup=reply_markup,
disable_web_page_preview=True,
)
await state.clear()
@admin_required
@error_handler
async def cancel_squad_migration(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
texts = get_texts(db_user.language)
await state.clear()
message = texts.t(
"ADMIN_SQUAD_MIGRATION_CANCELLED",
"❌ Переезд отменен.",
)
reply_markup = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_SQUAD_MIGRATION_BACK_BUTTON",
"⬅️ В Remnawave",
),
callback_data="admin_remnawave",
)
]
]
)
await callback.message.edit_text(message, reply_markup=reply_markup)
await callback.answer()
@admin_required
@error_handler
async def handle_migration_page_info(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
texts = get_texts(db_user.language)
await callback.answer(
texts.t("ADMIN_SQUAD_MIGRATION_PAGE_HINT", "Это текущая страница."),
show_alert=False,
)
@admin_required
@error_handler
async def show_remnawave_menu(
@@ -1920,6 +2689,15 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(restart_all_nodes, F.data == "admin_restart_all_nodes")
dp.callback_query.register(show_sync_options, F.data == "admin_rw_sync")
dp.callback_query.register(sync_all_users, F.data == "sync_all_users")
dp.callback_query.register(show_squad_migration_menu, F.data == "admin_rw_migration")
dp.callback_query.register(paginate_migration_source, F.data.startswith("admin_migration_source_page_"))
dp.callback_query.register(handle_migration_source_selection, F.data.startswith("admin_migration_source_"))
dp.callback_query.register(paginate_migration_target, F.data.startswith("admin_migration_target_page_"))
dp.callback_query.register(handle_migration_target_selection, F.data.startswith("admin_migration_target_"))
dp.callback_query.register(change_migration_target, F.data == "admin_migration_change_target")
dp.callback_query.register(confirm_squad_migration, F.data == "admin_migration_confirm")
dp.callback_query.register(cancel_squad_migration, F.data == "admin_migration_cancel")
dp.callback_query.register(handle_migration_page_info, F.data == "admin_migration_page_info")
dp.callback_query.register(show_squads_management, F.data == "admin_rw_squads")
dp.callback_query.register(show_squad_details, F.data.startswith("admin_squad_manage_"))
dp.callback_query.register(manage_squad_action, F.data.startswith("squad_add_users_"))
+41 -22
View File
@@ -371,7 +371,7 @@ async def handle_admin_ticket_reply(
return
# Обычный режим ответа админа
ticket = await TicketCRUD.get_ticket_by_id(db, ticket_id, load_messages=False)
ticket = await TicketCRUD.get_ticket_by_id(db, ticket_id, load_messages=False, load_user=True)
if not ticket:
texts = get_texts(db_user.language)
await message.answer(
@@ -859,52 +859,71 @@ async def notify_user_about_ticket_reply(bot: Bot, ticket: Ticket, reply_text: s
except Exception:
pass
from app.localization.texts import get_texts
# Получаем тикет с пользователем
ticket_with_user = await TicketCRUD.get_ticket_by_id(db, ticket.id, load_user=True)
if not ticket_with_user or not ticket_with_user.user:
# Обеспечим наличие данных пользователя в объекте тикета
ticket_with_user = ticket
if not getattr(ticket_with_user, "user", None):
ticket_with_user = await TicketCRUD.get_ticket_by_id(db, ticket.id, load_user=True)
user = getattr(ticket_with_user, "user", None)
if not user:
logger.error(f"User not found for ticket #{ticket.id}")
return
texts = get_texts(ticket_with_user.user.language)
if not getattr(user, "telegram_id", None):
logger.error(
"Cannot notify ticket #%s user without telegram_id (username=%s)",
ticket.id,
getattr(user, "username", None),
)
return
chat_id = int(user.telegram_id)
texts = get_texts(user.language)
# Формируем уведомление
base_text = texts.t(
"TICKET_REPLY_NOTIFICATION",
"TICKET_REPLY_NOTIFICATION",
"🎫 Получен ответ по тикету #{ticket_id}\n\n{reply_preview}\n\nНажмите кнопку ниже, чтобы перейти к тикету:"
).format(
ticket_id=ticket.id,
reply_preview=reply_text[:100] + "..." if len(reply_text) > 100 else reply_text
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text=texts.t("VIEW_TICKET", "👁️ Посмотреть тикет"), callback_data=f"view_ticket_{ticket.id}")],
[types.InlineKeyboardButton(text=texts.t("CLOSE_NOTIFICATION", "❌ Закрыть уведомление"), callback_data=f"close_ticket_notification_{ticket.id}")]
])
# Если было фото в последнем ответе админа — отправим как фото
last_message = await TicketMessageCRUD.get_last_message(db, ticket.id)
if last_message and last_message.has_media and last_message.media_type == "photo" and last_message.is_from_admin:
caption = base_text
try:
await bot.send_photo(
chat_id=ticket_with_user.user.telegram_id,
chat_id=chat_id,
photo=last_message.media_file_id,
caption=caption,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text=texts.t("VIEW_TICKET", "👁️ Посмотреть тикет"), callback_data=f"view_ticket_{ticket.id}")],
[types.InlineKeyboardButton(text=texts.t("CLOSE_NOTIFICATION", "❌ Закрыть уведомление"), callback_data=f"close_ticket_notification_{ticket.id}")]
])
reply_markup=keyboard,
)
return
except TelegramBadRequest as photo_error:
logger.error(
"Не удалось отправить фото-уведомление пользователю %s для тикета %s: %s",
chat_id,
ticket.id,
photo_error,
)
except Exception as e:
logger.error(f"Не удалось отправить фото-уведомление: {e}")
# Фоллбек: текстовое уведомление
await bot.send_message(
chat_id=ticket_with_user.user.telegram_id,
chat_id=chat_id,
text=base_text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text=texts.t("VIEW_TICKET", "👁️ Посмотреть тикет"), callback_data=f"view_ticket_{ticket.id}")],
[types.InlineKeyboardButton(text=texts.t("CLOSE_NOTIFICATION", "❌ Закрыть уведомление"), callback_data=f"close_ticket_notification_{ticket.id}")]
])
reply_markup=keyboard,
)
logger.info(f"Ticket #{ticket.id} reply notification sent to user {ticket_with_user.user.telegram_id}")
logger.info(f"Ticket #{ticket.id} reply notification sent to user {chat_id}")
except Exception as e:
logger.error(f"Error notifying user about ticket reply: {e}")
+26 -3
View File
@@ -569,10 +569,33 @@ async def process_topup_amount(
state: FSMContext
):
texts = get_texts(db_user.language)
try:
amount_rubles = float(message.text.replace(',', '.'))
if not message.text:
if message.successful_payment:
logger.info(
"Получено сообщение об успешном платеже без текста, "
"обработчик суммы пополнения завершает работу"
)
await state.clear()
return
await message.answer(
texts.INVALID_AMOUNT,
reply_markup=get_back_keyboard(db_user.language)
)
return
amount_text = message.text.strip()
if not amount_text:
await message.answer(
texts.INVALID_AMOUNT,
reply_markup=get_back_keyboard(db_user.language)
)
return
amount_rubles = float(amount_text.replace(',', '.'))
if amount_rubles < 1:
await message.answer("Минимальная сумма пополнения: 1 ₽")
return
+40 -13
View File
@@ -168,12 +168,14 @@ async def show_main_menu(
db_user.telegram_id
)
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
custom_buttons = []
if not settings.is_text_main_menu_mode():
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
await edit_or_answer_photo(
callback=callback,
@@ -191,11 +193,28 @@ async def show_main_menu(
custom_buttons=custom_buttons,
),
parse_mode="HTML",
force_text=settings.is_text_main_menu_mode(),
)
if not skip_callback_answer:
await callback.answer()
async def handle_profile_unavailable(callback: types.CallbackQuery) -> None:
language = getattr(callback.from_user, "language_code", None) or settings.DEFAULT_LANGUAGE
try:
texts = get_texts(language)
except Exception:
texts = get_texts()
await callback.answer(
texts.t(
"MENU_PROFILE_UNAVAILABLE",
"❗️ Личный кабинет пока недоступен. Попробуйте позже.",
),
show_alert=True,
)
async def show_service_rules(
callback: types.CallbackQuery,
db_user: User,
@@ -880,12 +899,14 @@ async def handle_back_to_menu(
db_user.telegram_id
)
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
custom_buttons = []
if not settings.is_text_main_menu_mode():
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
await edit_or_answer_photo(
callback=callback,
@@ -903,6 +924,7 @@ async def handle_back_to_menu(
custom_buttons=custom_buttons,
),
parse_mode="HTML",
force_text=settings.is_text_main_menu_mode(),
)
await callback.answer()
@@ -1036,7 +1058,12 @@ def register_handlers(dp: Dispatcher):
handle_back_to_menu,
F.data == "back_to_menu"
)
dp.callback_query.register(
handle_profile_unavailable,
F.data == "menu_profile_unavailable",
)
dp.callback_query.register(
show_service_rules,
F.data == "menu_rules"
+9 -3
View File
@@ -1,7 +1,9 @@
import logging
from decimal import Decimal, ROUND_HALF_UP
from aiogram import Dispatcher, types, F
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.services.payment_service import PaymentService
from app.external.telegram_stars import TelegramStarsService
@@ -114,6 +116,8 @@ async def handle_successful_payment(
if success:
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount)
amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP))
amount_text = settings.format_price(amount_kopeks).replace("", "")
keyboard = await payment_service.build_topup_success_keyboard(user)
@@ -129,7 +133,7 @@ async def handle_successful_payment(
"Спасибо за пополнение! 🚀",
).format(
stars_spent=payment.total_amount,
amount=int(rubles_amount),
amount=amount_text,
transaction_id=transaction_id_short,
),
parse_mode="HTML",
@@ -137,8 +141,10 @@ async def handle_successful_payment(
)
logger.info(
f"✅ Stars платеж успешно обработан: "
f"пользователь {user.id}, {payment.total_amount} звезд → {int(rubles_amount)}"
"✅ Stars платеж успешно обработан: пользователь %s, %s звезд → %s",
user.id,
payment.total_amount,
settings.format_price(amount_kopeks),
)
else:
logger.error(f"Ошибка обработки Stars платежа для пользователя {user.id}")
+40 -30
View File
@@ -335,12 +335,14 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
user.telegram_id
)
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
custom_buttons = []
if not settings.is_text_main_menu_mode():
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
await message.answer(
menu_text,
@@ -757,12 +759,14 @@ async def complete_registration_from_callback(
and SupportSettingsService.is_moderator(existing_user.telegram_id)
)
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
custom_buttons = []
if not settings.is_text_main_menu_mode():
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
try:
await callback.message.answer(
@@ -937,12 +941,14 @@ async def complete_registration_from_callback(
and SupportSettingsService.is_moderator(user.telegram_id)
)
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
custom_buttons = []
if not settings.is_text_main_menu_mode():
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
try:
await callback.message.answer(
@@ -1011,12 +1017,14 @@ async def complete_registration(
and SupportSettingsService.is_moderator(existing_user.telegram_id)
)
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
custom_buttons = []
if not settings.is_text_main_menu_mode():
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
try:
await message.answer(
@@ -1191,12 +1199,14 @@ async def complete_registration(
and SupportSettingsService.is_moderator(user.telegram_id)
)
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
custom_buttons = []
if not settings.is_text_main_menu_mode():
custom_buttons = await MainMenuButtonService.get_buttons_for_user(
db,
is_admin=is_admin,
has_active_subscription=has_active_subscription,
subscription_is_active=subscription_is_active,
)
try:
await message.answer(
+247 -55
View File
@@ -1,7 +1,9 @@
import base64
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Tuple, Optional
from urllib.parse import quote
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
@@ -56,6 +58,7 @@ from app.services.subscription_checkout_service import (
should_offer_checkout_resume,
)
from app.services.subscription_service import SubscriptionService
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
from app.services.promo_offer_service import promo_offer_service
from app.states import SubscriptionStates
from app.utils.pagination import paginate_list
@@ -5485,7 +5488,7 @@ async def claim_discount_offer(
buy_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
build_miniapp_or_callback_button(
text=button_text,
callback_data=button_callback,
)
@@ -5539,6 +5542,17 @@ async def handle_device_guide(
return
featured_app = next((app for app in apps if app.get('isFeatured', False)), apps[0])
featured_app_id = featured_app.get('id')
other_apps = [
app for app in apps
if isinstance(app, dict) and app.get('id') and app.get('id') != featured_app_id
]
other_app_names = ", ".join(
str(app.get('name')).strip()
for app in other_apps
if isinstance(app.get('name'), str) and app.get('name').strip()
)
if hide_subscription_link:
link_section = (
@@ -5556,6 +5570,20 @@ async def handle_device_guide(
+ f"\n<code>{subscription_link}</code>\n\n"
)
installation_description = get_step_description(featured_app, "installationStep", db_user.language)
add_description = get_step_description(featured_app, "addSubscriptionStep", db_user.language)
connect_description = get_step_description(featured_app, "connectAndUseStep", db_user.language)
additional_before_text = format_additional_section(
featured_app.get("additionalBeforeAddSubscriptionStep"),
texts,
db_user.language,
)
additional_after_text = format_additional_section(
featured_app.get("additionalAfterAddSubscriptionStep"),
texts,
db_user.language,
)
guide_text = (
texts.t(
"SUBSCRIPTION_DEVICE_GUIDE_TITLE",
@@ -5566,17 +5594,36 @@ async def handle_device_guide(
+ texts.t(
"SUBSCRIPTION_DEVICE_FEATURED_APP",
"📋 <b>Рекомендуемое приложение:</b> {app_name}",
).format(app_name=featured_app['name'])
+ "\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE", "<b>Шаг 1 - Установка:</b>")
+ f"\n{featured_app['installationStep']['description'][db_user.language]}\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_STEP_ADD_TITLE", "<b>Шаг 2 - Добавление подписки:</b>")
+ f"\n{featured_app['addSubscriptionStep']['description'][db_user.language]}\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE", "<b>Шаг 3 - Подключение:</b>")
+ f"\n{featured_app['connectAndUseStep']['description'][db_user.language]}\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_HOW_TO_TITLE", "💡 <b>Как подключить:</b>")
+ "\n"
+ "\n".join(
).format(app_name=featured_app.get('name', ''))
)
if other_app_names:
guide_text += "\n\n" + texts.t(
"SUBSCRIPTION_DEVICE_OTHER_APPS",
"📦 <b>Другие приложения:</b> {app_list}",
).format(app_list=other_app_names)
guide_text += "\n" + texts.t(
"SUBSCRIPTION_DEVICE_OTHER_APPS_HINT",
"Нажмите кнопку \"Другие приложения\" ниже, чтобы выбрать приложение.",
)
guide_text += "\n\n" + texts.t("SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE", "<b>Шаг 1 - Установка:</b>")
if installation_description:
guide_text += f"\n{installation_description}"
if additional_before_text:
guide_text += f"\n\n{additional_before_text}"
guide_text += "\n\n" + texts.t("SUBSCRIPTION_DEVICE_STEP_ADD_TITLE", "<b>Шаг 2 - Добавление подписки:</b>")
if add_description:
guide_text += f"\n{add_description}"
guide_text += "\n\n" + texts.t("SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE", "<b>Шаг 3 - Подключение:</b>")
if connect_description:
guide_text += f"\n{connect_description}"
guide_text += "\n\n" + texts.t("SUBSCRIPTION_DEVICE_HOW_TO_TITLE", "💡 <b>Как подключить:</b>")
guide_text += "\n" + "\n".join(
[
texts.t(
"SUBSCRIPTION_DEVICE_HOW_TO_STEP1",
@@ -5584,7 +5631,7 @@ async def handle_device_guide(
),
texts.t(
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2",
"2. Скопируйте ссылку подписки (нажмите на неё)",
"2. Нажмите кнопку \"Подключиться\" ниже",
),
texts.t(
"SUBSCRIPTION_DEVICE_HOW_TO_STEP3",
@@ -5596,14 +5643,18 @@ async def handle_device_guide(
),
]
)
)
if additional_after_text:
guide_text += f"\n\n{additional_after_text}"
await callback.message.edit_text(
guide_text,
reply_markup=get_connection_guide_keyboard(
subscription_link,
featured_app,
db_user.language
device_type,
db_user.language,
has_other_apps=bool(other_apps),
),
parse_mode="HTML"
)
@@ -5691,31 +5742,46 @@ async def handle_specific_app_guide(
+ f"\n<code>{subscription_link}</code>\n\n"
)
installation_description = get_step_description(app, "installationStep", db_user.language)
add_description = get_step_description(app, "addSubscriptionStep", db_user.language)
connect_description = get_step_description(app, "connectAndUseStep", db_user.language)
additional_before_text = format_additional_section(
app.get("additionalBeforeAddSubscriptionStep"),
texts,
db_user.language,
)
additional_after_text = format_additional_section(
app.get("additionalAfterAddSubscriptionStep"),
texts,
db_user.language,
)
guide_text = (
texts.t(
"SUBSCRIPTION_SPECIFIC_APP_TITLE",
"📱 <b>{app_name} - {device_name}</b>",
).format(app_name=app['name'], device_name=get_device_name(device_type, db_user.language))
).format(app_name=app.get('name', ''), device_name=get_device_name(device_type, db_user.language))
+ "\n\n"
+ link_section
+ texts.t("SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE", "<b>Шаг 1 - Установка:</b>")
+ f"\n{app['installationStep']['description'][db_user.language]}\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_STEP_ADD_TITLE", "<b>Шаг 2 - Добавление подписки:</b>")
+ f"\n{app['addSubscriptionStep']['description'][db_user.language]}\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE", "<b>Шаг 3 - Подключение:</b>")
+ f"\n{app['connectAndUseStep']['description'][db_user.language]}"
)
if 'additionalAfterAddSubscriptionStep' in app:
additional = app['additionalAfterAddSubscriptionStep']
guide_text += (
"\n\n"
+ texts.t(
"SUBSCRIPTION_ADDITIONAL_STEP_TITLE",
"<b>{title}:</b>",
).format(title=additional['title'][db_user.language])
+ f"\n{additional['description'][db_user.language]}"
)
guide_text += texts.t("SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE", "<b>Шаг 1 - Установка:</b>")
if installation_description:
guide_text += f"\n{installation_description}"
if additional_before_text:
guide_text += f"\n\n{additional_before_text}"
guide_text += "\n\n" + texts.t("SUBSCRIPTION_DEVICE_STEP_ADD_TITLE", "<b>Шаг 2 - Добавление подписки:</b>")
if add_description:
guide_text += f"\n{add_description}"
guide_text += "\n\n" + texts.t("SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE", "<b>Шаг 3 - Подключение:</b>")
if connect_description:
guide_text += f"\n{connect_description}"
if additional_after_text:
guide_text += f"\n\n{additional_after_text}"
await callback.message.edit_text(
guide_text,
@@ -5858,50 +5924,176 @@ def load_app_config() -> Dict[str, Any]:
config_path = settings.get_app_config_path()
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
data = json.load(f)
if isinstance(data, dict):
return data
logger.error("Некорректный формат app-config.json: ожидается объект")
except Exception as e:
logger.error(f"Ошибка загрузки конфига приложений: {e}")
return {}
return {}
def get_localized_value(values: Any, language: str, default_language: str = "en") -> str:
if not isinstance(values, dict):
return ""
candidates: List[str] = []
normalized_language = (language or "").strip().lower()
if normalized_language:
candidates.append(normalized_language)
if "-" in normalized_language:
candidates.append(normalized_language.split("-")[0])
default_language = (default_language or "").strip().lower()
if default_language and default_language not in candidates:
candidates.append(default_language)
for candidate in candidates:
if not candidate:
continue
value = values.get(candidate)
if isinstance(value, str) and value.strip():
return value
for value in values.values():
if isinstance(value, str) and value.strip():
return value
return ""
def get_step_description(app: Dict[str, Any], step_key: str, language: str) -> str:
if not isinstance(app, dict):
return ""
step = app.get(step_key)
if not isinstance(step, dict):
return ""
description = step.get("description")
return get_localized_value(description, language)
def format_additional_section(additional: Any, texts, language: str) -> str:
if not isinstance(additional, dict):
return ""
title = get_localized_value(additional.get("title"), language)
description = get_localized_value(additional.get("description"), language)
parts: List[str] = []
if title:
parts.append(
texts.t(
"SUBSCRIPTION_ADDITIONAL_STEP_TITLE",
"<b>{title}:</b>",
).format(title=title)
)
if description:
parts.append(description)
return "\n".join(parts)
def build_redirect_link(target_link: Optional[str], template: Optional[str]) -> Optional[str]:
if not target_link or not template:
return None
normalized_target = str(target_link).strip()
normalized_template = str(template).strip()
if not normalized_target or not normalized_template:
return None
encoded_target = quote(normalized_target, safe="")
result = normalized_template
replaced = False
replacements = [
("{subscription_link}", encoded_target),
("{link}", encoded_target),
("{subscription_link_raw}", normalized_target),
("{link_raw}", normalized_target),
]
for placeholder, replacement in replacements:
if placeholder in result:
result = result.replace(placeholder, replacement)
replaced = True
if not replaced:
result = f"{result}{encoded_target}"
return result
def get_apps_for_device(device_type: str, language: str = "ru") -> List[Dict[str, Any]]:
config = load_app_config()['platforms']
config = load_app_config()
platforms = config.get("platforms", {}) if isinstance(config, dict) else {}
if not isinstance(platforms, dict):
return []
device_mapping = {
'ios': 'ios',
'android': 'android',
'windows': 'windows',
'mac': 'macos',
'tv': 'androidTV'
'tv': 'androidTV',
'appletv': 'appleTV',
'apple_tv': 'appleTV',
}
config_key = device_mapping.get(device_type, device_type)
return config.get(config_key, [])
apps = platforms.get(config_key, [])
return apps if isinstance(apps, list) else []
def get_device_name(device_type: str, language: str = "ru") -> str:
if language == "en":
names = {
'ios': 'iPhone/iPad',
'android': 'Android',
'windows': 'Windows',
'mac': 'macOS',
'tv': 'Android TV'
}
else:
names = {
'ios': 'iPhone/iPad',
'android': 'Android',
'windows': 'Windows',
'mac': 'macOS',
'tv': 'Android TV'
}
names = {
'ios': 'iPhone/iPad',
'android': 'Android',
'windows': 'Windows',
'mac': 'macOS',
'tv': 'Android TV',
'appletv': 'Apple TV',
'apple_tv': 'Apple TV',
}
return names.get(device_type, device_type)
def create_deep_link(app: Dict[str, Any], subscription_url: str) -> str:
return subscription_url
def create_deep_link(app: Dict[str, Any], subscription_url: str) -> Optional[str]:
if not subscription_url:
return None
if not isinstance(app, dict):
return subscription_url
scheme = str(app.get("urlScheme", "")).strip()
payload = subscription_url
if app.get("isNeedBase64Encoding"):
try:
payload = base64.b64encode(subscription_url.encode("utf-8")).decode("utf-8")
except Exception as exc:
logger.warning(
"Не удалось закодировать ссылку подписки в base64 для приложения %s: %s",
app.get("id"),
exc,
)
payload = subscription_url
scheme_link = f"{scheme}{payload}" if scheme else None
template = settings.get_happ_cryptolink_redirect_template()
redirect_link = build_redirect_link(scheme_link, template) if scheme_link and template else None
return redirect_link or scheme_link or subscription_url
def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
+6
View File
@@ -674,6 +674,12 @@ def get_admin_remnawave_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
callback_data="admin_rw_squads"
)
],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_REMNAWAVE_MIGRATION", "🚚 Переезд"),
callback_data="admin_rw_migration"
)
],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_REMNAWAVE_TRAFFIC", "📈 Трафик"),
+229 -49
View File
@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
from app.localization.loader import DEFAULT_LANGUAGE
from app.localization.texts import get_texts
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
from app.utils.pricing_utils import format_period_description, apply_percentage_discount
from app.utils.subscription_utils import (
get_display_subscription_link,
@@ -17,6 +18,64 @@ import logging
logger = logging.getLogger(__name__)
def _get_localized_value(values, language: str, default_language: str = "en") -> str:
if not isinstance(values, dict):
return ""
candidates = []
normalized_language = (language or "").strip().lower()
if normalized_language:
candidates.append(normalized_language)
if "-" in normalized_language:
candidates.append(normalized_language.split("-")[0])
default_language = (default_language or "").strip().lower()
if default_language and default_language not in candidates:
candidates.append(default_language)
for candidate in candidates:
if not candidate:
continue
value = values.get(candidate)
if isinstance(value, str) and value.strip():
return value
for value in values.values():
if isinstance(value, str) and value.strip():
return value
return ""
def _build_additional_buttons(additional_section, language: str) -> List[InlineKeyboardButton]:
if not isinstance(additional_section, dict):
return []
buttons = additional_section.get("buttons")
if not isinstance(buttons, list):
return []
localized_buttons: List[InlineKeyboardButton] = []
for button in buttons:
if not isinstance(button, dict):
continue
button_text = _get_localized_value(button.get("buttonText"), language)
button_link = button.get("buttonLink")
if not button_text or not button_link:
continue
localized_buttons.append(
InlineKeyboardButton(text=button_text, url=button_link)
)
return localized_buttons
_LANGUAGE_DISPLAY_NAMES = {
"ru": "🇷🇺 Русский",
"en": "🇬🇧 English",
@@ -110,6 +169,59 @@ def get_language_selection_keyboard(
return InlineKeyboardMarkup(inline_keyboard=buttons)
def _build_text_main_menu_keyboard(
language: str,
texts,
*,
is_admin: bool,
is_moderator: bool,
) -> InlineKeyboardMarkup:
profile_text = texts.t("MENU_PROFILE", "👤 Личный кабинет")
miniapp_url = settings.get_main_menu_miniapp_url()
if miniapp_url:
profile_button = InlineKeyboardButton(
text=profile_text,
web_app=types.WebAppInfo(url=miniapp_url),
)
else:
profile_button = InlineKeyboardButton(
text=profile_text,
callback_data="menu_profile_unavailable",
)
keyboard_rows: List[List[InlineKeyboardButton]] = [[profile_button]]
if settings.is_language_selection_enabled():
keyboard_rows.append([
InlineKeyboardButton(text=texts.MENU_LANGUAGE, callback_data="menu_language")
])
support_enabled = False
try:
from app.services.support_settings_service import SupportSettingsService
support_enabled = SupportSettingsService.is_support_menu_enabled()
except Exception:
support_enabled = settings.SUPPORT_MENU_ENABLED
if support_enabled:
keyboard_rows.append([
InlineKeyboardButton(text=texts.MENU_SUPPORT, callback_data="menu_support")
])
if is_admin:
keyboard_rows.append([
InlineKeyboardButton(text=texts.MENU_ADMIN, callback_data="admin_panel")
])
elif is_moderator:
keyboard_rows.append([
InlineKeyboardButton(text="🧑‍⚖️ Модерация", callback_data="moderator_panel")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
def get_main_menu_keyboard(
language: str = DEFAULT_LANGUAGE,
is_admin: bool = False,
@@ -124,6 +236,14 @@ def get_main_menu_keyboard(
custom_buttons: Optional[list[InlineKeyboardButton]] = None,
) -> InlineKeyboardMarkup:
texts = get_texts(language)
if settings.is_text_main_menu_mode():
return _build_text_main_menu_keyboard(
language,
texts,
is_admin=is_admin,
is_moderator=is_moderator,
)
if settings.DEBUG:
print(f"DEBUG KEYBOARD: language={language}, is_admin={is_admin}, has_had_paid={has_had_paid_subscription}, has_active={has_active_subscription}, sub_active={subscription_is_active}, balance={balance_kopeks}")
@@ -1013,17 +1133,17 @@ def get_yookassa_payment_keyboard(
def get_autopay_notification_keyboard(subscription_id: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text=texts.t("TOPUP_BALANCE_BUTTON", "💳 Пополнить баланс"),
build_miniapp_or_callback_button(
text=texts.t("TOPUP_BALANCE_BUTTON", "💳 Пополнить баланс"),
callback_data="balance_topup"
)
],
[
InlineKeyboardButton(
text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"),
build_miniapp_or_callback_button(
text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"),
callback_data="menu_subscription"
)
]
@@ -1031,23 +1151,23 @@ def get_autopay_notification_keyboard(subscription_id: int, language: str = DEFA
def get_subscription_expiring_keyboard(subscription_id: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text=texts.MENU_EXTEND_SUBSCRIPTION,
build_miniapp_or_callback_button(
text=texts.MENU_EXTEND_SUBSCRIPTION,
callback_data="subscription_extend"
)
],
[
InlineKeyboardButton(
text=texts.t("TOPUP_BALANCE_BUTTON", "💳 Пополнить баланс"),
build_miniapp_or_callback_button(
text=texts.t("TOPUP_BALANCE_BUTTON", "💳 Пополнить баланс"),
callback_data="balance_topup"
)
],
[
InlineKeyboardButton(
text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"),
build_miniapp_or_callback_button(
text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"),
callback_data="menu_subscription"
)
]
@@ -1547,7 +1667,8 @@ def get_device_selection_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKey
InlineKeyboardButton(text=texts.t("DEVICE_GUIDE_MAC", "🎯 macOS"), callback_data="device_guide_mac")
],
[
InlineKeyboardButton(text=texts.t("DEVICE_GUIDE_ANDROID_TV", "📺 Android TV"), callback_data="device_guide_tv")
InlineKeyboardButton(text=texts.t("DEVICE_GUIDE_ANDROID_TV", "📺 Android TV"), callback_data="device_guide_tv"),
InlineKeyboardButton(text=texts.t("DEVICE_GUIDE_APPLE_TV", "📺 Apple TV"), callback_data="device_guide_appletv")
]
]
@@ -1564,42 +1685,80 @@ def get_device_selection_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKey
def get_connection_guide_keyboard(
subscription_url: str,
app: dict,
language: str = DEFAULT_LANGUAGE
subscription_url: str,
app: dict,
device_type: str,
language: str = DEFAULT_LANGUAGE,
has_other_apps: bool = False,
) -> InlineKeyboardMarkup:
from app.handlers.subscription import create_deep_link
texts = get_texts(language)
keyboard = []
if 'installationStep' in app and 'buttons' in app['installationStep']:
app_buttons = []
for button in app['installationStep']['buttons']:
button_text = button['buttonText'].get(language, button['buttonText']['en'])
button_text = _get_localized_value(button.get('buttonText'), language)
button_link = button.get('buttonLink')
if not button_text or not button_link:
continue
app_buttons.append(
InlineKeyboardButton(text=f"📥 {button_text}", url=button['buttonLink'])
InlineKeyboardButton(text=f"📥 {button_text}", url=button_link)
)
if len(app_buttons) == 2:
keyboard.append(app_buttons)
app_buttons = []
if app_buttons:
keyboard.append(app_buttons)
if settings.is_happ_cryptolink_mode():
copy_button = InlineKeyboardButton(
text=texts.t("COPY_SUBSCRIPTION_LINK", "📋 Скопировать ссылку подписки"),
additional_before_buttons = _build_additional_buttons(
app.get('additionalBeforeAddSubscriptionStep'),
language,
)
for button in additional_before_buttons:
keyboard.append([button])
connect_link = create_deep_link(app, subscription_url)
if connect_link:
connect_button = InlineKeyboardButton(
text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
url=connect_link,
)
elif settings.is_happ_cryptolink_mode():
connect_button = InlineKeyboardButton(
text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
callback_data="open_subscription_link",
)
else:
copy_button = InlineKeyboardButton(
text=texts.t("COPY_SUBSCRIPTION_LINK", "📋 Скопировать ссылку подписки"),
connect_button = InlineKeyboardButton(
text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
url=subscription_url,
)
keyboard.append([copy_button])
keyboard.append([connect_button])
additional_after_buttons = _build_additional_buttons(
app.get('additionalAfterAddSubscriptionStep'),
language,
)
for button in additional_after_buttons:
keyboard.append([button])
if has_other_apps:
keyboard.append([
InlineKeyboardButton(
text=texts.t("OTHER_APPS_BUTTON", "📋 Другие приложения"),
callback_data=f"app_list_{device_type}",
)
])
keyboard.extend([
[
InlineKeyboardButton(text=texts.t("CHOOSE_ANOTHER_DEVICE", "📱 Выбрать другое устройство"), callback_data="subscription_connect")
@@ -1652,43 +1811,64 @@ def get_specific_app_keyboard(
) -> InlineKeyboardMarkup:
from app.handlers.subscription import create_deep_link
texts = get_texts(language)
keyboard = []
if 'installationStep' in app and 'buttons' in app['installationStep']:
app_buttons = []
for button in app['installationStep']['buttons']:
button_text = button['buttonText'].get(language, button['buttonText']['en'])
button_text = _get_localized_value(button.get('buttonText'), language)
button_link = button.get('buttonLink')
if not button_text or not button_link:
continue
app_buttons.append(
InlineKeyboardButton(text=f"📥 {button_text}", url=button['buttonLink'])
InlineKeyboardButton(text=f"📥 {button_text}", url=button_link)
)
if len(app_buttons) == 2:
keyboard.append(app_buttons)
app_buttons = []
if app_buttons:
keyboard.append(app_buttons)
if settings.is_happ_cryptolink_mode():
copy_button = InlineKeyboardButton(
text=texts.t("COPY_SUBSCRIPTION_LINK", "📋 Скопировать ссылку подписки"),
additional_before_buttons = _build_additional_buttons(
app.get('additionalBeforeAddSubscriptionStep'),
language,
)
for button in additional_before_buttons:
keyboard.append([button])
connect_link = create_deep_link(app, subscription_url)
if connect_link:
connect_button = InlineKeyboardButton(
text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
url=connect_link,
)
elif settings.is_happ_cryptolink_mode():
connect_button = InlineKeyboardButton(
text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
callback_data="open_subscription_link",
)
else:
copy_button = InlineKeyboardButton(
text=texts.t("COPY_SUBSCRIPTION_LINK", "📋 Скопировать ссылку подписки"),
connect_button = InlineKeyboardButton(
text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
url=subscription_url,
)
keyboard.append([copy_button])
if 'additionalAfterAddSubscriptionStep' in app and 'buttons' in app['additionalAfterAddSubscriptionStep']:
for button in app['additionalAfterAddSubscriptionStep']['buttons']:
button_text = button['buttonText'].get(language, button['buttonText']['en'])
keyboard.append([
InlineKeyboardButton(text=button_text, url=button['buttonLink'])
])
keyboard.append([connect_button])
additional_after_buttons = _build_additional_buttons(
app.get('additionalAfterAddSubscriptionStep'),
language,
)
for button in additional_after_buttons:
keyboard.append([button])
keyboard.extend([
[
InlineKeyboardButton(text=texts.t("OTHER_APPS_BUTTON", "📋 Другие приложения"), callback_data=f"app_list_{device_type}")
+7 -1
View File
@@ -5,6 +5,7 @@
"COUNTRY_MANAGEMENT_NONE": "No countries connected",
"PAID_FEATURE_ONLY": "⚠ This feature is available only for paid subscriptions",
"PAID_FEATURE_ONLY_SHORT": "⚠ Paid subscriptions only",
"SUSPICIOUS_DISPLAY_NAME_BLOCKED": "🚫 We detected that your display name looks like a link or an official account. Please change your Telegram name and try again.",
"COUNTRY_NOT_AVAILABLE_PROMOGROUP": "❌ This server is not available for your promo group",
"COUNTRY_CHANGES_NOT_FOUND": "⚠️ No changes detected",
"COUNTRY_CHANGES_SUCCESS_HEADER": "✅ <b>Countries updated!</b>\n\n",
@@ -58,6 +59,7 @@
"DEVICE_CONNECTION_HELP": "❓ How to reconnect a device?",
"DEVICE_GUIDE_ANDROID": "🤖 Android",
"DEVICE_GUIDE_ANDROID_TV": "📺 Android TV",
"DEVICE_GUIDE_APPLE_TV": "📺 Apple TV",
"DEVICE_GUIDE_IOS": "📱 iOS (iPhone/iPad)",
"DEVICE_GUIDE_MAC": "🎯 macOS",
"DEVICE_GUIDE_WINDOWS": "💻 Windows",
@@ -395,6 +397,8 @@
"TRAFFIC_ALREADY_UNLIMITED": "⚠ You already have unlimited traffic",
"ADD_TRAFFIC_PROMPT": "📈 <b>Add traffic to your subscription</b>\n\nCurrent limit: {current_traffic}\nChoose extra traffic:",
"USER_NOT_FOUND": "❌ User not found",
"MENU_PROFILE": "👤 Personal account",
"MENU_PROFILE_UNAVAILABLE": "❗️ Personal account is not available yet. Please try again later.",
"MENU_LANGUAGE": "🌐 Language",
"SUBSCRIPTION_STATUS_EXPIRED": "Expired",
"SUBSCRIPTION_STATUS_TRIAL": "Trial",
@@ -434,12 +438,14 @@
"SUBSCRIPTION_DEVICE_GUIDE_TITLE": "📱 <b>Setup for {device_name}</b>",
"SUBSCRIPTION_DEVICE_LINK_TITLE": "🔗 <b>Subscription link:</b>",
"SUBSCRIPTION_DEVICE_FEATURED_APP": "📋 <b>Recommended app:</b> {app_name}",
"SUBSCRIPTION_DEVICE_OTHER_APPS": "📦 <b>Other apps:</b> {app_list}",
"SUBSCRIPTION_DEVICE_OTHER_APPS_HINT": "Tap the \"Other apps\" button below to choose another app.",
"SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "<b>Step 1 - Install:</b>",
"SUBSCRIPTION_DEVICE_STEP_ADD_TITLE": "<b>Step 2 - Add subscription:</b>",
"SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "<b>Step 3 - Connect:</b>",
"SUBSCRIPTION_DEVICE_HOW_TO_TITLE": "💡 <b>How to connect:</b>",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP1": "1. Install the app from the link above",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Copy the subscription link (tap on it)",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Tap the \"Connect\" button below",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP3": "3. Open the app and paste the link",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP4": "4. Connect to a server",
"SUBSCRIPTION_APPS_TITLE": "📱 <b>Apps for {device_name}</b>",
+7 -1
View File
@@ -6,6 +6,7 @@
"COUNTRY_MANAGEMENT_NONE": "Нет подключенных стран",
"PAID_FEATURE_ONLY": "⚠ Эта функция доступна только для платных подписок",
"PAID_FEATURE_ONLY_SHORT": "⚠ Только для платных подписок",
"SUSPICIOUS_DISPLAY_NAME_BLOCKED": "🚫 Мы обнаружили, что ваше отображаемое имя похоже на ссылку или служебный аккаунт. Пожалуйста, измените имя в профиле Telegram и повторите попытку.",
"COUNTRY_NOT_AVAILABLE_PROMOGROUP": "❌ Сервер недоступен для вашей промогруппы",
"COUNTRY_CHANGES_NOT_FOUND": "⚠️ Изменения не обнаружены",
"COUNTRY_CHANGES_SUCCESS_HEADER": "✅ <b>Страны успешно обновлены!</b>\n\n",
@@ -183,6 +184,7 @@
"DEVICE_CONNECTION_HELP": "❓ Как подключить устройство заново?",
"DEVICE_GUIDE_ANDROID": "🤖 Android",
"DEVICE_GUIDE_ANDROID_TV": "📺 Android TV",
"DEVICE_GUIDE_APPLE_TV": "📺 Apple TV",
"DEVICE_GUIDE_IOS": "📱 iOS (iPhone/iPad)",
"DEVICE_GUIDE_MAC": "🎯 macOS",
"DEVICE_GUIDE_WINDOWS": "💻 Windows",
@@ -254,6 +256,8 @@
"PUBLIC_OFFER_EMPTY_ALERT": "Публичная оферта ещё не заполнена.",
"PUBLIC_OFFER_HEADER": "📄 <b>Публичная оферта</b>",
"PUBLIC_OFFER_PAGE_INFO": "Страница {current} из {total}",
"MENU_PROFILE": "👤 Личный кабинет",
"MENU_PROFILE_UNAVAILABLE": "❗️ Личный кабинет пока недоступен. Попробуйте позже.",
"MENU_LANGUAGE": "🌐 Язык",
"MENU_PROMOCODE": "🎫 Промокод",
"MENU_REFERRALS": "🤝 Партнерка",
@@ -444,12 +448,14 @@
"SUBSCRIPTION_DEVICE_GUIDE_TITLE": "📱 <b>Настройка для {device_name}</b>",
"SUBSCRIPTION_DEVICE_LINK_TITLE": "🔗 <b>Ссылка подписки:</b>",
"SUBSCRIPTION_DEVICE_FEATURED_APP": "📋 <b>Рекомендуемое приложение:</b> {app_name}",
"SUBSCRIPTION_DEVICE_OTHER_APPS": "📦 <b>Другие приложения:</b> {app_list}",
"SUBSCRIPTION_DEVICE_OTHER_APPS_HINT": "Нажмите кнопку \"Другие приложения\" ниже, чтобы выбрать приложение.",
"SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "<b>Шаг 1 - Установка:</b>",
"SUBSCRIPTION_DEVICE_STEP_ADD_TITLE": "<b>Шаг 2 - Добавление подписки:</b>",
"SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "<b>Шаг 3 - Подключение:</b>",
"SUBSCRIPTION_DEVICE_HOW_TO_TITLE": "💡 <b>Как подключить:</b>",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP1": "1. Установите приложение по ссылке выше",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Скопируйте ссылку подписки (нажмите на неё)",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Нажмите кнопку \"Подключиться\" ниже",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP3": "3. Откройте приложение и вставьте ссылку",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP4": "4. Подключитесь к серверу",
"SUBSCRIPTION_APPS_TITLE": "📱 <b>Приложения для {device_name}</b>",
+159
View File
@@ -0,0 +1,159 @@
import logging
import re
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import (
CallbackQuery,
Message,
PreCheckoutQuery,
TelegramObject,
User as TgUser,
)
from app.config import settings
from app.localization.texts import get_texts
logger = logging.getLogger(__name__)
ZERO_WIDTH_PATTERN = re.compile(r"[\u200B-\u200D\uFEFF]")
LINK_PATTERNS = [
re.compile(pattern, re.IGNORECASE)
for pattern in (
r"t\.me/\+",
r"joinchat",
r"https?://",
r"www\.",
r"tg://",
r"telegram\.me",
r"t\.me",
)
]
DOMAIN_OBFUSCATION_PATTERN = re.compile(
r"(?<![0-9a-zа-яё])(?:t|т)[\s\W_]*?(?:m|м)(?:e|е)",
re.IGNORECASE,
)
CHAR_TRANSLATION = str.maketrans({
"а": "a",
"е": "e",
"о": "o",
"р": "p",
"с": "c",
"х": "x",
"у": "y",
"к": "k",
"т": "t",
"г": "g",
"м": "m",
"н": "n",
"л": "l",
"і": "i",
"ї": "i",
"ё": "e",
"ь": "",
"ъ": "",
"ў": "u",
"": "@",
})
COLLAPSE_PATTERN = re.compile(r"[\s\._\-/\\|,:;•·﹒․⋅··`~'\"!?()\[\]{}<>+=]+")
class DisplayNameRestrictionMiddleware(BaseMiddleware):
"""Blocks users whose display name imitates links or official accounts."""
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
user: TgUser | None = None
if isinstance(event, (Message, CallbackQuery, PreCheckoutQuery)):
user = event.from_user
if not user or user.is_bot:
return await handler(event, data)
display_name = self._build_display_name(user)
username = user.username or ""
display_suspicious = self._is_suspicious(display_name)
username_suspicious = self._is_suspicious(username)
if display_suspicious or username_suspicious:
suspicious_value = display_name if display_suspicious else username
language = self._resolve_language(user, data)
texts = get_texts(language)
warning = texts.get(
"SUSPICIOUS_DISPLAY_NAME_BLOCKED",
"🚫 Ваше отображаемое имя похоже на ссылку или служебный аккаунт. "
"Пожалуйста, измените имя и попробуйте снова.",
)
logger.warning(
"🚫 DisplayNameRestriction: user %s blocked due to suspicious name '%s'",
user.id,
suspicious_value,
)
if isinstance(event, Message):
await event.answer(warning)
elif isinstance(event, CallbackQuery):
await event.answer(warning, show_alert=True)
elif isinstance(event, PreCheckoutQuery):
await event.answer(ok=False, error_message=warning)
return None
return await handler(event, data)
@staticmethod
def _build_display_name(user: TgUser) -> str:
parts = [user.first_name or "", user.last_name or ""]
return " ".join(part for part in parts if part).strip()
@staticmethod
def _resolve_language(user: TgUser, data: Dict[str, Any]) -> str:
db_user = data.get("db_user")
if db_user and getattr(db_user, "language", None):
return db_user.language
language_code = getattr(user, "language_code", None)
return language_code or settings.DEFAULT_LANGUAGE
def _is_suspicious(self, value: str) -> bool:
if not value:
return False
cleaned = ZERO_WIDTH_PATTERN.sub("", value)
lower_value = cleaned.lower()
if "@" in cleaned or "" in cleaned:
return True
if any(pattern.search(lower_value) for pattern in LINK_PATTERNS):
return True
if DOMAIN_OBFUSCATION_PATTERN.search(lower_value):
return True
normalized = self._normalize_text(lower_value)
collapsed = COLLAPSE_PATTERN.sub("", normalized)
if "tme" in collapsed:
return True
banned_keywords = settings.get_display_name_banned_keywords()
return any(
keyword in normalized or keyword in collapsed
for keyword in banned_keywords
)
@staticmethod
def _normalize_text(value: str) -> str:
return value.translate(CHAR_TRANSLATION)
+6 -20
View File
@@ -3,7 +3,6 @@ from typing import Optional, Dict, Any, List
from datetime import datetime
from aiogram import Bot, types
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -345,35 +344,22 @@ class AdminNotificationService:
async def send_balance_topup_notification(
self,
db: AsyncSession,
user: User,
transaction: Transaction,
old_balance: int
old_balance: int,
*,
topup_status: str,
referrer_info: str,
subscription: Subscription | None,
promo_group: PromoGroup | None,
) -> bool:
if not self._is_enabled():
return False
try:
deposit_count_result = await db.execute(
select(func.count())
.select_from(Transaction)
.where(
Transaction.user_id == user.id,
Transaction.type == TransactionType.DEPOSIT.value,
Transaction.is_completed.is_(True)
)
)
deposit_count = deposit_count_result.scalar_one() or 0
topup_status = "🆕 Первое пополнение" if deposit_count <= 1 else "🔄 Пополнение"
payment_method = self._get_payment_method_display(transaction.payment_method)
balance_change = user.balance_kopeks - old_balance
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
subscription_result = await db.execute(
select(Subscription).where(Subscription.user_id == user.id)
)
subscription = subscription_result.scalar_one_or_none()
subscription_status = self._get_subscription_status(subscription)
promo_group = await self._get_user_promo_group(db, user)
promo_block = self._format_promo_group_block(promo_group)
message = f"""💰 <b>ПОПОЛНЕНИЕ БАЛАНСА</b>
+40 -21
View File
@@ -45,6 +45,7 @@ from app.services.payment_service import PaymentService
from app.services.subscription_service import SubscriptionService
from app.services.promo_offer_service import promo_offer_service
from app.utils.pricing_utils import apply_percentage_discount
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
from app.external.remnawave_api import (
RemnaWaveAPIError,
@@ -977,10 +978,10 @@ class MonitoringService:
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="menu_buy")],
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")]
[build_miniapp_or_callback_button(text="💎 Купить подписку", callback_data="menu_buy")],
[build_miniapp_or_callback_button(text="💳 Пополнить баланс", callback_data="balance_topup")],
])
await self._send_message_with_logo(
@@ -1033,11 +1034,11 @@ class MonitoringService:
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⏰ Продлить подписку", callback_data="subscription_extend")],
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")],
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")]
[build_miniapp_or_callback_button(text="⏰ Продлить подписку", callback_data="subscription_extend")],
[build_miniapp_or_callback_button(text="💳 Пополнить баланс", callback_data="balance_topup")],
[build_miniapp_or_callback_button(text="📱 Моя подписка", callback_data="menu_subscription")],
])
await self._send_message_with_logo(
@@ -1087,10 +1088,10 @@ class MonitoringService:
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="menu_buy")],
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="balance_topup")]
[build_miniapp_or_callback_button(text="💎 Купить подписку", callback_data="menu_buy")],
[build_miniapp_or_callback_button(text="💰 Пополнить баланс", callback_data="balance_topup")],
])
await self._send_message_with_logo(
@@ -1147,8 +1148,14 @@ class MonitoringService:
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), callback_data="subscription_connect")],
[InlineKeyboardButton(text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"), callback_data="menu_subscription")],
[build_miniapp_or_callback_button(
text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
callback_data="subscription_connect",
)],
[build_miniapp_or_callback_button(
text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"),
callback_data="menu_subscription",
)],
[InlineKeyboardButton(text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), callback_data="menu_support")],
])
@@ -1258,8 +1265,14 @@ class MonitoringService:
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), callback_data="subscription_extend")],
[InlineKeyboardButton(text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"), callback_data="balance_topup")],
[build_miniapp_or_callback_button(
text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"),
callback_data="subscription_extend",
)],
[build_miniapp_or_callback_button(
text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"),
callback_data="balance_topup",
)],
[InlineKeyboardButton(text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), callback_data="menu_support")],
])
@@ -1329,9 +1342,15 @@ class MonitoringService:
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🎁 Получить скидку", callback_data=f"claim_discount_{offer_id}")],
[InlineKeyboardButton(text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), callback_data="subscription_extend")],
[InlineKeyboardButton(text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"), callback_data="balance_topup")],
[build_miniapp_or_callback_button(text="🎁 Получить скидку", callback_data=f"claim_discount_{offer_id}")],
[build_miniapp_or_callback_button(
text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"),
callback_data="subscription_extend",
)],
[build_miniapp_or_callback_button(
text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"),
callback_data="balance_topup",
)],
[InlineKeyboardButton(text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), callback_data="menu_support")],
])
@@ -1394,11 +1413,11 @@ class MonitoringService:
required=settings.format_price(required)
)
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.types import InlineKeyboardMarkup
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")],
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")]
[build_miniapp_or_callback_button(text="💳 Пополнить баланс", callback_data="balance_topup")],
[build_miniapp_or_callback_button(text="📱 Моя подписка", callback_data="menu_subscription")],
])
await self._send_message_with_logo(
+155 -41
View File
@@ -2,7 +2,7 @@ import logging
import hashlib
import hmac
import uuid
from decimal import Decimal, InvalidOperation
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, ROUND_FLOOR
from typing import Optional, Dict, Any
from datetime import datetime
from aiogram import Bot
@@ -24,12 +24,14 @@ from app.external.cryptobot import CryptoBotService
from app.utils.currency_converter import currency_converter
from app.database.database import get_db
from app.localization.texts import get_texts
from app.utils.user_utils import format_referrer_info
from app.services.subscription_checkout_service import (
has_subscription_checkout_draft,
should_offer_checkout_resume,
)
from app.services.mulenpay_service import MulenPayService
from app.services.pal24_service import Pal24Service, Pal24APIError
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
from app.database.crud.mulenpay import (
create_mulenpay_payment,
get_mulenpay_payment_by_local_id,
@@ -70,7 +72,7 @@ class PaymentService:
and user.subscription.is_active
)
first_button = InlineKeyboardButton(
first_button = build_miniapp_or_callback_button(
text=(
texts.MENU_EXTEND_SUBSCRIPTION
if has_active_subscription
@@ -87,14 +89,14 @@ class PaymentService:
draft_exists = await has_subscription_checkout_draft(user.id)
if should_offer_checkout_resume(user, draft_exists):
keyboard_rows.append([
InlineKeyboardButton(
build_miniapp_or_callback_button(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="subscription_resume_checkout",
)
])
keyboard_rows.append([
InlineKeyboardButton(text="💰 Мой баланс", callback_data="menu_balance")
build_miniapp_or_callback_button(text="💰 Мой баланс", callback_data="menu_balance")
])
keyboard_rows.append([
InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_menu")
@@ -106,16 +108,32 @@ class PaymentService:
self,
amount_kopeks: int,
description: str,
payload: Optional[str] = None
payload: Optional[str] = None,
*,
stars_amount: Optional[int] = None,
) -> str:
if not self.bot or not self.stars_service:
raise ValueError("Bot instance required for Stars payments")
try:
amount_rubles = amount_kopeks / 100
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
amount_rubles = Decimal(amount_kopeks) / Decimal(100)
if stars_amount is None:
rate = Decimal(str(settings.get_stars_rate()))
if rate <= 0:
raise ValueError("Stars rate must be positive")
normalized_stars = (amount_rubles / rate).to_integral_value(
rounding=ROUND_FLOOR
)
stars_amount = int(normalized_stars)
if stars_amount <= 0:
stars_amount = 1
if stars_amount <= 0:
raise ValueError("Stars amount must be positive")
invoice_link = await self.bot.create_invoice_link(
title="Пополнение баланса VPN",
description=f"{description} (≈{stars_amount} ⭐)",
@@ -125,7 +143,11 @@ class PaymentService:
prices=[LabeledPrice(label="Пополнение", amount=stars_amount)]
)
logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽)")
logger.info(
"Создан Stars invoice на %s звезд (~%s)",
stars_amount,
settings.format_price(amount_kopeks),
)
return invoice_link
except Exception as e:
@@ -142,7 +164,7 @@ class PaymentService:
) -> bool:
try:
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(stars_amount)
amount_kopeks = int(rubles_amount * 100)
amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP))
transaction = await create_transaction(
db=db,
@@ -158,16 +180,21 @@ class PaymentService:
user = await get_user_by_id(db, user_id)
if user:
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
user.balance_kopeks += amount_kopeks
user.updated_at = datetime.utcnow()
promo_group = getattr(user, "promo_group", None)
subscription = getattr(user, "subscription", None)
referrer_info = format_referrer_info(user)
topup_status = "🆕 Первое пополнение" if was_first_topup else "🔄 Пополнение"
await db.commit()
await db.refresh(user)
logger.info(f"💰 Баланс пользователя {user.telegram_id} изменен: {old_balance}{user.balance_kopeks} (изменение: +{amount_kopeks})")
description_for_referral = f"Пополнение Stars: {int(rubles_amount)}₽ ({stars_amount} ⭐)"
description_for_referral = (
f"Пополнение Stars: {settings.format_price(amount_kopeks)} ({stars_amount})"
)
logger.info(f"🔍 Проверка реферальной логики для описания: '{description_for_referral}'")
if any(word in description_for_referral.lower() for word in ["пополнение", "stars", "yookassa", "topup"]) and not any(word in description_for_referral.lower() for word in ["комиссия", "бонус"]):
@@ -179,13 +206,27 @@ class PaymentService:
logger.error(f"Ошибка обработки реферального пополнения: {e}")
else:
logger.info(f"❌ Описание '{description_for_referral}' не подходит для реферальной логики")
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
await db.commit()
await db.refresh(user)
logger.info(f"💰 Баланс пользователя {user.telegram_id} изменен: {old_balance}{user.balance_kopeks} (изменение: +{amount_kopeks})")
if self.bot:
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
db, user, transaction, old_balance
user,
transaction,
old_balance,
topup_status=topup_status,
referrer_info=referrer_info,
subscription=subscription,
promo_group=promo_group,
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
@@ -206,14 +247,18 @@ class PaymentService:
reply_markup=keyboard,
)
logger.info(
f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {int(rubles_amount)}"
"✅ Отправлено уведомление пользователю %s о пополнении на %s",
user.telegram_id,
settings.format_price(amount_kopeks),
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
logger.info(
f"✅ Обработан Stars платеж: пользователь {user_id}, "
f"{stars_amount} звезд → {int(rubles_amount)}"
"✅ Обработан Stars платеж: пользователь %s, %s звезд → %s",
user_id,
stars_amount,
settings.format_price(amount_kopeks),
)
return True
else:
@@ -449,25 +494,42 @@ class PaymentService:
user = await get_user_by_id(db, updated_payment.user_id)
if user:
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
user.balance_kopeks += updated_payment.amount_kopeks
user.updated_at = datetime.utcnow()
promo_group = getattr(user, "promo_group", None)
subscription = getattr(user, "subscription", None)
referrer_info = format_referrer_info(user)
topup_status = "🆕 Первое пополнение" if was_first_topup else "🔄 Пополнение"
await db.commit()
await db.refresh(user)
try:
from app.services.referral_service import process_referral_topup
await process_referral_topup(db, user.id, updated_payment.amount_kopeks, self.bot)
except Exception as e:
logger.error(f"Ошибка обработки реферального пополнения YooKassa: {e}")
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
await db.commit()
await db.refresh(user)
if self.bot:
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
db, user, transaction, old_balance
user,
transaction,
old_balance,
topup_status=topup_status,
referrer_info=referrer_info,
subscription=subscription,
promo_group=promo_group,
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении YooKassa: {e}")
@@ -818,6 +880,7 @@ class PaymentService:
language: str,
ttl_seconds: Optional[int] = None,
payer_email: Optional[str] = None,
payment_method: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
if not self.pal24_service or not self.pal24_service.is_configured:
@@ -848,6 +911,8 @@ class PaymentService:
"language": language,
}
normalized_payment_method = (payment_method or "SBP").upper()
try:
response = await self.pal24_service.create_bill(
amount_kopeks=amount_kopeks,
@@ -857,7 +922,7 @@ class PaymentService:
ttl_seconds=ttl_seconds,
custom_payload=custom_payload,
payer_email=payer_email,
payment_method="SBP",
payment_method=normalized_payment_method,
)
except Pal24APIError as error:
logger.error("Ошибка Pal24 API при создании счета: %s", error)
@@ -952,6 +1017,7 @@ class PaymentService:
"sbp_url": transfer_url or primary_link,
"card_url": card_url,
"transfer_url": transfer_url,
"payment_method": normalized_payment_method,
}
logger.info(
@@ -1045,11 +1111,17 @@ class PaymentService:
return False
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
user.balance_kopeks += payment.amount_kopeks
user.updated_at = datetime.utcnow()
promo_group = getattr(user, "promo_group", None)
subscription = getattr(user, "subscription", None)
referrer_info = format_referrer_info(user)
topup_status = "🆕 Первое пополнение" if was_first_topup else "🔄 Пополнение"
await db.commit()
await db.refresh(user)
try:
from app.services.referral_service import process_referral_topup
@@ -1061,6 +1133,12 @@ class PaymentService:
referral_error,
)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
await db.commit()
await db.refresh(user)
await update_mulenpay_payment_status(
db,
payment=payment,
@@ -1077,10 +1155,13 @@ class PaymentService:
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
db,
user,
transaction,
old_balance,
topup_status=topup_status,
referrer_info=referrer_info,
subscription=subscription,
promo_group=promo_group,
)
except Exception as notify_error:
logger.error(
@@ -1238,10 +1319,17 @@ class PaymentService:
await link_pal24_payment_to_transaction(db, payment, transaction.id)
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
user.balance_kopeks += payment.amount_kopeks
user.updated_at = datetime.utcnow()
promo_group = getattr(user, "promo_group", None)
subscription = getattr(user, "subscription", None)
referrer_info = format_referrer_info(user)
topup_status = "🆕 Первое пополнение" if was_first_topup else "🔄 Пополнение"
await db.commit()
await db.refresh(user)
try:
from app.services.referral_service import process_referral_topup
@@ -1250,16 +1338,25 @@ class PaymentService:
except Exception as referral_error:
logger.error("Ошибка обработки реферального пополнения Pal24: %s", referral_error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
await db.commit()
await db.refresh(user)
if self.bot:
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
db,
user,
transaction,
old_balance,
topup_status=topup_status,
referrer_info=referrer_info,
subscription=subscription,
promo_group=promo_group,
)
except Exception as notify_error:
logger.error("Ошибка отправки админ уведомления Pal24: %s", notify_error)
@@ -1494,25 +1591,42 @@ class PaymentService:
user = await get_user_by_id(db, updated_payment.user_id)
if user:
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
user.balance_kopeks += amount_kopeks
user.updated_at = datetime.utcnow()
promo_group = getattr(user, "promo_group", None)
subscription = getattr(user, "subscription", None)
referrer_info = format_referrer_info(user)
topup_status = "🆕 Первое пополнение" if was_first_topup else "🔄 Пополнение"
await db.commit()
await db.refresh(user)
try:
from app.services.referral_service import process_referral_topup
await process_referral_topup(db, user.id, amount_kopeks, self.bot)
except Exception as e:
logger.error(f"Ошибка обработки реферального пополнения CryptoBot: {e}")
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
await db.commit()
await db.refresh(user)
if self.bot:
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
db, user, transaction, old_balance
user,
transaction,
old_balance,
topup_status=topup_status,
referrer_info=referrer_info,
subscription=subscription,
promo_group=promo_group,
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении CryptoBot: {e}")
+237 -8
View File
@@ -1,7 +1,7 @@
import logging
import os
import re
from contextlib import asynccontextmanager
from contextlib import AsyncExitStack, asynccontextmanager
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
@@ -12,7 +12,8 @@ from app.external.remnawave_api import (
RemnaWaveAPI, RemnaWaveUser, RemnaWaveInternalSquad,
RemnaWaveNode, UserStatus, TrafficLimitStrategy, RemnaWaveAPIError
)
from sqlalchemy import delete
from sqlalchemy import and_, cast, delete, func, select, update, String
from sqlalchemy.orm import selectinload
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.user import get_users_list, get_user_by_telegram_id, update_user
from app.database.crud.subscription import (
@@ -20,9 +21,16 @@ from app.database.crud.subscription import (
update_subscription_usage,
decrement_subscription_server_counts,
)
from app.database.crud.server_squad import get_server_squad_by_uuid
from app.database.models import (
User, SubscriptionServer, Transaction, ReferralEarning,
PromoCodeUse, SubscriptionStatus
User,
Subscription,
SubscriptionServer,
Transaction,
ReferralEarning,
PromoCodeUse,
SubscriptionStatus,
ServerSquad,
)
logger = logging.getLogger(__name__)
@@ -481,16 +489,237 @@ class RemnaWaveService:
try:
async with self.get_api_client() as api:
result = await api.delete_internal_squad(uuid)
if result:
logger.info(f"✅ Удален сквад {uuid}")
return result
except Exception as e:
logger.error(f"Ошибка удаления сквада {uuid}: {e}")
return False
async def migrate_squad_users(
self,
db: AsyncSession,
source_uuid: str,
target_uuid: str,
) -> Dict[str, Any]:
"""Переносит активных подписок с одного сквада на другой."""
if source_uuid == target_uuid:
return {
"success": False,
"error": "same_squad",
"message": "Источник и назначение совпадают",
}
source_uuid = source_uuid.strip()
target_uuid = target_uuid.strip()
source_server = await get_server_squad_by_uuid(db, source_uuid)
target_server = await get_server_squad_by_uuid(db, target_uuid)
if not source_server or not target_server:
return {
"success": False,
"error": "not_found",
"message": "Сквады не найдены",
}
subscription_query = (
select(Subscription)
.options(selectinload(Subscription.user))
.where(
Subscription.status.in_(
[
SubscriptionStatus.ACTIVE.value,
SubscriptionStatus.TRIAL.value,
]
),
cast(Subscription.connected_squads, String).like(
f'%"{source_uuid}"%'
),
)
)
result = await db.execute(subscription_query)
subscriptions = result.scalars().unique().all()
total_candidates = len(subscriptions)
if not subscriptions:
logger.info(
"🚚 Переезд сквада %s%s: подходящих подписок не найдено",
source_uuid,
target_uuid,
)
return {
"success": True,
"total": 0,
"updated": 0,
"panel_updated": 0,
"panel_failed": 0,
}
exit_stack = AsyncExitStack()
panel_updated = 0
panel_failed = 0
updated_subscriptions = 0
source_decrement = 0
target_increment = 0
try:
needs_panel_update = any(
subscription.user and subscription.user.remnawave_uuid
for subscription in subscriptions
)
api = None
if needs_panel_update:
api = await exit_stack.enter_async_context(self.get_api_client())
for subscription in subscriptions:
current_squads = list(subscription.connected_squads or [])
if source_uuid not in current_squads:
continue
had_target_before = target_uuid in current_squads
new_squads = [
squad_uuid for squad_uuid in current_squads if squad_uuid != source_uuid
]
if not had_target_before:
new_squads.append(target_uuid)
if subscription.user and subscription.user.remnawave_uuid:
if api is None:
panel_failed += 1
logger.error(
"❌ RemnaWave API недоступен для обновления пользователя %s",
subscription.user.telegram_id,
)
continue
try:
await api.update_user(
uuid=subscription.user.remnawave_uuid,
active_internal_squads=new_squads,
)
panel_updated += 1
except Exception as error:
panel_failed += 1
logger.error(
"❌ Ошибка обновления сквадов пользователя %s: %s",
subscription.user.telegram_id,
error,
)
continue
subscription.connected_squads = new_squads
subscription.updated_at = datetime.utcnow()
source_decrement += 1
if not had_target_before:
target_increment += 1
updated_subscriptions += 1
link_result = await db.execute(
select(SubscriptionServer)
.where(
and_(
SubscriptionServer.subscription_id == subscription.id,
SubscriptionServer.server_squad_id == source_server.id,
)
)
.limit(1)
)
link = link_result.scalars().first()
if link:
if had_target_before:
await db.execute(
delete(SubscriptionServer).where(
and_(
SubscriptionServer.subscription_id
== subscription.id,
SubscriptionServer.server_squad_id
== source_server.id,
)
)
)
else:
link.server_squad_id = target_server.id
elif not had_target_before:
db.add(
SubscriptionServer(
subscription_id=subscription.id,
server_squad_id=target_server.id,
paid_price_kopeks=0,
)
)
if updated_subscriptions:
if source_decrement:
await db.execute(
update(ServerSquad)
.where(ServerSquad.id == source_server.id)
.values(
current_users=func.greatest(
ServerSquad.current_users - source_decrement,
0,
)
)
)
if target_increment:
await db.execute(
update(ServerSquad)
.where(ServerSquad.id == target_server.id)
.values(
current_users=ServerSquad.current_users + target_increment
)
)
await db.commit()
else:
await db.rollback()
logger.info(
"🚚 Завершен переезд сквада %s%s: обновлено %s подписок (%s не обновлены в панели)",
source_uuid,
target_uuid,
updated_subscriptions,
panel_failed,
)
return {
"success": True,
"total": total_candidates,
"updated": updated_subscriptions,
"panel_updated": panel_updated,
"panel_failed": panel_failed,
"source_removed": source_decrement,
"target_added": target_increment,
}
except RemnaWaveConfigurationError:
await db.rollback()
raise
except Exception as error:
await db.rollback()
logger.error(
"❌ Ошибка переезда сквада %s%s: %s",
source_uuid,
target_uuid,
error,
)
return {
"success": False,
"error": "unexpected",
"message": str(error),
}
finally:
await exit_stack.aclose()
async def sync_users_from_panel(self, db: AsyncSession, sync_type: str = "all") -> Dict[str, int]:
try:
stats = {"created": 0, "updated": 0, "errors": 0, "deleted": 0}
File diff suppressed because it is too large Load Diff
+15
View File
@@ -112,6 +112,7 @@ class BotConfigurationService:
"WEBHOOK": "🌐 Webhook",
"LOG": "📝 Логирование",
"DEBUG": "🧪 Режим разработки",
"MODERATION": "🛡️ Модерация и фильтры",
}
CATEGORY_DESCRIPTIONS: Dict[str, str] = {
@@ -160,6 +161,7 @@ class BotConfigurationService:
"WEBHOOK": "Пути и секреты вебхуков.",
"LOG": "Уровни логирования и ротация.",
"DEBUG": "Отладочные функции и безопасный режим.",
"MODERATION": "Настройки фильтров отображаемых имен и защиты от фишинга.",
}
CATEGORY_KEY_OVERRIDES: Dict[str, str] = {
@@ -225,6 +227,7 @@ class BotConfigurationService:
"ENABLE_LOGO_MODE": "INTERFACE_BRANDING",
"LOGO_FILE": "INTERFACE_BRANDING",
"HIDE_SUBSCRIPTION_LINK": "INTERFACE_SUBSCRIPTION",
"MAIN_MENU_MODE": "INTERFACE",
"CONNECT_BUTTON_MODE": "CONNECT_BUTTON",
"MINIAPP_CUSTOM_URL": "CONNECT_BUTTON",
"APP_CONFIG_PATH": "ADDITIONAL",
@@ -280,6 +283,7 @@ class BotConfigurationService:
"LOG_": "LOG",
"WEB_API_": "WEB_API",
"DEBUG": "DEBUG",
"DISPLAY_NAME_": "MODERATION",
}
CHOICES: Dict[str, List[ChoiceOption]] = {
@@ -318,6 +322,10 @@ class BotConfigurationService:
ChoiceOption("link", "🔗 Прямая ссылка"),
ChoiceOption("happ_cryptolink", "🪙 Happ CryptoLink"),
],
"MAIN_MENU_MODE": [
ChoiceOption("default", "📋 Полное меню"),
ChoiceOption("text", "📝 Текстовое меню"),
],
"SERVER_STATUS_MODE": [
ChoiceOption("disabled", "🚫 Отключено"),
ChoiceOption("external_link", "🌐 Внешняя ссылка"),
@@ -398,6 +406,13 @@ class BotConfigurationService:
"warning": "Не забудьте отключить после завершения работ, иначе бот останется недоступен.",
"dependencies": "MAINTENANCE_MESSAGE, MAINTENANCE_CHECK_INTERVAL",
},
"DISPLAY_NAME_BANNED_KEYWORDS": {
"description": "Список слов и фрагментов, при наличии которых в отображаемом имени пользователь будет заблокирован.",
"format": "Перечислите ключевые слова через запятую или с новой строки.",
"example": "support, security, служебн",
"warning": "Слишком агрессивные фильтры могут блокировать добросовестных пользователей.",
"dependencies": "Фильтр отображаемых имен",
},
"REMNAWAVE_API_URL": {
"description": "Базовый адрес панели RemnaWave, с которой синхронизируется бот.",
"format": "Полный URL вида https://panel.example.com.",
+33 -12
View File
@@ -11,9 +11,10 @@ from app.database.models import Transaction, TransactionType, PaymentMethod
from app.database.crud.transaction import (
create_transaction, get_transaction_by_external_id, complete_transaction
)
from app.database.crud.user import get_user_by_telegram_id, add_user_balance
from app.database.crud.user import get_user_by_telegram_id
from app.external.tribute import TributeService as TributeAPI
from app.services.payment_service import PaymentService
from app.utils.user_utils import format_referrer_info
logger = logging.getLogger(__name__)
@@ -120,32 +121,52 @@ class TributeService:
amount_kopeks=amount_kopeks,
description=f"Пополнение через Tribute: {amount_kopeks/100}₽ (ID: {payment_id})"
)
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
user.balance_kopeks += amount_kopeks
user.updated_at = datetime.utcnow()
promo_group = getattr(user, "promo_group", None)
subscription = getattr(user, "subscription", None)
referrer_info = format_referrer_info(user)
topup_status = "🆕 Первое пополнение" if was_first_topup else "🔄 Пополнение"
await session.commit()
logger.info(f"✅ Баланс пользователя {user_telegram_id} обновлен: {old_balance} -> {user.balance_kopeks} коп (+{amount_kopeks})")
logger.info(f"✅ Создана транзакция ID: {transaction.id}")
try:
from app.services.referral_service import process_referral_topup
await process_referral_topup(session, user.id, amount_kopeks, self.bot)
except Exception as e:
logger.error(f"Ошибка обработки реферального пополнения Tribute: {e}")
if not user.has_made_first_topup:
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
await session.commit()
await session.refresh(user)
logger.info(
f"✅ Баланс пользователя {user_telegram_id} обновлен: {old_balance} -> {user.balance_kopeks} коп (+{amount_kopeks})"
)
logger.info(f"✅ Создана транзакция ID: {transaction.id}")
if was_first_topup:
logger.info(f"Отмечен первый топап для пользователя {user_telegram_id}")
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
session, user, transaction, old_balance
user,
transaction,
old_balance,
topup_status=topup_status,
referrer_info=referrer_info,
subscription=subscription,
promo_group=promo_group,
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о Tribute пополнении: {e}")
+7
View File
@@ -166,6 +166,13 @@ class SquadCreateStates(StatesGroup):
class SquadRenameStates(StatesGroup):
waiting_for_new_name = State()
class SquadMigrationStates(StatesGroup):
selecting_source = State()
selecting_target = State()
confirming = State()
class AdminSubmenuStates(StatesGroup):
in_users_submenu = State()
in_promo_submenu = State()
+35
View File
@@ -0,0 +1,35 @@
from aiogram import types
from aiogram.types import InlineKeyboardButton
from app.config import settings
DEFAULT_UNAVAILABLE_CALLBACK = "menu_profile_unavailable"
def build_miniapp_or_callback_button(
text: str,
*,
callback_data: str,
unavailable_callback: str = DEFAULT_UNAVAILABLE_CALLBACK,
) -> InlineKeyboardButton:
"""Create a button that opens the miniapp in text menu mode.
When the simplified text menu mode is enabled we should avoid exposing
deep bot flows and redirect the user to the configured miniapp instead.
If the miniapp URL is missing we fall back to a safe callback that shows
an alert about the unavailable profile rather than opening disabled
sections of the bot.
"""
if settings.is_text_main_menu_mode():
miniapp_url = settings.get_main_menu_miniapp_url()
if miniapp_url:
return InlineKeyboardButton(
text=text,
web_app=types.WebAppInfo(url=miniapp_url),
)
safe_callback = unavailable_callback or DEFAULT_UNAVAILABLE_CALLBACK
return InlineKeyboardButton(text=text, callback_data=safe_callback)
return InlineKeyboardButton(text=text, callback_data=callback_data)
+8 -3
View File
@@ -69,10 +69,12 @@ async def edit_or_answer_photo(
caption: str,
keyboard: types.InlineKeyboardMarkup,
parse_mode: str | None = "HTML",
*,
force_text: bool = False,
) -> None:
resolved_parse_mode = parse_mode or "HTML"
# Если режим логотипа выключен — работаем текстом
if not settings.ENABLE_LOGO_MODE:
# Если режим логотипа выключен или требуется текстовое сообщение — работаем текстом
if force_text or not settings.ENABLE_LOGO_MODE:
try:
if callback.message.photo:
await callback.message.delete()
@@ -84,7 +86,10 @@ async def edit_or_answer_photo(
parse_mode=resolved_parse_mode,
)
except TelegramBadRequest as error:
await callback.message.delete()
try:
await callback.message.delete()
except Exception:
pass
await _answer_text(callback, caption, keyboard, resolved_parse_mode, error)
return
+19
View File
@@ -12,6 +12,25 @@ from app.database.models import User, ReferralEarning, Transaction, TransactionT
logger = logging.getLogger(__name__)
def format_referrer_info(user: User) -> str:
"""Return formatted referrer info for admin notifications."""
referred_by_id = getattr(user, "referred_by_id", None)
if not referred_by_id:
return "Нет"
referrer = getattr(user, "referrer", None)
if not referrer:
return f"ID {referred_by_id} (не найден)"
if referrer.username:
return f"@{referrer.username} (ID: {referred_by_id})"
return f"ID {referrer.telegram_id}"
async def generate_unique_referral_code(db: AsyncSession, telegram_id: int) -> str:
max_attempts = 10
File diff suppressed because it is too large Load Diff
+88
View File
@@ -6,6 +6,11 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.server_squad import (
count_active_users_for_squad,
get_server_squad_by_uuid,
)
from ..dependencies import get_db_session, require_api_token
from ..schemas.remnawave import (
RemnaWaveConnectionStatus,
@@ -22,6 +27,10 @@ from ..schemas.remnawave import (
RemnaWaveSquadActionRequest,
RemnaWaveSquadCreateRequest,
RemnaWaveSquadListResponse,
RemnaWaveSquadMigrationPreviewResponse,
RemnaWaveSquadMigrationRequest,
RemnaWaveSquadMigrationResponse,
RemnaWaveSquadMigrationStats,
RemnaWaveSquadUpdateRequest,
RemnaWaveStatusResponse,
RemnaWaveSystemStatsResponse,
@@ -372,6 +381,30 @@ async def get_user_traffic(
return RemnaWaveUserTrafficResponse(telegram_id=telegram_id, **stats)
@router.get("/squads/{squad_uuid}/migration-preview", response_model=RemnaWaveSquadMigrationPreviewResponse)
async def preview_squad_migration(
squad_uuid: str,
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RemnaWaveSquadMigrationPreviewResponse:
service = _get_service()
_ensure_service_configured(service)
squad = await get_server_squad_by_uuid(db, squad_uuid)
if not squad:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Сквад не найден")
users_to_migrate = await count_active_users_for_squad(db, squad_uuid)
return RemnaWaveSquadMigrationPreviewResponse(
squad_uuid=squad.squad_uuid,
squad_name=squad.display_name,
current_users=squad.current_users or 0,
max_users=squad.max_users,
users_to_migrate=users_to_migrate,
)
@router.post("/sync/from-panel", response_model=RemnaWaveGenericSyncResponse)
async def sync_from_panel(
payload: RemnaWaveSyncFromPanelRequest,
@@ -454,3 +487,58 @@ async def get_sync_recommendations(
data = await service.get_sync_recommendations(db)
detail = "Рекомендации получены"
return RemnaWaveGenericSyncResponse(success=True, detail=detail, data=data)
@router.post("/squads/migrate", response_model=RemnaWaveSquadMigrationResponse)
async def migrate_squad(
payload: RemnaWaveSquadMigrationRequest,
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RemnaWaveSquadMigrationResponse:
service = _get_service()
_ensure_service_configured(service)
source_uuid = payload.source_uuid.strip()
target_uuid = payload.target_uuid.strip()
if source_uuid == target_uuid:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Источник и назначение совпадают")
source = await get_server_squad_by_uuid(db, source_uuid)
if not source:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Сквад-источник не найден")
target = await get_server_squad_by_uuid(db, target_uuid)
if not target:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Сквад-назначение не найден")
try:
result = await service.migrate_squad_users(
db,
source_uuid=source.squad_uuid,
target_uuid=target.squad_uuid,
)
except RemnaWaveConfigurationError as exc: # pragma: no cover - зависит от окружения
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, str(exc)) from exc
if not result.get("success"):
detail = result.get("message") or "Не удалось выполнить переезд"
return RemnaWaveSquadMigrationResponse(
success=False,
detail=detail,
error=result.get("error"),
)
stats = RemnaWaveSquadMigrationStats(
source_uuid=source.squad_uuid,
target_uuid=target.squad_uuid,
total=result.get("total", 0),
updated=result.get("updated", 0),
panel_updated=result.get("panel_updated", 0),
panel_failed=result.get("panel_failed", 0),
source_removed=result.get("source_removed", 0),
target_added=result.get("target_added", 0),
)
detail = result.get("message") or "Переезд выполнен"
return RemnaWaveSquadMigrationResponse(success=True, detail=detail, data=stats)
+453 -2
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ConfigDict, model_validator
class MiniAppBranding(BaseModel):
@@ -134,6 +134,108 @@ class MiniAppPromoOfferClaimResponse(BaseModel):
code: Optional[str] = None
class MiniAppSubscriptionAutopay(BaseModel):
enabled: bool = False
autopay_enabled: Optional[bool] = None
autopay_enabled_at: Optional[datetime] = None
days_before: Optional[int] = None
autopay_days_before: Optional[int] = None
default_days_before: Optional[int] = None
autopay_days_options: List[int] = Field(default_factory=list)
days_options: List[int] = Field(default_factory=list)
options: List[int] = Field(default_factory=list)
model_config = ConfigDict(extra="allow")
class MiniAppSubscriptionRenewalPeriod(BaseModel):
id: str
days: Optional[int] = None
months: Optional[int] = None
price_kopeks: Optional[int] = Field(default=None, alias="priceKopeks")
price_label: Optional[str] = Field(default=None, alias="priceLabel")
original_price_kopeks: Optional[int] = Field(default=None, alias="originalPriceKopeks")
original_price_label: Optional[str] = Field(default=None, alias="originalPriceLabel")
discount_percent: int = Field(default=0, alias="discountPercent")
price_per_month_kopeks: Optional[int] = Field(default=None, alias="pricePerMonthKopeks")
price_per_month_label: Optional[str] = Field(default=None, alias="pricePerMonthLabel")
is_recommended: bool = Field(default=False, alias="isRecommended")
description: Optional[str] = None
badge: Optional[str] = None
title: Optional[str] = None
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionRenewalOptionsRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionRenewalOptionsResponse(BaseModel):
success: bool = True
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
currency: str
balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
promo_group: Optional[MiniAppPromoGroup] = Field(default=None, alias="promoGroup")
promo_offer: Optional[Dict[str, Any]] = Field(default=None, alias="promoOffer")
periods: List[MiniAppSubscriptionRenewalPeriod] = Field(default_factory=list)
default_period_id: Optional[str] = Field(default=None, alias="defaultPeriodId")
missing_amount_kopeks: Optional[int] = Field(default=None, alias="missingAmountKopeks")
status_message: Optional[str] = Field(default=None, alias="statusMessage")
autopay_enabled: bool = False
autopay_days_before: Optional[int] = None
autopay_days_options: List[int] = Field(default_factory=list)
autopay: Optional[MiniAppSubscriptionAutopay] = None
autopay_settings: Optional[MiniAppSubscriptionAutopay] = None
model_config = ConfigDict(populate_by_name=True, extra="allow")
class MiniAppSubscriptionRenewalRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
period_id: Optional[str] = Field(default=None, alias="periodId")
period_days: Optional[int] = Field(default=None, alias="periodDays")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionRenewalResponse(BaseModel):
success: bool = True
message: Optional[str] = None
balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
renewed_until: Optional[datetime] = Field(default=None, alias="renewedUntil")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionAutopayRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
enabled: Optional[bool] = None
days_before: Optional[int] = Field(default=None, alias="daysBefore")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionAutopayResponse(BaseModel):
success: bool = True
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
autopay_enabled: bool = False
autopay_days_before: Optional[int] = None
autopay_days_options: List[int] = Field(default_factory=list)
autopay: Optional[MiniAppSubscriptionAutopay] = None
autopay_settings: Optional[MiniAppSubscriptionAutopay] = None
model_config = ConfigDict(populate_by_name=True, extra="allow")
class MiniAppPromoCode(BaseModel):
code: str
type: Optional[str] = None
@@ -253,9 +355,75 @@ class MiniAppReferralInfo(BaseModel):
referrals: Optional[MiniAppReferralList] = None
class MiniAppPaymentMethodsRequest(BaseModel):
init_data: str = Field(..., alias="initData")
class MiniAppPaymentMethod(BaseModel):
id: str
icon: Optional[str] = None
requires_amount: bool = False
currency: str = "RUB"
min_amount_kopeks: Optional[int] = None
max_amount_kopeks: Optional[int] = None
amount_step_kopeks: Optional[int] = None
class MiniAppPaymentMethodsResponse(BaseModel):
methods: List[MiniAppPaymentMethod] = Field(default_factory=list)
class MiniAppPaymentCreateRequest(BaseModel):
init_data: str = Field(..., alias="initData")
method: str
amount_rubles: Optional[float] = Field(default=None, alias="amountRubles")
amount_kopeks: Optional[int] = Field(default=None, alias="amountKopeks")
payment_option: Optional[str] = Field(default=None, alias="option")
class MiniAppPaymentCreateResponse(BaseModel):
success: bool = True
method: str
payment_url: Optional[str] = None
amount_kopeks: Optional[int] = None
extra: Dict[str, Any] = Field(default_factory=dict)
class MiniAppPaymentStatusQuery(BaseModel):
method: str
local_payment_id: Optional[int] = Field(default=None, alias="localPaymentId")
invoice_id: Optional[str] = Field(default=None, alias="invoiceId")
payment_id: Optional[str] = Field(default=None, alias="paymentId")
payload: Optional[str] = None
amount_kopeks: Optional[int] = Field(default=None, alias="amountKopeks")
started_at: Optional[str] = Field(default=None, alias="startedAt")
class MiniAppPaymentStatusRequest(BaseModel):
init_data: str = Field(..., alias="initData")
payments: List[MiniAppPaymentStatusQuery] = Field(default_factory=list)
class MiniAppPaymentStatusResult(BaseModel):
method: str
status: str
is_paid: bool = False
amount_kopeks: Optional[int] = None
currency: Optional[str] = None
completed_at: Optional[datetime] = None
transaction_id: Optional[int] = None
external_id: Optional[str] = None
message: Optional[str] = None
extra: Dict[str, Any] = Field(default_factory=dict)
class MiniAppPaymentStatusResponse(BaseModel):
results: List[MiniAppPaymentStatusResult] = Field(default_factory=list)
class MiniAppSubscriptionResponse(BaseModel):
success: bool = True
subscription_id: int
subscription_id: Optional[int] = None
remnawave_short_uuid: Optional[str] = None
user: MiniAppSubscriptionUser
subscription_url: Optional[str] = None
@@ -271,6 +439,7 @@ class MiniAppSubscriptionResponse(BaseModel):
happ_link: Optional[str] = None
happ_crypto_link: Optional[str] = None
happ_cryptolink_redirect_link: Optional[str] = None
happ_cryptolink_redirect_template: Optional[str] = None
balance_kopeks: int = 0
balance_rubles: float = 0.0
balance_currency: Optional[str] = None
@@ -283,8 +452,290 @@ class MiniAppSubscriptionResponse(BaseModel):
total_spent_label: Optional[str] = None
subscription_type: str
autopay_enabled: bool = False
autopay_days_before: Optional[int] = None
autopay_days_options: List[int] = Field(default_factory=list)
autopay: Optional[MiniAppSubscriptionAutopay] = None
autopay_settings: Optional[MiniAppSubscriptionAutopay] = None
branding: Optional[MiniAppBranding] = None
faq: Optional[MiniAppFaq] = None
legal_documents: Optional[MiniAppLegalDocuments] = None
referral: Optional[MiniAppReferralInfo] = None
subscription_missing: bool = False
subscription_missing_reason: Optional[str] = None
trial_available: bool = False
trial_duration_days: Optional[int] = None
trial_status: Optional[str] = None
model_config = ConfigDict(extra="allow")
class MiniAppSubscriptionServerOption(BaseModel):
uuid: str
name: Optional[str] = None
price_kopeks: Optional[int] = None
price_label: Optional[str] = None
discount_percent: Optional[int] = None
is_connected: bool = False
is_available: bool = True
disabled_reason: Optional[str] = None
class MiniAppSubscriptionTrafficOption(BaseModel):
value: Optional[int] = None
label: Optional[str] = None
price_kopeks: Optional[int] = None
price_label: Optional[str] = None
is_current: bool = False
is_available: bool = True
description: Optional[str] = None
class MiniAppSubscriptionDeviceOption(BaseModel):
value: int
label: Optional[str] = None
price_kopeks: Optional[int] = None
price_label: Optional[str] = None
class MiniAppSubscriptionCurrentSettings(BaseModel):
servers: List[MiniAppConnectedServer] = Field(default_factory=list)
traffic_limit_gb: Optional[int] = None
traffic_limit_label: Optional[str] = None
device_limit: int = 0
class MiniAppSubscriptionServersSettings(BaseModel):
available: List[MiniAppSubscriptionServerOption] = Field(default_factory=list)
min: int = 0
max: int = 0
can_update: bool = True
hint: Optional[str] = None
class MiniAppSubscriptionTrafficSettings(BaseModel):
options: List[MiniAppSubscriptionTrafficOption] = Field(default_factory=list)
can_update: bool = True
current_value: Optional[int] = None
class MiniAppSubscriptionDevicesSettings(BaseModel):
options: List[MiniAppSubscriptionDeviceOption] = Field(default_factory=list)
can_update: bool = True
min: int = 0
max: int = 0
step: int = 1
current: int = 0
price_kopeks: Optional[int] = None
price_label: Optional[str] = None
class MiniAppSubscriptionBillingContext(BaseModel):
months_remaining: int = 1
period_hint_days: Optional[int] = None
renews_at: Optional[datetime] = None
class MiniAppSubscriptionSettings(BaseModel):
subscription_id: int
currency: str = "RUB"
current: MiniAppSubscriptionCurrentSettings
servers: MiniAppSubscriptionServersSettings
traffic: MiniAppSubscriptionTrafficSettings
devices: MiniAppSubscriptionDevicesSettings
billing: Optional[MiniAppSubscriptionBillingContext] = None
class MiniAppSubscriptionSettingsResponse(BaseModel):
success: bool = True
settings: MiniAppSubscriptionSettings
class MiniAppSubscriptionSettingsRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = None
model_config = ConfigDict(populate_by_name=True)
@model_validator(mode="before")
@classmethod
def _populate_aliases(cls, values: Any) -> Any:
if isinstance(values, dict):
if "subscriptionId" in values and "subscription_id" not in values:
values["subscription_id"] = values["subscriptionId"]
return values
class MiniAppSubscriptionServersUpdateRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = None
servers: Optional[List[str]] = None
squads: Optional[List[str]] = None
server_uuids: Optional[List[str]] = None
squad_uuids: Optional[List[str]] = None
model_config = ConfigDict(populate_by_name=True)
@model_validator(mode="before")
@classmethod
def _populate_aliases(cls, values: Any) -> Any:
if isinstance(values, dict):
alias_map = {
"subscriptionId": "subscription_id",
"serverUuids": "server_uuids",
"squadUuids": "squad_uuids",
}
for alias, target in alias_map.items():
if alias in values and target not in values:
values[target] = values[alias]
return values
class MiniAppSubscriptionTrafficUpdateRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = None
traffic: Optional[int] = None
traffic_gb: Optional[int] = None
model_config = ConfigDict(populate_by_name=True)
@model_validator(mode="before")
@classmethod
def _populate_aliases(cls, values: Any) -> Any:
if isinstance(values, dict):
alias_map = {
"subscriptionId": "subscription_id",
"trafficGb": "traffic_gb",
}
for alias, target in alias_map.items():
if alias in values and target not in values:
values[target] = values[alias]
return values
class MiniAppSubscriptionDevicesUpdateRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = None
devices: Optional[int] = None
device_limit: Optional[int] = None
model_config = ConfigDict(populate_by_name=True)
@model_validator(mode="before")
@classmethod
def _populate_aliases(cls, values: Any) -> Any:
if isinstance(values, dict):
alias_map = {
"subscriptionId": "subscription_id",
"deviceLimit": "device_limit",
}
for alias, target in alias_map.items():
if alias in values and target not in values:
values[target] = values[alias]
return values
class MiniAppSubscriptionUpdateResponse(BaseModel):
success: bool = True
message: Optional[str] = None
class MiniAppSubscriptionPurchaseOptionsRequest(BaseModel):
init_data: str = Field(..., alias="initData")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionPurchaseOptionsResponse(BaseModel):
success: bool = True
currency: str
balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
data: Dict[str, Any] = Field(default_factory=dict)
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionPurchasePreviewRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
selection: Optional[Dict[str, Any]] = None
period_id: Optional[str] = Field(default=None, alias="periodId")
period_days: Optional[int] = Field(default=None, alias="periodDays")
period: Optional[str] = None
traffic_value: Optional[int] = Field(default=None, alias="trafficValue")
traffic: Optional[int] = None
traffic_gb: Optional[int] = Field(default=None, alias="trafficGb")
servers: Optional[List[str]] = None
countries: Optional[List[str]] = None
server_uuids: Optional[List[str]] = Field(default=None, alias="serverUuids")
devices: Optional[int] = None
device_limit: Optional[int] = Field(default=None, alias="deviceLimit")
model_config = ConfigDict(populate_by_name=True)
@model_validator(mode="before")
@classmethod
def _merge_selection(cls, values: Any) -> Any:
if not isinstance(values, dict):
return values
selection = values.get("selection")
if isinstance(selection, dict):
merged = {**selection, **values}
else:
merged = dict(values)
aliases = {
"period_id": ("periodId", "period", "code"),
"period_days": ("periodDays",),
"traffic_value": ("trafficValue", "traffic", "trafficGb"),
"servers": ("countries", "server_uuids", "serverUuids"),
"devices": ("deviceLimit",),
}
for target, sources in aliases.items():
if merged.get(target) is not None:
continue
for source in sources:
if source in merged and merged[source] is not None:
merged[target] = merged[source]
break
return merged
class MiniAppSubscriptionPurchasePreviewResponse(BaseModel):
success: bool = True
preview: Dict[str, Any] = Field(default_factory=dict)
balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionPurchaseRequest(MiniAppSubscriptionPurchasePreviewRequest):
pass
class MiniAppSubscriptionPurchaseResponse(BaseModel):
success: bool = True
message: Optional[str] = None
balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionTrialRequest(BaseModel):
init_data: str = Field(..., alias="initData")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionTrialResponse(BaseModel):
success: bool = True
message: Optional[str] = None
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
trial_status: Optional[str] = Field(default=None, alias="trialStatus")
trial_duration_days: Optional[int] = Field(default=None, alias="trialDurationDays")
model_config = ConfigDict(populate_by_name=True)
+31
View File
@@ -169,3 +169,34 @@ class RemnaWaveGenericSyncResponse(BaseModel):
success: bool
detail: Optional[str] = None
data: Optional[Dict[str, Any]] = None
class RemnaWaveSquadMigrationPreviewResponse(BaseModel):
squad_uuid: str
squad_name: str
current_users: int
max_users: Optional[int] = None
users_to_migrate: int
class RemnaWaveSquadMigrationRequest(BaseModel):
source_uuid: str
target_uuid: str
class RemnaWaveSquadMigrationStats(BaseModel):
source_uuid: str
target_uuid: str
total: int = 0
updated: int = 0
panel_updated: int = 0
panel_failed: int = 0
source_removed: int = 0
target_added: int = 0
class RemnaWaveSquadMigrationResponse(BaseModel):
success: bool
detail: Optional[str] = None
error: Optional[str] = None
data: Optional[RemnaWaveSquadMigrationStats] = None
+162
View File
@@ -0,0 +1,162 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Градиенты для щита -->
<linearGradient id="shieldGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#4a90e2;stop-opacity:1" />
<stop offset="50%" style="stop-color:#357abd;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2c5aa0;stop-opacity:1" />
</linearGradient>
<!-- Градиент для свечения -->
<radialGradient id="glowGradient">
<stop offset="0%" style="stop-color:#00d4ff;stop-opacity:0.8" />
<stop offset="100%" style="stop-color:#00d4ff;stop-opacity:0" />
</radialGradient>
<!-- Анимация мерцания -->
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
<animate attributeName="stdDeviation"
values="2;4;2"
dur="2s"
repeatCount="indefinite"/>
</filter>
<!-- Анимация электрических разрядов -->
<animate id="pulse" attributeName="opacity"
values="0.3;1;0.3"
dur="1.5s"
repeatCount="indefinite"/>
</defs>
<!-- Фоновое свечение -->
<circle cx="100" cy="100" r="80" fill="url(#glowGradient)" opacity="0.4">
<animate attributeName="r"
values="75;85;75"
dur="2s"
repeatCount="indefinite"/>
</circle>
<!-- Основной щит -->
<path d="M 100 30
C 100 30, 140 35, 140 35
C 140 35, 145 80, 145 100
C 145 120, 140 140, 130 155
C 120 170, 100 180, 100 180
C 100 180, 80 170, 70 155
C 60 140, 55 120, 55 100
C 55 80, 60 35, 60 35
C 60 35, 100 30, 100 30 Z"
fill="url(#shieldGradient)"
stroke="#00d4ff"
stroke-width="2"
filter="url(#glow)"/>
<!-- Электрические разряды по краям -->
<path d="M 100 30 L 105 40 L 100 45 L 110 55"
stroke="#00ffff"
stroke-width="1.5"
fill="none"
opacity="0.7">
<animate attributeName="opacity"
values="0.3;1;0.3;1;0.3"
dur="1.5s"
repeatCount="indefinite"/>
</path>
<path d="M 140 50 L 135 55 L 140 60 L 132 70"
stroke="#00ffff"
stroke-width="1.5"
fill="none"
opacity="0.7">
<animate attributeName="opacity"
values="1;0.3;1;0.3;1"
dur="1.5s"
repeatCount="indefinite"/>
</path>
<path d="M 60 50 L 65 55 L 60 60 L 68 70"
stroke="#00ffff"
stroke-width="1.5"
fill="none"
opacity="0.7">
<animate attributeName="opacity"
values="0.5;1;0.5;1;0.5"
dur="1.5s"
repeatCount="indefinite"/>
</path>
<!-- Внутреннее свечение -->
<path d="M 100 45
C 100 45, 130 48, 130 48
C 130 48, 133 85, 133 100
C 133 115, 130 135, 122 147
C 114 159, 100 167, 100 167
C 100 167, 86 159, 78 147
C 70 135, 67 115, 67 100
C 67 85, 70 48, 70 48
C 70 48, 100 45, 100 45 Z"
fill="none"
stroke="#00d4ff"
stroke-width="1"
opacity="0.5">
<animate attributeName="opacity"
values="0.3;0.7;0.3"
dur="2s"
repeatCount="indefinite"/>
</path>
<!-- Текст VPN -->
<text x="100" y="105"
font-family="Arial, sans-serif"
font-size="28"
font-weight="bold"
fill="#ffffff"
text-anchor="middle"
stroke="#00d4ff"
stroke-width="0.5">
VPN
<animate attributeName="fill-opacity"
values="0.8;1;0.8"
dur="2s"
repeatCount="indefinite"/>
</text>
<!-- Энергетические частицы -->
<circle cx="85" cy="80" r="2" fill="#00ffff" opacity="0.8">
<animate attributeName="cy"
values="80;120;80"
dur="3s"
repeatCount="indefinite"/>
<animate attributeName="opacity"
values="0;0.8;0"
dur="3s"
repeatCount="indefinite"/>
</circle>
<circle cx="115" cy="90" r="2" fill="#00ffff" opacity="0.8">
<animate attributeName="cy"
values="90;130;90"
dur="2.5s"
repeatCount="indefinite"/>
<animate attributeName="opacity"
values="0;0.8;0"
dur="2.5s"
repeatCount="indefinite"/>
</circle>
<circle cx="100" cy="70" r="2" fill="#00ffff" opacity="0.8">
<animate attributeName="cy"
values="70;110;70"
dur="2.8s"
repeatCount="indefinite"/>
<animate attributeName="opacity"
values="0;0.8;0"
dur="2.8s"
repeatCount="indefinite"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+52 -1
View File
@@ -38,6 +38,7 @@
"DEVICE_CONNECTION_HELP": "❓ How to reconnect a device?",
"DEVICE_GUIDE_ANDROID": "🤖 Android",
"DEVICE_GUIDE_ANDROID_TV": "📺 Android TV",
"DEVICE_GUIDE_APPLE_TV": "📺 Apple TV",
"DEVICE_GUIDE_IOS": "📱 iOS (iPhone/iPad)",
"DEVICE_GUIDE_MAC": "🎯 macOS",
"DEVICE_GUIDE_WINDOWS": "💻 Windows",
@@ -391,6 +392,19 @@
"SUBSCRIPTION_NONE": "❌ No active subscription",
"SUBSCRIPTION_NOT_FOUND": "❌ Subscription not found",
"SUBSCRIPTION_PURCHASED": "🎉 Subscription purchased successfully!",
"MINIAPP_PURCHASE_DISCOUNT_PERIOD": "Period discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_TRAFFIC": "Traffic discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_SERVERS": "Servers discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_DEVICES": "Devices discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_PROMO": "Promo offer: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_BREAKDOWN_DISCOUNT_NOTE": "Discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_BREAKDOWN_BASE": "Base plan",
"MINIAPP_PURCHASE_BREAKDOWN_TRAFFIC": "Traffic",
"MINIAPP_PURCHASE_BREAKDOWN_SERVERS": "Servers",
"MINIAPP_PURCHASE_BREAKDOWN_DEVICES": "Devices",
"MINIAPP_PURCHASE_BREAKDOWN_PROMO": "Promo discount",
"MINIAPP_PURCHASE_SUMMARY_DISCOUNT": "You save {amount}",
"MINIAPP_PURCHASE_STATUS_INSUFFICIENT": "Not enough funds on balance",
"SUBSCRIPTION_SUMMARY": "\n📋 <b>Final configuration</b>\n\n📅 <b>Period:</b> {period} days\n📈 <b>Traffic:</b> {traffic}\n🌍 <b>Countries:</b> {countries}\n📱 <b>Devices:</b> {devices}\n\n💰 <b>Total:</b> {total_price}\n\nConfirm the purchase?\n",
"SUBSCRIPTION_TRIAL": "🧪 Trial subscription",
"SUPPORT_INFO": "\n🛠️ <b>Technical support</b>\n\nFor any questions contact our support:\n\n👤 {settings.SUPPORT_USERNAME}\n\nWe can help with:\n• Connection setup\n• Troubleshooting issues\n• Payment questions\n• Other requests\n\n⏰ Response time: usually within 1-2 hours\n",
@@ -462,12 +476,14 @@
"SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "<blockquote expandable><code>{crypto_link}</code></blockquote>",
"SUBSCRIPTION_DEVICE_LINK_TITLE": "🔗 <b>Subscription link:</b>",
"SUBSCRIPTION_DEVICE_FEATURED_APP": "📋 <b>Recommended app:</b> {app_name}",
"SUBSCRIPTION_DEVICE_OTHER_APPS": "📦 <b>Other apps:</b> {app_list}",
"SUBSCRIPTION_DEVICE_OTHER_APPS_HINT": "Tap the \"Other apps\" button below to choose another app.",
"SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "<b>Step 1 - Install:</b>",
"SUBSCRIPTION_DEVICE_STEP_ADD_TITLE": "<b>Step 2 - Add subscription:</b>",
"SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "<b>Step 3 - Connect:</b>",
"SUBSCRIPTION_DEVICE_HOW_TO_TITLE": "💡 <b>How to connect:</b>",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP1": "1. Install the app from the link above",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Copy the subscription link (tap on it)",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Tap the \"Connect\" button below",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP3": "3. Open the app and paste the link",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP4": "4. Connect to a server",
"SUBSCRIPTION_APPS_TITLE": "📱 <b>Apps for {device_name}</b>",
@@ -870,7 +886,42 @@
"ADMIN_REMNAWAVE_MANAGE_NODES": "🖥️ Manage nodes",
"ADMIN_REMNAWAVE_SYNC": "🔄 Synchronization",
"ADMIN_REMNAWAVE_MANAGE_SQUADS": "🌐 Manage squads",
"ADMIN_REMNAWAVE_MIGRATION": "🚚 Migration",
"ADMIN_REMNAWAVE_TRAFFIC": "📈 Traffic",
"ADMIN_SQUAD_MIGRATION_TITLE": "🚚 <b>Squad migration</b>",
"ADMIN_SQUAD_MIGRATION_SELECT_SOURCE": "Choose the squad you want to migrate from:",
"ADMIN_SQUAD_MIGRATION_NO_OPTIONS": "No squads available. Add new ones or cancel the operation.",
"ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE": "✅ Available",
"ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE": "🚫 Unavailable",
"ADMIN_SQUAD_MIGRATION_SERVER_LABEL": "{name} — 👥 {users} ({status})",
"ADMIN_SQUAD_MIGRATION_SQUAD_BUTTON": "🌍 {name} — 👥 {users} ({status})",
"ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE_SHORT": "✅",
"ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE_SHORT": "🚫",
"ADMIN_SQUAD_MIGRATION_PAGE": "Page {page}/{pages}",
"ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE": "Source: {source}",
"ADMIN_SQUAD_MIGRATION_SELECT_TARGET": "Choose the destination squad:",
"ADMIN_SQUAD_MIGRATION_TARGET_EMPTY": "No other squads available. Cancel or create new squads.",
"ADMIN_SQUAD_MIGRATION_SQUAD_NOT_FOUND": "Squad not found or unavailable.",
"ADMIN_SQUAD_MIGRATION_SAME_SQUAD": "You can't choose the same squad.",
"ADMIN_SQUAD_MIGRATION_CONFIRM_DETAILS": "Review the migration parameters:",
"ADMIN_SQUAD_MIGRATION_CONFIRM_SOURCE": "• From: {source}",
"ADMIN_SQUAD_MIGRATION_CONFIRM_TARGET": "• To: {target}",
"ADMIN_SQUAD_MIGRATION_CONFIRM_COUNT": "• Users to migrate: {count}",
"ADMIN_SQUAD_MIGRATION_CONFIRM_PROMPT": "Confirm the operation.",
"ADMIN_SQUAD_MIGRATION_CONFIRM_BUTTON": "✅ Confirm",
"ADMIN_SQUAD_MIGRATION_CHANGE_TARGET": "🔄 Change destination",
"ADMIN_SQUAD_MIGRATION_IN_PROGRESS": "Starting migration...",
"ADMIN_SQUAD_MIGRATION_API_ERROR": "❌ Remnawave API is not configured: {error}",
"ADMIN_SQUAD_MIGRATION_ERROR": "❌ Failed to migrate (code: {code}). {details}",
"ADMIN_SQUAD_MIGRATION_NEW_BUTTON": "🔁 New migration",
"ADMIN_SQUAD_MIGRATION_BACK_BUTTON": "⬅️ Back to Remnawave",
"ADMIN_SQUAD_MIGRATION_SUCCESS_TITLE": "✅ Migration completed",
"ADMIN_SQUAD_MIGRATION_RESULT_TOTAL": "Subscriptions matched: {count}",
"ADMIN_SQUAD_MIGRATION_RESULT_UPDATED": "Migrated: {count}",
"ADMIN_SQUAD_MIGRATION_RESULT_PANEL_UPDATED": "Updated in panel: {count}",
"ADMIN_SQUAD_MIGRATION_RESULT_PANEL_FAILED": "Panel update failed: {count}",
"ADMIN_SQUAD_MIGRATION_CANCELLED": "❌ Migration cancelled.",
"ADMIN_SQUAD_MIGRATION_PAGE_HINT": "This is the current page.",
"ADMIN_STATS_USERS": "👥 Users",
"ADMIN_STATS_SUBSCRIPTIONS": "📱 Subscriptions",
"ADMIN_STATS_REVENUE": "💰 Revenue",
+52 -1
View File
@@ -215,6 +215,7 @@
"DEVICE_CONNECTION_HELP": "❓ Как подключить устройство заново?",
"DEVICE_GUIDE_ANDROID": "🤖 Android",
"DEVICE_GUIDE_ANDROID_TV": "📺 Android TV",
"DEVICE_GUIDE_APPLE_TV": "📺 Apple TV",
"DEVICE_GUIDE_IOS": "📱 iOS (iPhone/iPad)",
"DEVICE_GUIDE_MAC": "🎯 macOS",
"DEVICE_GUIDE_WINDOWS": "💻 Windows",
@@ -364,6 +365,19 @@
"SUBSCRIPTION_NONE": "❌ Нет активной подписки",
"SUBSCRIPTION_NOT_FOUND": "❌ Подписка не найдена",
"SUBSCRIPTION_PURCHASED": "🎉 Подписка успешно приобретена!",
"MINIAPP_PURCHASE_DISCOUNT_PERIOD": "Скидка на период: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_TRAFFIC": "Скидка на трафик: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_SERVERS": "Скидка на серверы: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_DEVICES": "Скидка на устройства: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_PROMO": "Промо-предложение: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_BREAKDOWN_DISCOUNT_NOTE": "Скидка: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_BREAKDOWN_BASE": "Базовый план",
"MINIAPP_PURCHASE_BREAKDOWN_TRAFFIC": "Трафик",
"MINIAPP_PURCHASE_BREAKDOWN_SERVERS": "Серверы",
"MINIAPP_PURCHASE_BREAKDOWN_DEVICES": "Устройства",
"MINIAPP_PURCHASE_BREAKDOWN_PROMO": "Промо скидка",
"MINIAPP_PURCHASE_SUMMARY_DISCOUNT": "Экономия {amount}",
"MINIAPP_PURCHASE_STATUS_INSUFFICIENT": "Недостаточно средств на балансе",
"SUBSCRIPTION_SETTINGS_BUTTON": "⚙️ Настройки подписки",
"SUBSCRIPTION_SUMMARY": "\n📋 <b>Итоговая конфигурация</b>\n\n📅 <b>Период:</b> {period} дней\n📈 <b>Трафик:</b> {traffic}\n🌍 <b>Страны:</b> {countries}\n📱 <b>Устройства:</b> {devices}\n\n💰 <b>Итого к оплате:</b> {total_price}\n\nПодтвердить покупку?\n",
"SUBSCRIPTION_TRIAL": "🧪 Тестовая подписка",
@@ -464,12 +478,14 @@
"SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "<blockquote expandable><code>{crypto_link}</code></blockquote>",
"SUBSCRIPTION_DEVICE_LINK_TITLE": "🔗 <b>Ссылка подписки:</b>",
"SUBSCRIPTION_DEVICE_FEATURED_APP": "📋 <b>Рекомендуемое приложение:</b> {app_name}",
"SUBSCRIPTION_DEVICE_OTHER_APPS": "📦 <b>Другие приложения:</b> {app_list}",
"SUBSCRIPTION_DEVICE_OTHER_APPS_HINT": "Нажмите кнопку \"Другие приложения\" ниже, чтобы выбрать приложение.",
"SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE": "<b>Шаг 1 - Установка:</b>",
"SUBSCRIPTION_DEVICE_STEP_ADD_TITLE": "<b>Шаг 2 - Добавление подписки:</b>",
"SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE": "<b>Шаг 3 - Подключение:</b>",
"SUBSCRIPTION_DEVICE_HOW_TO_TITLE": "💡 <b>Как подключить:</b>",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP1": "1. Установите приложение по ссылке выше",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Скопируйте ссылку подписки (нажмите на неё)",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Нажмите кнопку \"Подключиться\" ниже",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP3": "3. Откройте приложение и вставьте ссылку",
"SUBSCRIPTION_DEVICE_HOW_TO_STEP4": "4. Подключитесь к серверу",
"SUBSCRIPTION_APPS_TITLE": "📱 <b>Приложения для {device_name}</b>",
@@ -872,7 +888,42 @@
"ADMIN_REMNAWAVE_MANAGE_NODES": "🖥️ Управление нодами",
"ADMIN_REMNAWAVE_SYNC": "🔄 Синхронизация",
"ADMIN_REMNAWAVE_MANAGE_SQUADS": "🌐 Управление сквадами",
"ADMIN_REMNAWAVE_MIGRATION": "🚚 Переезд",
"ADMIN_REMNAWAVE_TRAFFIC": "📈 Трафик",
"ADMIN_SQUAD_MIGRATION_TITLE": "🚚 <b>Переезд сквадов</b>",
"ADMIN_SQUAD_MIGRATION_SELECT_SOURCE": "Выберите сквад, из которого нужно переехать:",
"ADMIN_SQUAD_MIGRATION_NO_OPTIONS": "Нет доступных сквадов. Добавьте новые или отмените операцию.",
"ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE": "✅ Доступен",
"ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE": "🚫 Недоступен",
"ADMIN_SQUAD_MIGRATION_SERVER_LABEL": "{name} — 👥 {users} ({status})",
"ADMIN_SQUAD_MIGRATION_SQUAD_BUTTON": "🌍 {name} — 👥 {users} ({status})",
"ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE_SHORT": "✅",
"ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE_SHORT": "🚫",
"ADMIN_SQUAD_MIGRATION_PAGE": "Стр. {page}/{pages}",
"ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE": "Источник: {source}",
"ADMIN_SQUAD_MIGRATION_SELECT_TARGET": "Выберите сквад, в который нужно переехать:",
"ADMIN_SQUAD_MIGRATION_TARGET_EMPTY": "Нет других сквадов для переезда. Отмените операцию или создайте новые сквады.",
"ADMIN_SQUAD_MIGRATION_SQUAD_NOT_FOUND": "Сквад не найден или недоступен.",
"ADMIN_SQUAD_MIGRATION_SAME_SQUAD": "Нельзя выбрать тот же сквад.",
"ADMIN_SQUAD_MIGRATION_CONFIRM_DETAILS": "Проверьте параметры переезда:",
"ADMIN_SQUAD_MIGRATION_CONFIRM_SOURCE": "• Из: {source}",
"ADMIN_SQUAD_MIGRATION_CONFIRM_TARGET": "• В: {target}",
"ADMIN_SQUAD_MIGRATION_CONFIRM_COUNT": "• Пользователей к переносу: {count}",
"ADMIN_SQUAD_MIGRATION_CONFIRM_PROMPT": "Подтвердите выполнение операции.",
"ADMIN_SQUAD_MIGRATION_CONFIRM_BUTTON": "✅ Подтвердить",
"ADMIN_SQUAD_MIGRATION_CHANGE_TARGET": "🔄 Изменить сервер назначения",
"ADMIN_SQUAD_MIGRATION_IN_PROGRESS": "Запускаю переезд...",
"ADMIN_SQUAD_MIGRATION_API_ERROR": "❌ Remnawave API не настроен: {error}",
"ADMIN_SQUAD_MIGRATION_ERROR": "❌ Не удалось выполнить переезд (код: {code}). {details}",
"ADMIN_SQUAD_MIGRATION_NEW_BUTTON": "🔁 Новый переезд",
"ADMIN_SQUAD_MIGRATION_BACK_BUTTON": "⬅️ В Remnawave",
"ADMIN_SQUAD_MIGRATION_SUCCESS_TITLE": "✅ Переезд завершен",
"ADMIN_SQUAD_MIGRATION_RESULT_TOTAL": "Найдено подписок: {count}",
"ADMIN_SQUAD_MIGRATION_RESULT_UPDATED": "Перенесено: {count}",
"ADMIN_SQUAD_MIGRATION_RESULT_PANEL_UPDATED": "Обновлено в панели: {count}",
"ADMIN_SQUAD_MIGRATION_RESULT_PANEL_FAILED": "Не удалось обновить в панели: {count}",
"ADMIN_SQUAD_MIGRATION_CANCELLED": "❌ Переезд отменен.",
"ADMIN_SQUAD_MIGRATION_PAGE_HINT": "Это текущая страница.",
"ADMIN_STATS_USERS": "👥 Пользователи",
"ADMIN_STATS_SUBSCRIPTIONS": "📱 Подписки",
"ADMIN_STATS_REVENUE": "💰 Доходы",
+486 -61
View File
@@ -1,5 +1,16 @@
{
"config": {},
"config": {
"additionalLocales": [
"ru",
"zh",
"fa"
],
"branding": {
"name": "Subscription",
"logoUrl": "https://raw.githubusercontent.com/Fr1ngg/remnawave-bedolaga-telegram-bot/bf0c1ce711a26fa2f24559e7e4443820e68d758b/assets/bedolaga_app3.svg",
"supportUrl": "https://t.me"
}
},
"platforms": {
"ios": [
{
@@ -12,19 +23,102 @@
{
"buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215",
"buttonText": {
"en": "App Store [EU]",
"fa": "App Store [EU]",
"ru": "App Store [EU]",
"zh": "App Store [EU]"
"en": "Open in App Store [EU]",
"fa": "باز کردن در App Store [EU]",
"ru": "Открыть в App Store [EU]",
"zh": "App Store 中打开 [EU]"
}
},
{
"buttonLink": "https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973",
"buttonText": {
"en": "App Store [RU]",
"fa": "App Store [RU]",
"ru": "App Store [RU]",
"zh": "App Store [RU]"
"en": "Open in App Store [RU]",
"fa": "باز کردن در App Store [RU]",
"ru": "Открыть в App Store [RU]",
"zh": "App Store 中打开 [RU]"
}
}
],
"description": {
"en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
"fa": "صفحه را در App Store باز کنید و برنامه را نصب کنید. آن را اجرا کنید، در پنجره مجوز پیکربندی VPN روی Allow کلیک کنید و رمز عبور خود را وارد کنید.",
"ru": "Откройте страницу в App Store и установите приложение. Запустите его, в окне разрешения VPN-конфигурации нажмите Allow и введите свой пароль.",
"zh": "在 App Store 中打开页面并安装应用。启动应用后,在 VPN 配置权限窗口中点击\"允许\"并输入您的密码。"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below — the app will open and the subscription will be added automatically",
"fa": "برای افزودن خودکار اشتراک روی دکمه زیر کلیک کنید - برنامه باز خواهد شد",
"ru": "Нажмите кнопку ниже — приложение откроется, и подписка добавится автоматически.",
"zh": "点击下方按钮 — 应用将打开并自动添加订阅"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
"zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
}
}
},
{
"id": "streisand",
"name": "Streisand",
"isFeatured": false,
"urlScheme": "streisand://import/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://apps.apple.com/ru/app/streisand/id6450534064",
"buttonText": {
"en": "Open in App Store",
"fa": "باز کردن در App Store",
"ru": "Открыть в App Store",
"zh": "在 App Store 中打开"
}
}
],
"description": {
"en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
"fa": "صفحه را در App Store باز کنید و برنامه را نصب کنید. آن را اجرا کنید، در پنجره مجوز پیکربندی VPN روی Allow کلیک کنید و رمز عبور خود را وارد کنید.",
"ru": "Откройте страницу в App Store и установите приложение. Запустите его, в окне разрешения VPN-конфигурации нажмите Allow и введите свой пароль.",
"zh": "在 App Store 中打开页面并安装应用。启动应用后,在 VPN 配置权限窗口中点击\"允许\"并输入您的密码。"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below — the app will open and the subscription will be added automatically",
"fa": "برای افزودن خودکار اشتراک روی دکمه زیر کلیک کنید - برنامه باز خواهد شد",
"ru": "Нажмите кнопку ниже — приложение откроется, и подписка добавится автоматически.",
"zh": "点击下方按钮 — 应用将打开并自动添加订阅"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
"zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
}
}
},
{
"id": "shadowrocket",
"name": "Shadowrocket",
"isFeatured": false,
"urlScheme": "sub://",
"isNeedBase64Encoding": true,
"installationStep": {
"buttons": [
{
"buttonLink": "https://apps.apple.com/ru/app/shadowrocket/id932747118",
"buttonText": {
"en": "Open in App Store",
"fa": "باز کردن در App Store",
"ru": "Открыть в App Store",
"zh": "在 App Store 中打开"
}
}
],
@@ -64,10 +158,10 @@
{
"buttonLink": "https://play.google.com/store/apps/details?id=com.happproxy",
"buttonText": {
"en": "Google Play",
"fa": "Google Play",
"ru": "Google Play",
"zh": "Google Play"
"en": "Open in Google Play",
"fa": "باز کردن در Google Play",
"ru": "Открыть в Google Play",
"zh": "Google Play 中打开"
}
},
{
@@ -103,53 +197,210 @@
"zh": "打开应用并连接到服务器"
}
}
}
],
"macos": [
},
{
"id": "happ",
"name": "Happ",
"isFeatured": true,
"urlScheme": "happ://add/",
"id": "clash-meta",
"name": "Clash Meta",
"isFeatured": false,
"urlScheme": "clash://install-config?url=",
"installationStep": {
"buttons": [
{
"buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215",
"buttonLink": "https://github.com/MetaCubeX/ClashMetaForAndroid/releases/download/v2.11.7/cmfa-2.11.7-meta-universal-release.apk",
"buttonText": {
"en": "App Store [EU]",
"fa": "App Store [EU]",
"ru": "App Store [EU]",
"zh": "App Store [EU]"
"en": "Download APK",
"fa": "دانلود APK",
"ru": "Скачать APK",
"zh": "下载 APK"
}
},
{
"buttonLink": "https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973",
"buttonLink": "https://f-droid.org/packages/com.github.metacubex.clash.meta/",
"buttonText": {
"en": "App Store [RU]",
"fa": "App Store [RU]",
"ru": "App Store [RU]",
"zh": "App Store [RU]"
"en": "Open in F-Droid",
"fa": "در F-Droid باز کنید",
"ru": "Открыть в F-Droid",
"zh": "在 F-Droid 中打开"
}
}
],
"description": {
"en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
"fa": "صفحه را در App Store باز کنید و برنامه را نصب کنید. آن را اجرا کنید، در پنجره مجوز پیکربندی VPN روی Allow کلیک کنید و رمز عبور خود را وارد کنید.",
"ru": "Откройте страницу в App Store и установите приложение. Запустите его, в окне разрешения VPN-конфигурации нажмите Allow и введите свой пароль.",
"zh": "在 App Store 中打开页面并安装应用。启动应用后,在 VPN 配置权限窗口中点击\"允许\"并输入您的密码。"
"en": "Download and install Clash Meta APK",
"fa": "دانلود و نصب Clash Meta APK",
"ru": "Скачайте и установите Clash Meta APK",
"zh": "下载并安装 Clash Meta APK"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below — the app will open and the subscription will be added automatically",
"fa": "برای افزودن خودکار اشتراک روی دکمه زیر کلیک کنید - برنامه باز خواهد شد",
"ru": "Нажмите кнопку ниже — приложение откроется, и подписка добавится автоматически.",
"zh": "点击下方按钮 — 应用将打开并自动添加订阅"
"en": "Tap the button to import configuration",
"fa": "برای وارد کردن پیکربندی روی دکمه ضربه بزنید",
"ru": "Нажмите кнопку, чтобы импортировать конфигурацию",
"zh": "点击按钮导入配置"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
"en": "Open Clash Meta and tap on Connect",
"fa": "Clash Meta را باز کنید و روی اتصال ضربه بزنید",
"ru": "Откройте Clash Meta и нажмите Подключиться",
"zh": "打开 Clash Meta 并点击连接"
}
}
}
],
"macos": [
{
"id": "clash-verge",
"name": "Clash Verge",
"isFeatured": true,
"urlScheme": "clash://install-config?url=",
"installationStep": {
"buttons": [
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_x64-setup.exe",
"buttonText": {
"en": "Windows",
"fa": "ویندوز",
"ru": "Windows",
"zh": "Windows"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_x64.dmg",
"buttonText": {
"en": "macOS (Intel)",
"fa": "مک (اینتل)",
"ru": "macOS (Intel)",
"zh": "macOS (Intel)"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_aarch64.dmg",
"buttonText": {
"en": "macOS (Apple Silicon)",
"fa": "مک (Apple Silicon)",
"ru": "macOS (Apple Silicon)",
"zh": "macOS (Apple Silicon)"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases",
"buttonText": {
"en": "Linux",
"fa": "لینوکس",
"ru": "Linux",
"zh": "Linux"
}
}
],
"description": {
"en": "Choose the version for your device, click the button below and install the app.",
"fa": "نسخه مناسب برای دستگاه خود را انتخاب کنید، دکمه زیر را فشار دهید و برنامه را نصب کنید",
"ru": "Выберите подходящую версию для вашего устройства, нажмите на кнопку ниже и установите приложение.",
"zh": "选择适合您设备的版本,点击下方按钮并安装应用。"
}
},
"additionalBeforeAddSubscriptionStep": {
"buttons": [],
"description": {
"en": "After launching the app, you can change the language in settings. In the left panel, find the gear icon, then navigate to Verge 设置 and select 语言设置.",
"fa": "پس از راه‌اندازی برنامه، می‌توانید زبان را در تنظیمات تغییر دهید. در پنل سمت چپ، نماد چرخ دنده را پیدا کنید، سپس به Verge 设置 بروید و 语言设置 را انتخاب کنید.",
"ru": "После запуска приложения вы можете сменить язык в настройках. В левой панели найдите иконку шестеренки, далее ориентируйтесь на Verge 设置 и выберите пункт 语言设置.",
"zh": "启动应用后,您可以在设置中更改语言。在左侧面板找到齿轮图标,然后导航到 Verge 设置并选择语言设置。"
},
"title": {
"en": "Change language",
"fa": "تغییر زبان",
"ru": "Смена языка",
"zh": "更改语言"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
"ru": "Нажмите кнопку ниже, чтобы добавить подписку",
"zh": "点击下方按钮添加订阅"
}
},
"additionalAfterAddSubscriptionStep": {
"buttons": [],
"title": {
"en": "If the subscription is not added",
"fa": "اگر اشتراک در برنامه نصب نشده است",
"ru": "Если подписка не добавилась",
"zh": "如果订阅未添加"
},
"description": {
"en": "If nothing happens after clicking the button, add the subscription manually. Click the Get Link button in the top right corner of this page, copy the link. In Clash Verge, go to the Profiles section and paste the link in the text field, then click the Import button.",
"fa": "اگر پس از کلیک روی دکمه اتفاقی نیفتاد، اشتراک را به صورت دستی اضافه کنید. در گوشه بالا سمت راست این صفحه روی دکمه دریافت لینک کلیک کنید، لینک را کپی کنید. در Clash Verge به بخش پروفایل‌ها بروید و لینک را در فیلد متنی وارد کنید، سپس روی دکمه وارد کردن کلیک کنید.",
"ru": "Если после нажатия на кнопку ничего не произошло, добавьте подписку вручную. Нажмите на этой страницу кнопку Получить ссылку в правом верхнем углу, скопируйте ссылку. В Clash Verge перейдите в раздел Профили и вставьте ссылку в текстовое поле, затем нажмите на кнопку Импорт.",
"zh": "如果点击按钮后没有反应,请手动添加订阅。点击此页面右上角的获取链接按钮,复制链接。在 Clash Verge 中,转到配置文件部分,将链接粘贴到文本字段中,然后点击导入按钮。"
}
},
"connectAndUseStep": {
"description": {
"en": "You can select a server in the Proxy section, and enable VPN in the Settings section. Set the TUN Mode switch to ON.",
"fa": "می‌توانید در بخش پروکسی سرور را انتخاب کنید و در بخش تنظیمات VPN را فعال کنید. کلید TUN Mode را در حالت روشن قرار دهید.",
"ru": "Выбрать сервер можно в разделе Прокси, включить VPN можно в разделе Настройки. Установите переключатель TUN Mode в положение ВКЛ.",
"zh": "您可以在代理部分选择服务器,在设置部分启用 VPN。将 TUN 模式开关设置为开启。"
}
}
},
{
"id": "hiddify",
"name": "Hiddify",
"isFeatured": false,
"urlScheme": "hiddify://import/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-Windows-Setup-x64.exe",
"buttonText": {
"en": "Windows",
"fa": "ویندوز",
"ru": "Windows",
"zh": "Windows"
}
},
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-MacOS.dmg",
"buttonText": {
"en": "macOS",
"fa": "مک",
"ru": "macOS",
"zh": "macOS"
}
},
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-Linux-x64.AppImage",
"buttonText": {
"en": "Linux",
"fa": "لینوکس",
"ru": "Linux",
"zh": "Linux"
}
}
],
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. If needed, select a different server in the Proxy section",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. در صورت نیاز، سرور دیگری را در بخش پروکسی انتخاب کنید",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. При необходимости выберите другой сервер в разделе Прокси.",
"zh": "在主界面中,点击中央的大电源按钮连接 VPN。如有需要,可在代理部分选择不同的服务器"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
"ru": "Нажмите кнопку ниже, чтобы добавить подписку",
"zh": "点击下方按钮添加订阅"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, select a different server from the server list.",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
"zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
@@ -159,75 +410,249 @@
],
"windows": [
{
"id": "happ",
"name": "Happ",
"isFeatured": false,
"urlScheme": "happ://add/",
"id": "clash-verge",
"name": "Clash Verge",
"isFeatured": true,
"urlScheme": "clash://install-config?url=",
"installationStep": {
"buttons": [
{
"buttonLink": "https://github.com/Happ-proxy/happ-desktop/releases/latest/download/setup-Happ.x86.exe",
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_x64-setup.exe",
"buttonText": {
"en": "Download",
"ru": "Скачать"
"en": "Windows",
"fa": "ویندوز",
"ru": "Windows",
"zh": "Windows"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_x64.dmg",
"buttonText": {
"en": "macOS (Intel)",
"fa": "مک (اینتل)",
"ru": "macOS (Intel)",
"zh": "macOS (Intel)"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.2.2/Clash.Verge_2.2.2_aarch64.dmg",
"buttonText": {
"en": "macOS (Apple Silicon)",
"fa": "مک (Apple Silicon)",
"ru": "macOS (Apple Silicon)",
"zh": "macOS (Apple Silicon)"
}
},
{
"buttonLink": "https://github.com/clash-verge-rev/clash-verge-rev/releases",
"buttonText": {
"en": "Linux",
"fa": "لینوکس",
"ru": "Linux",
"zh": "Linux"
}
}
],
"description": {
"en": "Download and install Happ.",
"ru": "Скачайте и установите Happ"
"en": "Choose the version for your device, click the button below and install the app.",
"fa": "نسخه مناسب برای دستگاه خود را انتخاب کنید، دکمه زیر را فشار دهید و برنامه را نصب کنید",
"ru": "Выберите подходящую версию для вашего устройства, нажмите на кнопку ниже и установите приложение.",
"zh": "选择适合您设备的版本,点击下方按钮并安装应用。"
}
},
"additionalBeforeAddSubscriptionStep": {
"buttons": [],
"description": {
"en": "After launching the app, you can change the language in settings. In the left panel, find the gear icon, then navigate to Verge 设置 and select 语言设置.",
"fa": "پس از راه‌اندازی برنامه، می‌توانید زبان را در تنظیمات تغییر دهید. در پنل سمت چپ، نماد چرخ دنده را پیدا کنید، سپس به Verge 设置 بروید و 语言设置 را انتخاب کنید.",
"ru": "После запуска приложения вы можете сменить язык в настройках. В левой панели найдите иконку шестеренки, далее ориентируйтесь на Verge 设置 и выберите пункт 语言设置.",
"zh": "启动应用后,您可以在设置中更改语言。在左侧面板找到齿轮图标,然后导航到 Verge 设置并选择语言设置。"
},
"title": {
"en": "Change language",
"fa": "تغییر زبان",
"ru": "Смена языка",
"zh": "更改语言"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
"fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
"ru": "Нажмите кнопку ниже, чтобы добавить подписку",
"zh": "点击下方按钮添加订阅"
}
},
"additionalAfterAddSubscriptionStep": {
"buttons": [],
"title": {
"en": "If the subscription is not added",
"fa": "اگر اشتراک در برنامه نصب نشده است",
"ru": "Если подписка не добавилась",
"zh": "如果订阅未添加"
},
"description": {
"en": "If nothing happens after clicking the button, add the subscription manually. Click the Get Link button in the top right corner of this page, copy the link. In Clash Verge, go to the Profiles section and paste the link in the text field, then click the Import button.",
"fa": "اگر پس از کلیک روی دکمه اتفاقی نیفتاد، اشتراک را به صورت دستی اضافه کنید. در گوشه بالا سمت راست این صفحه روی دکمه دریافت لینک کلیک کنید، لینک را کپی کنید. در Clash Verge به بخش پروفایل‌ها بروید و لینک را در فیلد متنی وارد کنید، سپس روی دکمه وارد کردن کلیک کنید.",
"ru": "Если после нажатия на кнопку ничего не произошло, добавьте подписку вручную. Нажмите на этой страницу кнопку Получить ссылку в правом верхнем углу, скопируйте ссылку. В Clash Verge перейдите в раздел Профили и вставьте ссылку в текстовое поле, затем нажмите на кнопку Импорт.",
"zh": "如果点击按钮后没有反应,请手动添加订阅。点击此页面右上角的获取链接按钮,复制链接。在 Clash Verge 中,转到配置文件部分,将链接粘贴到文本字段中,然后点击导入按钮。"
}
},
"connectAndUseStep": {
"description": {
"en": "You can select a server",
"ru": "Выберете локацию, включить VPN"
"en": "You can select a server in the Proxy section, and enable VPN in the Settings section. Set the TUN Mode switch to ON.",
"fa": "می‌توانید در بخش پروکسی سرور را انتخاب کنید و در بخش تنظیمات VPN را فعال کنید. کلید TUN Mode را در حالت روشن قرار دهید.",
"ru": "Выبрать сервер можно в разделе Прокси, включить VPN можно в разделе Настройки. Установите переключатель TUN Mode в положение ВКЛ.",
"zh": "您可以在代理部分选择服务器,在设置部分启用 VPN。将 TUN 模式开关设置为开启。"
}
}
},
{
"id": "hiddify",
"name": "Hiddify",
"isFeatured": false,
"urlScheme": "hiddify://import/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-Windows-Setup-x64.exe",
"buttonText": {
"en": "Windows",
"fa": "ویندوز",
"ru": "Windows",
"zh": "Windows"
}
},
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-MacOS.dmg",
"buttonText": {
"en": "macOS",
"fa": "مک",
"ru": "macOS",
"zh": "macOS"
}
},
{
"buttonLink": "https://github.com/hiddify/hiddify-app/releases/download/v2.5.7/Hiddify-Linux-x64.AppImage",
"buttonText": {
"en": "Linux",
"fa": "لینوکس",
"ru": "Linux",
"zh": "Linux"
}
}
],
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. If needed, select a different server in the Proxy section",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. در صورت نیاز، سرور دیگری را در بخش پروکسی انتخاب کنید",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. При необходимости выберите другой сервер в разделе Прокси.",
"zh": "在主界面中,点击中央的大电源按钮连接 VPN。如有需要,可在代理部分选择不同的服务器"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
"ru": "Нажмите кнопку ниже, чтобы добавить подписку",
"zh": "点击下方按钮添加订阅"
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, select a different server from the server list.",
"fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
"zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
}
}
}
],
"linux": [],
"androidTV": [
{
"id": "vpn4tv",
"name": "VPN4TV",
"id": "new-app-androidtv-1760203310792",
"name": "Happ",
"isFeatured": true,
"urlScheme": "",
"urlScheme": "happ://add/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://play.google.com/store/apps/details?id=com.vpn4tv.hiddify",
"buttonText": {
"en": "Google Play",
"ru": "Google Play"
"ru": "Button TextGoogle Play",
"zh": "Button Text",
"fa": "Button Text"
}
}
],
"description": {
"en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
"ru": "Откройте страницу в Google Play и установите приложение"
"ru": "Откройте страницу в Google Play и установите приложение",
"zh": "-",
"fa": "-"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал"
"ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал",
"zh": "-",
"fa": "-"
}
},
"connectAndUseStep": {
"description": {
"en": "Open the app and connect to the server",
"ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN"
"ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN",
"zh": "-",
"fa": "-"
}
}
}
],
"linux": [],
"appleTV": []
"appleTV": [
{
"id": "new-app-appletv-1760203488851",
"name": "Happ",
"isFeatured": true,
"urlScheme": "happ://add/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://play.google.com/store/apps/details?id=com.vpn4tv.hiddify",
"buttonText": {
"en": "Google Play",
"ru": "Google Play",
"zh": "Button Text",
"fa": "Button Text"
}
}
],
"description": {
"en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
"ru": "Откройте страницу в Google Play и установите приложение",
"zh": "-",
"fa": "-"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал",
"zh": "-",
"fa": "-"
}
},
"connectAndUseStep": {
"description": {
"en": "Open the app and connect to the server",
"ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN",
"zh": "-",
"fa": "-"
}
}
}
]
}
}
}
+11834 -190
View File
File diff suppressed because it is too large Load Diff
+353
View File
@@ -0,0 +1,353 @@
import os
import sys
import types
from datetime import datetime, timedelta
from pathlib import Path
import pytest
ROOT_DIR = Path(__file__).resolve().parents[1]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
BACKUP_DIR = ROOT_DIR / 'data' / 'backups'
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
os.environ.setdefault('BOT_TOKEN', 'test-token')
from app.config import settings
from app.webapi.routes import miniapp
from app.database.models import PaymentMethod
from app.webapi.schemas.miniapp import (
MiniAppPaymentCreateRequest,
MiniAppPaymentMethodsRequest,
MiniAppPaymentStatusQuery,
)
@pytest.fixture
def anyio_backend():
return 'asyncio'
def test_compute_cryptobot_limits_scale_with_rate():
low_rate_min, low_rate_max = miniapp._compute_cryptobot_limits(70.0)
high_rate_min, high_rate_max = miniapp._compute_cryptobot_limits(120.0)
assert low_rate_min == 7000
assert low_rate_max == 7000000
assert high_rate_min == 12000
assert high_rate_max == 12000000
assert high_rate_min > low_rate_min
assert high_rate_max > low_rate_max
@pytest.mark.anyio("asyncio")
async def test_create_payment_link_pal24_uses_selected_option(monkeypatch):
monkeypatch.setattr(settings, 'PAL24_ENABLED', True, raising=False)
monkeypatch.setattr(settings, 'PAL24_API_TOKEN', 'token', raising=False)
monkeypatch.setattr(settings, 'PAL24_SHOP_ID', 'shop', raising=False)
monkeypatch.setattr(settings, 'PAL24_MIN_AMOUNT_KOPEKS', 1000, raising=False)
monkeypatch.setattr(settings, 'PAL24_MAX_AMOUNT_KOPEKS', 5000000, raising=False)
captured_calls = []
class DummyPaymentService:
def __init__(self, *args, **kwargs):
pass
async def create_pal24_payment(self, db, **kwargs):
captured_calls.append({'db': db, **kwargs})
return {
'local_payment_id': 101,
'bill_id': 'BILL42',
'order_id': 'ORD42',
'payment_method': kwargs.get('payment_method'),
'sbp_url': 'https://sbp',
'card_url': 'https://card',
'link_url': 'https://link',
}
async def fake_resolve_user(db, init_data):
return types.SimpleNamespace(id=123, language='ru'), {}
monkeypatch.setattr(miniapp, 'PaymentService', lambda *args, **kwargs: DummyPaymentService())
monkeypatch.setattr(miniapp, '_resolve_user_from_init_data', fake_resolve_user)
payload = MiniAppPaymentCreateRequest(
initData='test',
method='pal24',
amountKopeks=15000,
option='card',
)
response = await miniapp.create_payment_link(payload, db=types.SimpleNamespace())
assert response.payment_url == 'https://card'
assert response.extra['selected_option'] == 'card'
assert response.extra['payment_method'] == 'CARD'
assert captured_calls and captured_calls[0]['payment_method'] == 'CARD'
@pytest.mark.anyio("asyncio")
async def test_resolve_yookassa_status_includes_identifiers(monkeypatch):
payment = types.SimpleNamespace(
id=55,
user_id=1,
amount_kopeks=15000,
currency='RUB',
status='pending',
is_paid=False,
captured_at=None,
updated_at=None,
created_at=datetime.utcnow(),
transaction_id=42,
yookassa_payment_id='yk_1',
)
async def fake_get_by_local_id(db, local_id):
return payment if local_id == 55 else None
async def fake_get_by_id(db, payment_id):
return None
stub_module = types.SimpleNamespace(
get_yookassa_payment_by_local_id=fake_get_by_local_id,
get_yookassa_payment_by_id=fake_get_by_id,
)
monkeypatch.setitem(sys.modules, 'app.database.crud.yookassa', stub_module)
user = types.SimpleNamespace(id=1)
query = MiniAppPaymentStatusQuery(
method='yookassa',
localPaymentId=55,
paymentId='yk_1',
amountKopeks=15000,
startedAt='2024-01-01T00:00:00Z',
payload='payload123',
)
result = await miniapp._resolve_yookassa_payment_status(db=None, user=user, query=query)
assert result.extra['local_payment_id'] == 55
assert result.extra['payment_id'] == 'yk_1'
assert result.extra['invoice_id'] == 'yk_1'
assert result.extra['payload'] == 'payload123'
assert result.extra['started_at'] == '2024-01-01T00:00:00Z'
@pytest.mark.anyio("asyncio")
async def test_resolve_pal24_status_includes_identifiers(monkeypatch):
async def fake_get_pal24_payment_by_bill_id(db, bill_id):
return None
stub_module = types.SimpleNamespace(
get_pal24_payment_by_bill_id=fake_get_pal24_payment_by_bill_id,
)
monkeypatch.setitem(sys.modules, 'app.database.crud.pal24', stub_module)
paid_at = datetime.utcnow()
payment = types.SimpleNamespace(
id=321,
user_id=1,
amount_kopeks=25000,
currency='RUB',
is_paid=True,
status='PAID',
paid_at=paid_at,
updated_at=paid_at,
created_at=paid_at - timedelta(minutes=1),
transaction_id=777,
bill_id='BILL99',
order_id='ORD99',
payment_method='SBP',
)
class StubPal24Service:
async def get_pal24_payment_status(self, db, local_id):
assert local_id == 321
return {
'payment': payment,
'status': 'PAID',
'remote_status': 'PAID',
}
user = types.SimpleNamespace(id=1)
query = MiniAppPaymentStatusQuery(
method='pal24',
localPaymentId=321,
amountKopeks=25000,
startedAt='2024-01-01T00:00:00Z',
payload='pal24_payload',
)
result = await miniapp._resolve_pal24_payment_status(
StubPal24Service(),
db=None,
user=user,
query=query,
)
assert result.status == 'paid'
assert result.extra['local_payment_id'] == 321
assert result.extra['bill_id'] == 'BILL99'
assert result.extra['order_id'] == 'ORD99'
assert result.extra['payment_method'] == 'SBP'
assert result.extra['payload'] == 'pal24_payload'
assert result.extra['started_at'] == '2024-01-01T00:00:00Z'
assert result.extra['remote_status'] == 'PAID'
@pytest.mark.anyio("asyncio")
async def test_create_payment_link_stars_normalizes_amount(monkeypatch):
monkeypatch.setattr(settings, 'TELEGRAM_STARS_ENABLED', True, raising=False)
monkeypatch.setattr(settings, 'TELEGRAM_STARS_RATE_RUB', 1000.0, raising=False)
monkeypatch.setattr(settings, 'BOT_TOKEN', 'test-token', raising=False)
captured = {}
class DummyPaymentService:
def __init__(self, bot):
captured['bot'] = bot
async def create_stars_invoice(
self,
amount_kopeks,
description,
payload,
*,
stars_amount=None,
):
captured['amount_kopeks'] = amount_kopeks
captured['description'] = description
captured['payload'] = payload
captured['stars_amount'] = stars_amount
return 'https://invoice.example'
class DummySession:
def __init__(self):
self.closed = False
async def close(self):
self.closed = True
captured['session_closed'] = True
class DummyBot:
def __init__(self, token):
captured['bot_token'] = token
self.session = DummySession()
async def fake_resolve_user(db, init_data):
return types.SimpleNamespace(id=7, language='ru'), {}
monkeypatch.setattr(miniapp, 'PaymentService', lambda bot: DummyPaymentService(bot))
monkeypatch.setattr(miniapp, 'Bot', DummyBot)
monkeypatch.setattr(miniapp, '_resolve_user_from_init_data', fake_resolve_user)
payload = MiniAppPaymentCreateRequest(
initData='data',
method='stars',
amountKopeks=101000,
)
response = await miniapp.create_payment_link(payload, db=types.SimpleNamespace())
assert response.payment_url == 'https://invoice.example'
assert response.amount_kopeks == 100000
assert response.extra['stars_amount'] == 1
assert response.extra['requested_amount_kopeks'] == 101000
assert captured['amount_kopeks'] == 100000
assert captured['stars_amount'] == 1
assert captured['bot_token'] == 'test-token'
assert captured.get('session_closed') is True
@pytest.mark.anyio("asyncio")
async def test_get_payment_methods_exposes_stars_min_amount(monkeypatch):
monkeypatch.setattr(settings, 'TELEGRAM_STARS_ENABLED', True, raising=False)
monkeypatch.setattr(settings, 'TELEGRAM_STARS_RATE_RUB', 999.99, raising=False)
async def fake_resolve_user(db, init_data):
return types.SimpleNamespace(id=1, language='ru'), {}
monkeypatch.setattr(miniapp, '_resolve_user_from_init_data', fake_resolve_user)
payload = MiniAppPaymentMethodsRequest(initData='abc')
response = await miniapp.get_payment_methods(payload, db=types.SimpleNamespace())
stars_method = next((method for method in response.methods if method.id == 'stars'), None)
assert stars_method is not None
assert stars_method.min_amount_kopeks == 99999
assert stars_method.amount_step_kopeks == 99999
@pytest.mark.anyio("asyncio")
async def test_find_recent_deposit_ignores_transactions_before_attempt():
started_at = datetime(2024, 5, 1, 12, 0, 0)
transaction = types.SimpleNamespace(
id=10,
amount_kopeks=1000,
completed_at=None,
created_at=started_at - timedelta(minutes=1),
)
class DummyResult:
def __init__(self, value):
self._value = value
def scalar_one_or_none(self):
return self._value
class DummySession:
def __init__(self, value):
self._value = value
async def execute(self, query): # noqa: ARG002
return DummyResult(self._value)
result = await miniapp._find_recent_deposit(
DummySession(transaction),
user_id=1,
payment_method=PaymentMethod.TELEGRAM_STARS,
amount_kopeks=1000,
started_at=started_at,
)
assert result is None
@pytest.mark.anyio("asyncio")
async def test_find_recent_deposit_accepts_recent_transactions():
started_at = datetime(2024, 5, 1, 12, 0, 0)
transaction = types.SimpleNamespace(
id=11,
amount_kopeks=1000,
completed_at=started_at + timedelta(seconds=5),
created_at=started_at + timedelta(seconds=5),
)
class DummyResult:
def __init__(self, value):
self._value = value
def scalar_one_or_none(self):
return self._value
class DummySession:
def __init__(self, value):
self._value = value
async def execute(self, query): # noqa: ARG002
return DummyResult(self._value)
result = await miniapp._find_recent_deposit(
DummySession(transaction),
user_id=1,
payment_method=PaymentMethod.TELEGRAM_STARS,
amount_kopeks=1000,
started_at=started_at,
)
assert result is transaction