Compare commits

...

131 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
27 changed files with 3171 additions and 453 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.8-$(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.8-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.8-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.8-$(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.8-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.8-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.8"
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": "-"
}
}
}
]
}
}
}
+37
View File
@@ -227,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 = ""
@@ -293,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:
@@ -577,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):
+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([
+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"
+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:
+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}")
+6 -1
View File
@@ -59,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",
@@ -396,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",
@@ -435,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>",
+6 -1
View File
@@ -184,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",
@@ -255,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": "🤝 Партнерка",
@@ -445,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>",
+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(
+4 -3
View File
@@ -31,6 +31,7 @@ from app.services.subscription_checkout_service import (
)
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,
@@ -71,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
@@ -88,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")
+5
View File
@@ -227,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",
@@ -321,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", "🌐 Внешняя ссылка"),
+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
+33
View File
@@ -2241,6 +2241,38 @@ async def get_subscription_details(
)
subscription = getattr(user, "subscription", None)
usage_synced = False
if subscription and _is_remnawave_configured():
service = SubscriptionService()
try:
usage_synced = await service.sync_subscription_usage(db, subscription)
except Exception as error: # pragma: no cover - defensive logging
logger.warning(
"Failed to sync subscription usage for user %s: %s",
getattr(user, "id", "unknown"),
error,
)
if usage_synced:
try:
await db.refresh(subscription, attribute_names=["traffic_used_gb", "updated_at"])
except Exception as refresh_error: # pragma: no cover - defensive logging
logger.debug(
"Failed to refresh subscription after usage sync: %s",
refresh_error,
)
try:
await db.refresh(user)
except Exception as refresh_error: # pragma: no cover - defensive logging
logger.debug(
"Failed to refresh user after usage sync: %s",
refresh_error,
)
user = await get_user_by_telegram_id(db, telegram_id)
subscription = getattr(user, "subscription", subscription)
lifetime_used = _bytes_to_gb(getattr(user, "lifetime_used_traffic_bytes", 0))
transactions_query = (
@@ -2559,6 +2591,7 @@ async def get_subscription_details(
happ_link=links_payload.get("happ_link") if subscription else None,
happ_crypto_link=links_payload.get("happ_crypto_link") if subscription else None,
happ_cryptolink_redirect_link=happ_redirect_link,
happ_cryptolink_redirect_template=settings.get_happ_cryptolink_redirect_template(),
balance_kopeks=user.balance_kopeks,
balance_rubles=round(user.balance_rubles, 2),
balance_currency=balance_currency,
+1
View File
@@ -439,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
+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

+4 -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",
@@ -475,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>",
+4 -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",
@@ -477,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>",
+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": "-"
}
}
}
]
}
}
}
+1256 -140
View File
File diff suppressed because it is too large Load Diff