Compare commits

..
796 Commits
Author SHA1 Message Date
3252a8andGitHub 3e1f1cb787 Merge pull request #15 from 3252a8/dev
Split subscription stats by access type, CI/CD pipelines and notification/docs-demo fixesDev
2026-05-31 00:30:11 +03:00
3252a8 74da8ab98e chore: compact admin subscription stats cards 2026-05-31 00:26:42 +03:00
3252a8 19f7daff6c fix: stabilize docs demo runtime routing 2026-05-31 00:25:23 +03:00
3252a8 cf3af17243 fix: avoid Telegram notification probe messages 2026-05-31 00:17:20 +03:00
3252a8 93353db511 feat: split subscription stats by access type 2026-05-31 00:08:31 +03:00
3252a8andGitHub e7f93a5f47 Merge pull request #14 from 3252a8/dependabot/npm_and_yarn/frontend/npm_and_yarn-de2076f775
chore(deps-dev): bump svelte from 5.55.5 to 5.56.0 in /frontend in the npm_and_yarn group across 1 directory
2026-05-30 23:26:04 +03:00
3252a8andGitHub c2ab881be2 Merge pull request #13 from 3252a8/feature/ci-cd-workflows
ci: add GitHub Actions for image builds, PR checks and security scans
2026-05-30 23:22:51 +03:00
3252a8 4a84bba697 ci: use v-prefixed trivy-action tag (v0.36.0) 2026-05-30 23:20:22 +03:00
3252a8 cda3b741a1 ci: fix workflow failures on PR
- drop permissions block from reusable build workflow so callers set token
  scope (fixes PR-checks startup failure: ci.yml grants only contents:read
  while the reusable demanded packages:write)
- remove codeql.yml: repo already uses CodeQL default setup, which conflicts
  with an advanced configuration
- pin trivy-action to 0.36.0 (0.28.0 tag does not exist; <0.35.0 is the
  compromised supply-chain release flagged by dependency-review)
- make pip-audit and npm audit informational (continue-on-error); they flag
  upstream/transitive advisories, dependency-review stays the PR gate
2026-05-30 23:14:46 +03:00
dependabot[bot]andGitHub 2e82febdcc chore(deps-dev): bump svelte
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte).


Updates `svelte` from 5.55.5 to 5.56.0
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.56.0
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 20:09:10 +00:00
3252a8 bd7710d03c ci: add GitHub Actions for image builds, PR checks and security scans
- dev-images: build/push backend, worker, frontend to ghcr.io and Docker
  Hub on every push to dev (tags: dev, dev-<sha>)
- release-images: same images on v* tag push (tags: latest, <version>)
- PR checks (into main/dev): ruff lint+format, eslint+prettier, no-push
  Docker build of all targets
- CodeQL (python, js/ts), dependency-review, pip-audit, npm audit, Trivy fs
- pin .github/workflows/*.yml to LF
2026-05-30 22:57:41 +03:00
3252a8andGitHub 1a66d67e44 Merge pull request #12 from 3252a8/dev
Telegram delivery tracking, subscription lifecycle mirroring and per-tariff referral bonuses
2026-05-30 22:28:57 +03:00
3252a8 c1e2fe2c95 chore: improve admin logs user cards 2026-05-30 22:15:33 +03:00
3252a8 ea4ee4c4a7 fix: restore admin content scrolling 2026-05-30 21:49:40 +03:00
3252a8 fbc3e193bf chore: improve custom scroll areas 2026-05-30 21:41:32 +03:00
3252a8 3541f2f78b feat: block crawlers from production webapp 2026-05-30 21:25:03 +03:00
3252a8 acc222da41 feat: prompt users to start Telegram bot for notifications 2026-05-30 21:15:17 +03:00
3252a8 8c0e778388 chore: standardize web controls 2026-05-30 12:16:02 +03:00
3252a8 c6c5352813 fix: format premium traffic limits from bytes 2026-05-30 12:00:01 +03:00
3252a8 067d6fb59f chore: clarify panel webhook subscription logs 2026-05-30 11:52:07 +03:00
3252a8 c3e55bc853 feature: sync user email notifications 2026-05-30 11:46:07 +03:00
3252a8 09be82aad5 fix: improve subscription email renewal flow 2026-05-30 00:23:53 +03:00
3252a8 49781af921 feat: surface remnawave panel webhook settings 2026-05-29 23:55:12 +03:00
3252a8 604ba873dc docs: clarify remnawave panel webhook setup 2026-05-29 23:54:35 +03:00
3252a8 923ff9b208 fix: hide inactive devices usage summary 2026-05-29 23:23:55 +03:00
3252a8 7e7a2e58a2 fix: show telegram login without email auth 2026-05-29 23:09:49 +03:00
3252a8 e27886e15d docs: update referral bonus tariff docs 2026-05-29 23:00:18 +03:00
3252a8 7fe8e676cd feat: group referral bonus display by tariff 2026-05-29 22:55:01 +03:00
3252a8 6803c7801f fix: configure referral bonuses per tariff period 2026-05-29 22:30:31 +03:00
3252a8 cab963dcdc fix: prevent subscription worker deadlocks 2026-05-29 22:07:16 +03:00
3252a8 001e54cfe2 fix: preserve subscriptions on panel lookup failures 2026-05-29 21:46:42 +03:00
3252a8 5e257c0d3a chore: enforce frontend formatting line endings 2026-05-29 18:14:35 +03:00
3252a8 19f0f27a3b feat: mirror subscription lifecycle notifications 2026-05-29 18:08:28 +03:00
3252a8 6fbb8eebec fix: serve docs demo runtime in dev 2026-05-29 18:04:00 +03:00
3252a8 32616c80ab docs: add notifications table 2026-05-29 17:50:30 +03:00
3252a8 3186be1e3e docs: update deployment examples 2026-05-29 15:33:09 +03:00
3252a8 1329eb4fe2 fix: avoid docs demo redirect loop 2026-05-28 23:44:52 +03:00
3252a8 913b2d428e fix: handle docs demo app route 2026-05-28 23:33:40 +03:00
3252a8andGitHub 5892a72575 Merge pull request #7 from 3252a8/dev
Backups, docs demo, and Mini App improvements
2026-05-28 22:57:40 +03:00
3252a8 d299a3c524 feature: Add local subscription notification worker 2026-05-28 22:48:48 +03:00
3252a8 1c1ef06e29 fix: Fix trial settings persistence 2026-05-28 22:48:48 +03:00
3252a8 bea62d75a9 fix: persist account language changes 2026-05-28 22:07:43 +03:00
3252a8 e998b9ddd1 chore: compact traffic cards 2026-05-28 21:52:38 +03:00
3252a8 2f192447ce fix: hide premium traffic when regular limit is depleted 2026-05-28 21:00:06 +03:00
3252a8 e89ed870dc fix: compact mobile subscription status card 2026-05-28 19:44:50 +03:00
3252a8 6a722bfa17 refactor: share email code confirmation screen 2026-05-28 17:11:15 +03:00
3252a8 a96aaa763f feat: use admin identity in demo auth flow 2026-05-28 17:02:22 +03:00
3252a8 bbe05d7f57 feat: support telegram auth in demo flow 2026-05-28 16:49:23 +03:00
3252a8 da28b69461 feat: add demo auth flow 2026-05-28 16:40:16 +03:00
3252a8 cd469ae2bb feat: add demo device top-up flow 2026-05-28 15:54:35 +03:00
3252a8 9af49453f8 fix: refresh devices after billing updates 2026-05-28 15:54:30 +03:00
3252a8 e543665704 fix: add favicon to docs demo shell 2026-05-28 15:32:45 +03:00
3252a8 39c0696e53 fix: show activation dialog for forced actions 2026-05-28 15:29:58 +03:00
3252a8 c6cc4ab963 fix: use standard purchase copy in demo 2026-05-28 15:25:18 +03:00
3252a8 4dfe71f392 fix: align demo state flows 2026-05-28 15:21:55 +03:00
3252a8 cf05c6580d fix: keep demo mock switch on app shell 2026-05-28 15:05:48 +03:00
3252a8 d544ebd879 feat: add fullscreen docs demo routes 2026-05-28 14:59:54 +03:00
3252a8 7e0e8ff319 feat: enrich docs demo mock data 2026-05-28 14:59:43 +03:00
3252a8 613a9860a0 refactor: support prefixed webapp routes 2026-05-28 14:59:34 +03:00
3252a8 92f0277dad docs: refresh docs demo data 2026-05-28 13:10:07 +03:00
3252a8 749693078b fix: use app favicon fallback in admin appearance 2026-05-28 13:10:01 +03:00
3252a8 b566a34725 fix: mark depleted traffic on webapp home 2026-05-28 13:09:55 +03:00
3252a8 3ab6c18a76 fix: preserve account language on telegram auth 2026-05-28 11:31:45 +03:00
3252a8 52458eb223 fix: align tariff action in status card 2026-05-28 11:31:41 +03:00
3252a8 790946d89e fix: label docs demo admin version 2026-05-28 10:09:26 +03:00
3252a8 30c5d9ae11 fix: refine home tariff actions 2026-05-28 10:06:10 +03:00
3252a8 12e60629fe fix: preserve admin view on language change 2026-05-28 09:54:27 +03:00
3252a8 6c0d2932c3 docs: make demo bar collapsible on mobile 2026-05-28 09:12:58 +03:00
3252a8 ce19c7e2d1 docs: add mobile docs navigation menu 2026-05-28 08:21:58 +03:00
3252a8 93c9dde572 chore: harden docs demo config fetch 2026-05-28 08:21:54 +03:00
3252a8 d46324cb6a docs: fix mobile homepage navigation 2026-05-28 08:07:09 +03:00
3252a8 f153ca5bf4 fix: complete english localization refresh 2026-05-28 00:55:22 +03:00
3252a8 62e950f5c4 docs: refine static demo experience 2026-05-28 00:55:11 +03:00
3252a8 2b8bcd10b8 docs: make demo open fullscreen 2026-05-28 00:19:22 +03:00
3252a8 4c2ee19957 docs: add interactive demo page 2026-05-27 23:56:52 +03:00
3252a8 8217d13cd6 feat: add docs demo runtime build 2026-05-27 23:56:48 +03:00
3252a8 a2a887b898 fix: recognize backups admin route 2026-05-27 23:56:41 +03:00
3252a8 1620de9a01 refactor: isolate mock runtime from production webapp 2026-05-27 23:56:37 +03:00
3252a8 b6c6887842 chore: improve backup admin controls 2026-05-27 23:22:20 +03:00
3252a8 ef4b493e65 chore: change backup archive name style 2026-05-27 23:01:42 +03:00
3252a8 50dd1951c4 fix: prevent support message row stretching 2026-05-27 22:37:50 +03:00
3252a8 b3894e53e4 fix: tighten support chat message spacing 2026-05-27 22:21:44 +03:00
3252a8 ded044b4c0 fix: make support replies finish promptly 2026-05-27 22:21:24 +03:00
3252a8 bce78c4f28 chore: backup warning details 2026-05-27 19:08:49 +03:00
3252a8 75586d8883 fix: harden hwid provider payment edge cases 2026-05-27 18:31:14 +03:00
3252a8 f2fc335221 fix: anchor hwid pricing to paid period 2026-05-27 18:13:27 +03:00
3252a8 1c9e55d797 fix: wire subscription service for yookassa hwid payments 2026-05-27 18:13:22 +03:00
3252a8 25056602d8 fix: serialize webapp datetime payloads 2026-05-27 14:55:05 +03:00
3252a8 bd2e67059f chore: optimize docker layer caching 2026-05-27 14:44:58 +03:00
3252a8 fe34edfe73 chore: verify formatting checks 2026-05-27 14:29:45 +03:00
3252a8 4706be53ab feat: manual backup button 2026-05-27 14:25:37 +03:00
3252a8 4bd547f06a chore: tune backup function and related docs 2026-05-27 14:11:22 +03:00
3252a8 0250264fa0 fix: serialize HWID top-up validity dates 2026-05-27 13:59:03 +03:00
3252a8 3aede8fe95 feat: add backups feature 2026-05-27 13:53:30 +03:00
3252a8 e90988ea5c feat: persist support ticket drafts 2026-05-27 08:10:08 +03:00
3252a8 69d3400310 fix: stack admin badges on mobile 2026-05-27 07:55:05 +03:00
3252a8 6a44da1f31 fix: refresh trial settings in admin tariffs 2026-05-27 07:52:20 +03:00
3252a8 77c8785ec0 chore: set default webapp title to minishop 2026-05-27 07:22:34 +03:00
3252a8 f2c0a9f6d5 fix: preserve home logo scale after admin navigation 2026-05-27 07:13:53 +03:00
3252a8 f77a6ea46d feat: add default webapp brand assets 2026-05-27 07:07:31 +03:00
3252a8 a92ad32b23 docs: update docs 2026-05-27 00:11:24 +03:00
3252a8 a6af8f8415 docs: tune docs visual 2026-05-26 23:55:07 +03:00
3252a8andGitHub da9db7b7f8 Merge pull request #5 from 3252a8/dev
Update and structurize docs
2026-05-26 23:42:59 +03:00
3252a8 11048a6ed8 docs: refactor docs structure 2026-05-26 23:31:04 +03:00
3252a8 c3381bdd31 chore: add image publish and mirror sync scripts 2026-05-26 22:30:10 +03:00
3252a8 53f1cec401 docs: point repository links to GitHub 2026-05-26 22:21:00 +03:00
3252a8 bf46a15446 docs: update GitLab repository links 2026-05-26 21:57:49 +03:00
3252a8 0df52d0235 docs: refactor docs structure 2026-05-26 21:45:44 +03:00
3252a8 0c167c8f09 docs: refactor docs structure 2026-05-26 17:26:46 +03:00
3252a8 804ccdabec docs: use nova starlight theme 2026-05-26 16:18:15 +03:00
3252a8 5833b18052 docs: docs-site initial 2026-05-26 16:04:04 +03:00
3252a8 f30e729f1f Merge branch 'dev' into 'main'
Install guides, payment fixes, and runtime translations

See merge request 3252a8/remnawave-minshop!1
2026-05-26 10:39:29 +00:00
3252a8 1e63431ae3 chore: merge main into dev 2026-05-26 12:51:00 +03:00
3252a8 072e7273c4 docs: standardize MIT license file 2026-05-26 12:48:58 +03:00
3252a8 df079138ee docs: standardize MIT license file 2026-05-26 12:44:27 +03:00
3252a8 87f32114d7 docs: update license file attribution 2026-05-26 12:29:58 +03:00
3252a8 b330e604f6 chore: use Docker Hub images in compose files 2026-05-26 11:15:10 +03:00
3252a8 827d69231c feat: add Docker Hub image script 2026-05-26 08:58:09 +03:00
3252a8 0623850c3f fix: open Telegram Stars invoices inside Mini App 2026-05-25 22:13:11 +03:00
3252a8 4c77d129e7 fix: update support page content without page refresh 2026-05-25 22:03:30 +03:00
3252a8 c68cb97964 fix: bind HWID top-ups to subscription periods 2026-05-25 19:33:50 +03:00
3252a8 59fa301344 feat: add runtime locale overrides 2026-05-25 17:47:45 +03:00
3252a8 23784d00fe chore: update custom themes styles 2026-05-25 13:52:25 +03:00
3252a8 17cff74b7c chore: tune sub activate modal view 2026-05-25 13:11:48 +03:00
3252a8 2a3a3c21ba chore: adjust webapp payment layout 2026-05-25 12:42:17 +03:00
3252a8 ba7811621e fix: preserve premium topup squad access 2026-05-25 12:18:58 +03:00
3252a8 340afe1b80 fix: stop premium squad sync churn 2026-05-25 11:54:03 +03:00
3252a8 11b7823188 fix: expire Wata payment links sooner 2026-05-25 11:34:10 +03:00
3252a8 de1763cfe1 feat: enable payment provider for admin only 2026-05-25 11:19:54 +03:00
3252a8 d30b069876 fix: reuse pending Wata payment links on retry 2026-05-25 10:49:57 +03:00
3252a8 0d68da9624 fix: handle Wata payment links 2026-05-25 09:17:39 +03:00
3252a8 4d857b386b fix: remove inline startup panel sync 2026-05-25 00:40:24 +03:00
3252a8 be7a3bc153 fix: queue bot panel sync requests 2026-05-25 00:36:15 +03:00
3252a8 5c71fc0de2 fix: hide links from payment success message 2026-05-25 00:23:48 +03:00
3252a8 82cc33587c fix: stop panel description churn 2026-05-25 00:06:29 +03:00
3252a8 121f3c6ddf feat: add compact panel sync update diagnostics 2026-05-24 23:44:29 +03:00
3252a8 3410eeaec1 fix(admin): delete Remnawave user with bot account 2026-05-24 23:35:19 +03:00
3252a8 85ecd644b8 fix: refresh webapp profile after activation 2026-05-24 23:25:21 +03:00
3252a8 7cffb4667f fix: expose iOS home screen icons for web app 2026-05-24 23:13:15 +03:00
3252a8 0449ded505 feat: render webapp preview metadata 2026-05-24 22:54:01 +03:00
3252a8 e15cbffdb1 feat: expose webapp title in admin settings 2026-05-24 22:53:50 +03:00
3252a8 9674c674a2 feat: resume activation handoff after payments 2026-05-24 22:45:54 +03:00
3252a8 bafc5f4709 feat: show linked emails in telegram logs 2026-05-24 22:32:35 +03:00
3252a8 a60de46173 feat: guide users after subscription activation 2026-05-24 22:24:02 +03:00
3252a8 1165dc3fa6 chore: verify formatting checks 2026-05-24 22:04:12 +03:00
3252a8 f5ab18a67b feat: redesign webapp trial offer 2026-05-24 22:02:22 +03:00
3252a8 65db4aaff6 fix: clean legacy emails from panel descriptions 2026-05-24 21:45:38 +03:00
3252a8 d9a23235e5 fix: stop syncing email in panel descriptions 2026-05-24 21:36:00 +03:00
3252a8 00b54e1f15 feat: improve admin trial settings controls 2026-05-24 19:19:39 +03:00
3252a8 3c4ff66150 feat: streamline admin tariff settings 2026-05-24 19:10:36 +03:00
3252a8 2b3078fcce chore: fix formatting checks 2026-05-24 18:51:37 +03:00
3252a8 6f4074e8d6 feat: surface trial activation in mini app 2026-05-24 18:48:01 +03:00
3252a8 235ee25d9f fix: refresh YooKassa webapp payments 2026-05-24 18:47:37 +03:00
3252a8 d1b9990bac feat: manage trial settings on tariffs page 2026-05-24 18:46:23 +03:00
3252a8 60d1ba4efc fix: handle Wata prepayment webhooks 2026-05-24 14:04:21 +03:00
3252a8 29b6e6eb24 fix: keep payment user cards in payments 2026-05-24 09:32:47 +03:00
3252a8 aa65972483 feat: add admin payment detail view 2026-05-24 09:30:58 +03:00
3252a8 ad275a7c83 feat: polish admin settings layout 2026-05-24 09:11:43 +03:00
3252a8 02b57ead00 feat: separate legacy tariff settings 2026-05-24 09:11:34 +03:00
3252a8 7f484fa7b3 feat: warn about legacy tariff settings 2026-05-24 00:05:16 +03:00
3252a8 3ea906a74d feat: group Platega admin settings 2026-05-24 00:01:58 +03:00
3252a8 939c40ccbf feat: collapse admin settings by default 2026-05-24 00:01:18 +03:00
3252a8 f0eb291f56 feat: tune visual of install instructions page 2026-05-23 23:55:06 +03:00
3252a8 46391b10e2 feat: tune deeplink fallback page 2026-05-23 23:26:10 +03:00
3252a8 5918a6cc71 fix: open install guide deeplinks via external app gateway 2026-05-23 23:07:42 +03:00
3252a8 0eeabc7b3a fix: process yookassa hwid device topups 2026-05-23 22:28:05 +03:00
3252a8 31eb5c06ad fix: repair linked panel email during sync 2026-05-23 22:27:51 +03:00
3252a8 60d8c297f9 docs: update deploy examples 2026-05-23 22:01:40 +03:00
3252a8 dab70d5a97 docs: update install guides documentation 2026-05-23 16:22:57 +03:00
3252a8 520a6289a8 Merge branch 'feature/install-page' into dev 2026-05-23 16:16:46 +03:00
3252a8 7adca53116 feat: enable bot install guides by default 2026-05-23 16:05:41 +03:00
3252a8 fbc3e6c084 chore: fix frontend formatting checks 2026-05-23 16:01:07 +03:00
3252a8 19ba5c8f11 feat: support install guide app deeplinks 2026-05-23 09:52:26 +03:00
3252a8 ce0d4dccf7 feat: open bot install guides in mini app 2026-05-23 09:48:30 +03:00
3252a8 7c874cd4aa feat: support install guides in custom themes 2026-05-22 23:39:17 +03:00
3252a8 17224b4f74 feat: harden public install guide loading 2026-05-22 23:29:58 +03:00
3252a8 40414264be feat: polish install guide loading state 2026-05-22 23:24:59 +03:00
3252a8 d9a4a007e2 feat: add install guide share tokens and animations 2026-05-22 23:20:25 +03:00
3252a8 d3925cad22 Merge branch 'dev' into feature/install-page
# Conflicts:
#	backend/bot/app/web/admin_api_impl/settings.py
#	backend/bot/app/web/webapp/cache_helpers.py
#	frontend/src/admin/sections/SettingsSection.svelte
#	tests/test_admin_settings_manifest_i18n.py
2026-05-22 22:57:24 +03:00
3252a8 443e2e62db fix: auto-merge duplicate panel identities 2026-05-22 22:39:45 +03:00
3252a8andGitHub 4b7c58a6c9 Merge pull request #1 from 3252a8/dev
Stabilize account linking, auth, and admin settings
2026-05-22 22:24:07 +03:00
3252a8 19aa2ec9c9 fix: log account merge notifications 2026-05-22 22:02:28 +03:00
3252a8 c4c5b8e3a0 fix: merge active sub email account with expired sub telegram account 2026-05-22 18:23:38 +03:00
3252a8 33a7dbc0e6 fix: app version display pattern in admin panel sidebar 2026-05-22 18:23:11 +03:00
3252a8 72921b9a8f fix: telegram oauth in logged in email account 2026-05-22 17:48:17 +03:00
3252a8 835436fa1a fix: ensure web app pay button is spawning when enable payment provider 2026-05-22 16:29:59 +03:00
3252a8 648f4ba4bc feat: show payment provider webhook urls 2026-05-22 15:57:49 +03:00
3252a8 f5023dc46b fix: repair missing panel user references 2026-05-22 15:39:51 +03:00
3252a8 4c0a798050 fix: load admin assets from stable paths 2026-05-22 15:39:45 +03:00
3252a8 cfe7c4ec5f fix: style mobile admin button 2026-05-22 15:11:28 +03:00
3252a8 c82779e15b fix: restore telegram mini app auth 2026-05-22 14:47:01 +03:00
3252a8 9e56715e77 fix: cover email telegram account linking 2026-05-22 14:41:56 +03:00
3252a8 2254b9ad19 fix: avoid repeated panel identity syncs 2026-05-22 14:11:38 +03:00
3252a8 81707d9c7c fix: clean merged panel identities 2026-05-22 14:11:31 +03:00
3252a8 9e61a3d8a8 feat: install instruction inside web app 2026-05-22 13:58:34 +03:00
3252a8 8a38524774 docs: add url to remnawave 2026-05-21 23:09:22 +03:00
3252a8 87d3f1b410 docs: add nginx example 2026-05-21 22:40:53 +03:00
3252a8 1094a65852 Merge branch 'dev' 2026-05-21 14:24:44 +03:00
3252a8 0ab4b11f97 fix: windows 95 theme support tickets list 2026-05-21 14:05:54 +03:00
3252a8 6d4fb5888f refactor: apply custom theme without cache reload 2026-05-21 13:56:05 +03:00
3252a8 e7301e2c48 refactor: optimize support polling 2026-05-21 13:41:01 +03:00
3252a8 f69f6546f0 docs: add details to env.example 2026-05-21 11:39:07 +03:00
3252a8 7fe53993aa docs: update docs and minimize env.example 2026-05-21 11:26:59 +03:00
3252a8 028f0680c6 feat: allow to edit remnawave panel settings in admin panel, ensure using i18n 2026-05-21 11:26:03 +03:00
3252a8 b3f67a5398 fix: admin panel loading 2026-05-21 10:43:55 +03:00
3252a8 db3611487e perf: optimize webapp asset delivery, split admin webapp bundle 2026-05-21 10:17:23 +03:00
3252a8 f2c722bdfc perf: reduce webapp and admin cache stampedes 2026-05-21 09:44:50 +03:00
3252a8 ccb125ab9d fix: retry Telegram startup setup until reachable 2026-05-21 08:28:27 +03:00
3252a8 a300a4a9c0 feat: add new sections to custom themes 2026-05-21 08:22:40 +03:00
3252a8 044cb4de7e refactor(redis): strengthen shared cache invalidation 2026-05-21 06:26:48 +03:00
3252a8 3405d12696 refactor: improve admin stats cache usage and etc 2026-05-20 23:35:45 +03:00
3252a8 11cd35373e fix: remove unnecessary caption in premium squad servers list in web app 2026-05-20 23:25:18 +03:00
3252a8 5001185bf8 refactor: improve panel sync performance 2026-05-20 23:18:03 +03:00
3252a8 a7f298743d refactor: improve startup and sync performance 2026-05-20 23:04:29 +03:00
3252a8 8192eaf55b refactor: improve premium squads feature performance, add benchmarks 2026-05-20 22:31:37 +03:00
3252a8 5a0e0033ec chore: rename tg support button contact 2026-05-20 22:05:33 +03:00
3252a8 7521f89ffd feat: show optional serivce description before payment 2026-05-20 21:55:22 +03:00
3252a8 d8a1da1f13 feat: add support tickets and imrpove web app loading 2026-05-20 16:38:24 +03:00
3252a8 3b846f0d44 fix: tune visual of setup password button 2026-05-19 15:38:04 +03:00
3252a8 f5006af6c0 feat: email and password login 2026-05-19 15:21:02 +03:00
3252a8 3152631911 fix: preserve Telegram usernames with underscores 2026-05-19 00:07:32 +03:00
3252a8 1d9f069f45 feat: add admin telegram profile links and avatar preview 2026-05-18 23:46:46 +03:00
3252a8 707f569f62 fix: heleket signature verify 2026-05-18 22:56:56 +03:00
3252a8 e3643ee9a0 fix: handle Caddy-proxied payment webhooks and method selection 2026-05-18 22:45:42 +03:00
3252a8 49cf5ebad8 fix: auto-append new providers to PAYMENT_METHODS_ORDER 2026-05-18 22:03:40 +03:00
3252a8 b79f5f4d7d fix: make provider services reactive to runtime config changes 2026-05-18 21:53:53 +03:00
3252a8 d870915dc0 fix: apply persisted provider overrides on startup 2026-05-18 21:40:45 +03:00
3252a8 fcd494f815 fix: restore settings binding in platega webapp payment creator 2026-05-18 21:23:57 +03:00
3252a8 a7728f80d0 fix: resolve yookassa webhook path via provider spec in startup log 2026-05-18 21:13:30 +03:00
3252a8 9eb6387973 feat: expose provider button defaults in admin settings manifest 2026-05-18 20:59:26 +03:00
3252a8 7be5510208 refactor: move yookassa env-config into module 2026-05-18 20:46:54 +03:00
3252a8 e0269bbf86 refactor: move platega env-config into module 2026-05-18 20:37:15 +03:00
3252a8 e76558d68b refactor: move stars presentation into module 2026-05-18 20:30:30 +03:00
3252a8 cceba2e98e refactor: move cryptopay env-config into module 2026-05-18 16:32:19 +03:00
3252a8 1e93d25a40 refactor: move freekassa env-config into module 2026-05-18 16:22:49 +03:00
3252a8 74ef9d31b4 refactor: move wata env-config into module 2026-05-18 16:18:04 +03:00
3252a8 15b3f9c084 refactor: move severpay env-config into module 2026-05-18 16:13:58 +03:00
3252a8 87efa6c77b refactor: provider env-config lives in modules (heleket poc) 2026-05-18 16:03:01 +03:00
3252a8 07b35036e7 refactor: keep provider presentation self-contained in spec 2026-05-18 15:35:55 +03:00
3252a8 c9c12b9ed6 feat: add heleket payment provider 2026-05-18 15:26:07 +03:00
3252a8 960754c54e fix: fix menus and modal views after payment providers refactor 2026-05-18 11:01:32 +03:00
3252a8 51c9c8b4f0 refactor: payments providers 2026-05-18 10:29:00 +03:00
3252a8 fff7e90e14 feat: add wata payment provider 2026-05-17 23:09:04 +03:00
3252a8 11429e887e Merge branch 'dev' 2026-05-17 22:23:14 +03:00
3252a8 820548cb2d fix: include git tag context in startup banners 2026-05-17 22:20:45 +03:00
3252a8 e2038085ca fix: show commit SHA in startup banners 2026-05-17 22:11:17 +03:00
3252a8 3676bf1ae7 Merge branch 'feature/arch-refactoring' into dev 2026-05-17 21:41:55 +03:00
3252a8 57f766dda2 docs: add themes screenshot 2026-05-17 21:41:36 +03:00
3252a8 da8f2d08df docs: caddy update 2026-05-17 21:36:32 +03:00
3252a8 9579f019d4 chore: lock migration doc and script in sync with current compose layout 2026-05-17 18:08:08 +03:00
3252a8 7c27337e0a chore: extend KNOWN_CONTAINERS to cover split-arch containers 2026-05-17 18:08:08 +03:00
3252a8 2dffce4244 docs: update migration guide for v2.7.0 to v3.4+ split-architecture upgrade 2026-05-17 18:08:08 +03:00
3252a8 27a9f0aff8 chore: cover env, baked file and live git paths of _resolve_app_version 2026-05-17 17:38:35 +03:00
3252a8 67920040ea fix: bake app version into image via throwaway version-builder stage 2026-05-17 17:38:30 +03:00
3252a8 fd1b910236 chore: cover can_topup_devices flag derivation 2026-05-17 17:26:49 +03:00
3252a8 09ba53d185 fix: stop rendering duplicate period grid and pay button in legacy mode 2026-05-17 17:26:42 +03:00
3252a8 91e79388d1 fix: hide device topup button when no HWID packages or unlimited devices 2026-05-17 17:26:37 +03:00
3252a8 cb5d59571e chore: cover referral bonus skip-paths, inviter and referee award flows 2026-05-17 10:23:01 +03:00
3252a8 365c6c7858 chore: cover HWID device topup gating, audit and panel failure paths 2026-05-17 10:22:57 +03:00
3252a8 6886a90ee4 chore: cover topup, premium-topup and tariff-switch panel failure paths 2026-05-17 10:22:53 +03:00
3252a8 7d1c9ee373 fix: surface panel update failures in topup, premium-topup and tariff switch 2026-05-17 10:22:48 +03:00
3252a8 dc05595b9b chore: attach orphaned type:ignore comment to its statement 2026-05-17 10:09:41 +03:00
3252a8 d2581b1c52 chore: pin yookassa and subscription wiring in build_core_services 2026-05-17 10:03:09 +03:00
3252a8 eb37ed4c74 refactor: stop swallowing critical service wiring failures 2026-05-17 10:03:05 +03:00
3252a8 6800734136 chore: cover auto-renew wiring contract end-to-end 2026-05-17 09:58:45 +03:00
3252a8 d4b7da3a54 fix: read yookassa_service directly in auto-renew instead of broken local import 2026-05-17 09:58:39 +03:00
3252a8 8382fa9232 chore: cover provider label regression in payment success email path 2026-05-17 09:46:52 +03:00
3252a8 071b9f2b25 fix: add missing _PROVIDER_LABELS to PaymentContextMixin 2026-05-17 09:46:48 +03:00
3252a8 1e8ffa15d0 chore: add tests for redis infra, webhook queue and split-arch entrypoints 2026-05-17 00:31:12 +03:00
3252a8 fc7f97c136 refactor: drop unused legacy run_bot flags and dead backend/main.py 2026-05-17 00:31:08 +03:00
3252a8 99732211b7 docs: correct backend path prefix for admin manifest references 2026-05-17 00:30:59 +03:00
3252a8 aaa8e957f6 fix: serve hashed minified webapp bundle from frontend image 2026-05-17 00:30:54 +03:00
3252a8 30fb774d93 refactor: project architecture refactor, container splitting 2026-05-17 00:01:28 +03:00
3252a8 e0b5218037 chore: print remnawave-minishop ascii banner on startup 2026-05-16 00:16:21 +03:00
3252a8 387bdb6abc fix: stop logging full panel response bodies by default to avoid secret leakage 2026-05-16 00:09:37 +03:00
3252a8 3298dc3e77 test: align CSP expectation with current img-src that allows blob 2026-05-16 00:04:42 +03:00
3252a8 55dce99d34 refactor: ack panel webhooks immediately and run event handling in bounded background task 2026-05-16 00:02:22 +03:00
3252a8 87f9e23bae refactor: cache panel squad and host lookups in-memory with TTL 2026-05-16 00:01:40 +03:00
3252a8 1e97dd9fe5 refactor: add per-chat throttle in message queue to avoid 429 on rapid same-chat sends 2026-05-15 23:59:49 +03:00
3252a8 a05c54fc66 refactor: tighten panel API timeouts and retry safe requests once on transient errors 2026-05-15 23:59:05 +03:00
3252a8 aeb51b9ccc refactor: add indexes on payments(status, created_at) and message_logs(timestamp) 2026-05-15 23:57:39 +03:00
3252a8 b72a23f1e1 refactor: parallelize tariff worker panel calls and unblock startup sync 2026-05-15 23:18:24 +03:00
3252a8 4d96a3e646 Merge branch 'feature/custom-themes' into dev 2026-05-15 23:10:27 +03:00
3252a8 fc8e573243 feat: tune light theme, reduce accent color usage overall 2026-05-15 23:01:23 +03:00
3252a8 f80e336e1f feat: tune visual of windows 95 theme 2026-05-15 21:46:10 +03:00
3252a8 2a1ff8e45e feat: separate webapp favicon configuration in appearance admin panel section 2026-05-15 19:27:33 +03:00
3252a8 e9ad259957 docs: update web app theme specific docs 2026-05-15 15:46:08 +03:00
3252a8 86ddceb646 feat: custom themes polishing 2026-05-15 14:38:39 +03:00
3252a8 a1725a6872 feat: custom themes logo size tuning 2026-05-15 13:54:47 +03:00
3252a8 af7cee5014 feat: custom themes polishing 2026-05-15 11:17:27 +03:00
3252a8 b6ee5e8790 feat: custom themes initial 2026-05-14 16:33:50 +03:00
3252a8 46520fb37c Merge branch 'dev' 2026-05-13 23:07:39 +03:00
3252a8 18ba00c98a feat: show user logs in admin panel user card 2026-05-13 22:51:04 +03:00
3252a8 e065c85a3e docs: update docs 2026-05-13 22:34:24 +03:00
3252a8 7401649841 refactor: split backend domains and add API behavior coverage 2026-05-13 18:38:10 +03:00
3252a8 a96007d48a fix: avoid redundant premium squad restore updates 2026-05-13 15:11:56 +03:00
3252a8 2169fd8b50 chore: harden webapp auth and update deps 2026-05-13 15:05:30 +03:00
3252a8 2b7d1f0be8 chore: fix lint warnings 2026-05-13 14:13:39 +03:00
3252a8 eec5635541 refactor: brand logo customization and emoji font selection 2026-05-13 14:02:35 +03:00
3252a8 e21a0758c3 fix: user card open in payments list for email only users 2026-05-12 23:53:35 +03:00
3252a8 defbac43d7 feat: tune admin panel visual, add animations, expand dashboard 2026-05-12 23:40:41 +03:00
3252a8 94612e11f2 fix: fix unlimited premium squads applying via admin panel 2026-05-12 22:01:11 +03:00
3252a8 11187487b4 chore: run lint and prettifier 2026-05-12 21:54:12 +03:00
3252a8 f31540afdb fix: dedupe traffic billing warnings when panel limit drifts 2026-05-12 21:32:47 +03:00
3252a8 6afbb72a34 feat: lock traffic topup when under 80% of traffic used 2026-05-12 21:27:37 +03:00
3252a8 af66103111 docs: update docs 2026-05-12 16:34:23 +03:00
3252a8 92277b2e27 feat: show and filter premium squad status in admin panel users list 2026-05-12 16:14:11 +03:00
3252a8 475a1953d1 feat: tune visual of web app admin panel, tune limit messages and etc 2026-05-12 15:42:17 +03:00
3252a8 8af59750c6 fix(tariff-worker): match premium bandwidth stats by username, not uuid 2026-05-12 11:20:23 +03:00
3252a8 00a0792c35 fix: reconcile legacy database schema for admin user detail API 2026-05-12 11:07:44 +03:00
3252a8 b9c0dfb9a1 fix: locales in admin panel settings page 2026-05-12 10:31:21 +03:00
3252a8 9679190709 feat: ability to turn off logs for admins users 2026-05-12 10:16:59 +03:00
3252a8 c8d4b3565c feat: tune visual of web app admin panel 2026-05-12 09:59:15 +03:00
3252a8 fe976022ec refactor: migrate ui elements to lib 2026-05-12 09:05:22 +03:00
3252a8 6a367c1dec refactor: consolidate web app UI foundation 2026-05-11 23:25:34 +03:00
3252a8 9f338c3416 feat: tune visual of web app admin panel 2026-05-11 23:06:16 +03:00
3252a8 a575cb057b refactor: slice web app wip 2026-05-11 22:32:23 +03:00
3252a8 116b0a5c08 fix: tune tariffs folder 2026-05-11 22:12:19 +03:00
3252a8 ed6b214b29 refactor: slice web app wip 2026-05-11 21:41:31 +03:00
3252a8 7d8bbbc904 refactor: slice web app wip 2026-05-11 08:31:07 +03:00
3252a8 763c4ad1bb refactor: slice web app wip 2026-05-11 07:57:34 +03:00
3252a8 5243cc4284 docs: update docs 2026-05-11 06:34:45 +03:00
3252a8 5a015fe8b4 Merge branch 'feature/new-tariffs' into dev 2026-05-11 06:10:36 +03:00
3252a8 b759ddb0f5 feat: tune premium squads visual in web app 2026-05-11 00:15:29 +03:00
3252a8 57821ba2a9 feat: tune visual of web app admin panel 2026-05-10 23:36:05 +03:00
3252a8 410cb89f3e fix: do not change squad for referrer 2026-05-10 21:57:36 +03:00
3252a8 e01f56bc34 feat: tune visual of web app admin panel 2026-05-10 20:19:43 +03:00
3252a8 ff59eda3d7 feat: web app setting reload without container reloading 2026-05-10 18:07:20 +03:00
3252a8 b7e497e241 Merge branch 'dev' into feature/new-tariffs 2026-05-10 13:18:39 +03:00
3252a8 7a81a2f20b fix: telegram oauth 2026-05-10 13:17:21 +03:00
3252a8 990eba1e1e feat: premium squads inside one tariff 2026-05-10 12:51:53 +03:00
3252a8 6f9a87cf98 feat: admin dashboard 2026-05-09 22:32:44 +03:00
3252a8 0a9d929181 docs: update docs 2026-05-08 18:14:30 +03:00
3252a8 7dc4806be7 Merge branch 'dev' into feature/new-tariffs 2026-05-07 22:23:04 +03:00
3252a8 8783129c16 fix: webapp accesability when tg is unreachable 2026-05-06 21:39:11 +03:00
3252a8 50d78c8746 feat: hide tariff options and etc when only one exist 2026-05-04 21:50:44 +03:00
3252a8 63784bc036 fix: use limited tag for mark exhausted traffic users 2026-05-03 22:46:12 +03:00
3252a8 aa5a9496ed feat: hwid limits and topups 2026-05-03 22:36:33 +03:00
3252a8 00f8e5fb20 Merge branch 'dev' into feature/new-tariffs
# Conflicts:
#	bot/services/stars_service.py
2026-05-03 22:19:04 +03:00
3252a8 ca8df10eab fix: telegram stars payment in web app 2026-05-03 18:10:20 +03:00
3252a8 b461838c0d Merge branch 'dev' into feature/new-tariffs 2026-05-02 13:21:11 +03:00
3252a8 50857d5283 fix: lang select dropdown wasn't work on android 2026-05-02 13:20:42 +03:00
3252a8 449bc0c780 Merge branch 'dev' into feature/new-tariffs 2026-05-01 22:51:36 +03:00
3252a8 e1223f8c57 fix: connect button disappeard in web app 2026-05-01 22:48:27 +03:00
3252a8 7822ec1b09 Merge branch 'dev' into feature/new-tariffs 2026-05-01 22:37:58 +03:00
3252a8 81f5168726 chore: improve telegram bot menu navigation and referrals 2026-05-01 22:16:00 +03:00
3252a8 77f2420ae3 Merge branch 'dev' into feature/new-tariffs 2026-04-30 22:58:12 +03:00
3252a8 4de730bf5a feat: add Telegram OAuth login flow 2026-04-30 19:50:17 +03:00
3252a8 77da0d97c0 chore: tariffs 2026-04-30 18:57:10 +03:00
3252a8 e8b395cf2f Merge branch 'dev' into feature/new-tariffs 2026-04-30 14:05:19 +03:00
3252a8 d15b1be3b9 fix: click on another button when switching lang 2026-04-30 14:05:01 +03:00
3252a8 8a6b2704d4 feat: new tariffs web app and bot interactions 2026-04-30 13:48:32 +03:00
3252a8 ce610b4804 Merge branch 'dev' into feature/new-tariffs
# Conflicts:
#	.gitignore
#	bot/app/web/subscription_webapp.py
#	tests/test_settings.py
2026-04-30 10:52:20 +03:00
3252a8 4d0241a0ea fix: remove logo bg in email templates 2026-04-30 10:29:01 +03:00
3252a8 2c1e86863d feat: support hwid in web app 2026-04-30 09:25:43 +03:00
3252a8 16b11327ee fix: traffic strategy 2026-04-30 07:57:14 +03:00
3252a8 3d5998b8fb feat: support traffic and trial flow in web app 2026-04-30 07:34:29 +03:00
3252a8 2df4d2d770 feat: tune webapp visual 2026-04-30 00:06:31 +03:00
3252a8 4ce78f277e feat: migrate to svelte initial 2026-04-29 11:10:12 +03:00
3252a8 d7d5c2b1ef refactor: remove local storing tg js 2026-04-28 23:33:24 +03:00
3252a8 4958e1fdfd fix: delegate tariff resets to panel 2026-04-28 15:36:13 +03:00
3252a8 258b2f4dec feat: align tariff resets to calendar month 2026-04-28 15:25:46 +03:00
3252a8 338008bbd0 docs: document tariff catalog 2026-04-28 14:34:25 +03:00
3252a8 829758dc44 feat: update tariff UI and mini app 2026-04-28 14:34:12 +03:00
3252a8 9deb98bbe7 feat: add traffic periods topups and throttling 2026-04-28 14:34:00 +03:00
3252a8 26af92b5ac feat: route purchases through tariff catalog 2026-04-28 14:33:52 +03:00
3252a8 87b2443a06 feat: add tariff config and database schema 2026-04-28 14:33:38 +03:00
3252a8 aabc0e312d feat: log email and tg linking 2026-04-28 13:54:05 +03:00
3252a8 d15d58df36 docs: tariffs documentation 2026-04-28 10:47:59 +03:00
3252a8 21f2ac4534 fix: show red traffic bar in web app when sub is expired 2026-04-28 10:32:56 +03:00
3252a8 dd0e6e75af feat: add magic login link to email with login codes 2026-04-28 09:50:17 +03:00
3252a8 0251f939d7 feat: log users when reg via email 2026-04-27 23:29:28 +03:00
3252a8 a00bc0f345 feat: design for emails 2026-04-27 23:05:14 +03:00
3252a8 ae4ce43e1c chore: tune default web app logo 2026-04-27 22:28:34 +03:00
3252a8 19ed8916f4 fix: migration and docker image name/tag 2026-04-27 21:31:06 +03:00
3252a8 f7de08bfd7 docs: update readme 2026-04-27 21:06:21 +03:00
3252a8 6e42f67931 docs: donation wallet 2026-04-27 21:02:49 +03:00
3252a8 b87b6cd378 fix: webapp session 2026-04-27 20:50:24 +03:00
3252a8 b39fb73c30 docs: project name change, migration script 2026-04-27 16:11:06 +03:00
3252a8 3120884ed5 docs: update readme 2026-04-27 15:15:25 +03:00
3252a8 c927244b1a docs: update readme 2026-04-27 15:13:00 +03:00
3252a8 e049f38c7c fix: readme format 2026-04-27 15:08:29 +03:00
3252a8 ddc6ee0024 feat: build webapp when docker build 2026-04-27 14:59:35 +03:00
3252a8 fa5f1460ab fix: separate platega buttons not showed up in webapp 2026-04-27 14:57:49 +03:00
3252a8 9f556f3e04 fix: add platega debug 2026-04-27 14:48:58 +03:00
3252a8 70b618a94b feat: use web page for cryptopay in web app 2026-04-27 14:38:09 +03:00
3252a8 0610156f80 feat: add multiple platega buttons 2026-04-27 14:34:41 +03:00
3252a8 587b39de1d fix(bot): connect button opens subscription page instead of mini app
Previously the connect button in post-payment messages and in 'My subscription' section opened the mini app (personal cabinet) when SUBSCRIPTION_MINI_APP_URL was set, ignoring the actual subscription URL. Now the real subscription URL is preferred, with the mini app used only as a fallback.
2026-04-27 11:06:32 +03:00
3252a8 e35885b54b docs: update readme 2026-04-27 09:44:28 +03:00
3252a8 c1b7ace876 fix(webapp): merge bundled FALLBACK_I18N with server locales
The bot's locales/*.json files do not contain the webapp-specific keys
(connect, extend_subscription, loading, etc.), so applyI18n was
overwriting the localized HTML defaults with raw key names.

Merge the bundled FALLBACK_I18N table with whatever the server provides
per language, letting server values override but falling back to the
in-bundle translations for keys the bot does not ship.
2026-04-27 09:42:18 +03:00
3252a8 f416b0aed1 fix(webapp): allow oauth.telegram.org iframe in CSP
The Telegram Login Widget embeds oauth.telegram.org in an iframe; without
an explicit frame-src directive it fell back to default-src 'self' and was
blocked. Add frame-src https://oauth.telegram.org so the login flow loads.
2026-04-27 09:34:09 +03:00
3252a8 c8192e4427 fix(webapp): allow inline JSON config and Telegram widget eval under CSP
Add a per-request nonce to inline <script type="application/json"> blocks
(webapp-config, i18n) so they survive script-src 'self' and locales/config
actually load in the browser. Also add 'unsafe-eval' to script-src so the
vendored telegram-widget.js (which uses eval/new Function) can initialise.
2026-04-27 09:31:52 +03:00
3252a8 dae1a6889b feat(ops): add /healthz endpoint and container healthcheck
Expose a lightweight /healthz route on the main aiohttp server and
wire docker-compose healthcheck so orchestrators can detect failures.
2026-04-27 08:43:58 +03:00
3252a8 4740666d63 chore: annotate XTR provider_token and add missing return type 2026-04-27 08:43:54 +03:00
3252a8 d6b703debb refactor(logging): use logger.exception to preserve stack traces
Unify error logging across services: replace logger.error(f"...{e}")
and logger.error(..., exc_info=True) with logger.exception() so the
stack trace is consistently captured.
2026-04-27 08:43:48 +03:00
3252a8 f1113eb80a fix(webhooks): return 5xx on YooKassa processing errors
Replace 200-on-error with 500 so the payment provider retries transient
failures instead of treating them as successfully processed.
2026-04-27 08:43:44 +03:00
3252a8 604f0d9656 security: audit inline Mini App handlers 2026-04-26 20:34:02 +03:00
3252a8 8ce5a92625 chore: harden ignore rules 2026-04-26 20:33:59 +03:00
3252a8 72b6e93c94 docs: add Telegram clock-skew note for auth_date 2026-04-26 20:33:57 +03:00
3252a8 d60952718e security: harden Mini App auth, CSRF, and validation 2026-04-26 20:33:54 +03:00
3252a8 53dcc59770 security: double-check CryptoPay webhook signatures 2026-04-26 20:33:49 +03:00
3252a8 5ec179b6d6 webapp: add hashed minified asset pipeline 2026-04-26 20:04:30 +03:00
3252a8 250df445f0 docker: run app as non-root user 2026-04-26 19:47:56 +03:00
3252a8 12376e5070 db: add composite indexes and merge-user optimizations 2026-04-26 19:47:14 +03:00
3252a8 c2afc6107a webapp: harden mini app rendering and controls 2026-04-26 19:47:06 +03:00
3252a8 77370eb963 security: harden webhooks and session secrets 2026-04-26 19:46:57 +03:00
3252a8 94b0787cad feat: use i18n 2026-04-26 12:34:16 +03:00
3252a8 9c499fe3c2 feat: add caddy docker compose example 2026-04-24 23:16:31 +03:00
3252a8 7e26f9da9b feat: automatic merge two paid subs (email and tg) 2026-04-24 23:07:57 +03:00
3252a8 778615a97f feat: tune web app visual 2026-04-24 22:35:55 +03:00
3252a8 651572f15f refactor: show tg avatar in detached browser 2026-04-24 21:31:58 +03:00
3252a8 5ccb8ddabe refactor: promo and email bruteforce defence 2026-04-24 21:18:29 +03:00
3252a8 86f944e544 feat: tune web app visual 2026-04-24 21:04:53 +03:00
3252a8 bc29f5ebd6 feat: promocode and ref in web app 2026-04-24 13:52:53 +03:00
3252a8 9a84be85c6 feat: move tg bot interaction keyboard to separate command, show only web app and support buttons by default 2026-04-24 12:43:21 +03:00
3252a8 c08ac854b2 fix: get email from panel if exist, tune webapp visual 2026-04-24 11:14:50 +03:00
3252a8 807a8933b9 feat: email login, smtp codes 2026-04-23 21:20:32 +03:00
3252a8 b9cb1fec06 feat: tune webapp visual, use telegram widget for login 2026-04-23 14:06:21 +03:00
3252a8 eab803652b fix: tg bot wont start 2026-04-22 22:44:19 +03:00
3252a8 259d0646bc fix: tune webapp visual and fix some errors 2026-04-22 22:35:10 +03:00
3252a8 4f1b7d0832 feat: web app 2026-04-22 16:00:55 +03:00
3252a8 1aa529ab23 Remove GitHub Actions and stale GHCR defaults 2026-04-22 15:19:54 +03:00
3252a8 a46502380c Remove locales volume from bot 2026-04-03 20:49:14 +03:00
3252a8 85c276bcf5 Setup ghcr 2026-04-03 20:36:34 +03:00
3252a8 55f8db11ce Add setup instructions 2026-02-15 18:58:52 +03:00
3252a8 79af61a48e Add configurable welcome bonus days for referred signups
- add REFERRAL_WELCOME_BONUS_DAYS setting (default 3) in settings.py
- document REFERRAL_WELCOME_BONUS_DAYS in .env.example
- apply welcome bonus on first /start only for newly created users with referred_by_id
- replace hardcoded 3 days with settings.REFERRAL_WELCOME_BONUS_DAYS
- skip bonus flow when value is 0 or less
- send user notification after successful bonus application
- add i18n key referral_welcome_bonus_applied to ru.json and en.json
2026-02-11 13:26:56 +03:00
3252a8 69d2e2a899 Fix promo failed message when using deeplink 2026-02-10 19:53:55 +03:00
3252a8 069ad967b5 Add user ref page deeplink 2026-02-10 19:36:29 +03:00
3252a8 18a6b3e18d Tune users rating 2026-02-09 10:29:40 +03:00
3252a8 3b9043332c Move user links to id's from buttons 2026-02-09 10:22:06 +03:00
3252a8 d94b57bd0b User card links in ratings 2026-02-09 10:09:16 +03:00
3252a8 5247092ca2 Add users rating feature 2026-02-09 10:03:55 +03:00
3252a8 049789c9c5 Tune invite buttons 2026-02-06 12:48:37 +03:00
3252a8 6b74ae4a8e Update locals 2026-01-25 10:20:54 +03:00
3252a8 c13c01ea43 Update traffic limit info in user profile 2026-01-25 10:16:00 +03:00
3252a8 48e7d3569f Fix used traffic display 2026-01-25 10:11:16 +03:00
3252a8 4f3b45cbd8 Merge branch 'main' into fork-new
# Conflicts:
#	docker-compose.yml
2026-01-25 09:57:57 +03:00
kavoreandGitHub a7ec55d741 Merge pull request #152 from VAQYBIN/main
Add step-by-step install into README.md
2026-01-18 23:09:37 +03:00
VAQYBIN 0701af0f35 feat(docs): Добавлена пошаговая инструкция в README.md по установке бота 2026-01-19 01:06:44 +05:00
kavoreandGitHub c0851c5339 Merge pull request #151 from kavore/dev
bugfix
2026-01-17 23:55:25 +03:00
kavore 785b6c2d41 merge 2026-01-17 23:54:58 +03:00
kavore cce3fd4f58 bugfix #4 2026-01-17 23:23:06 +03:00
kavore 0dd6beebb2 remove nalogo and use custom client 2026-01-17 21:58:12 +03:00
kavore 8fb2a76698 bugfix 2026-01-17 21:25:02 +03:00
kavoreandGitHub 3d6713caa9 Merge pull request #150 from kavore/dev
bugfix and nalogo custom label
2026-01-17 21:11:41 +03:00
kavore 8d8fdce519 bugfix 2026-01-17 21:10:46 +03:00
kavore c385a1466c added custom label 2026-01-17 21:06:58 +03:00
kavoreandGitHub 8e1fdb33d2 Merge pull request #149 from kavore/dev
added nalogo
2026-01-17 16:18:38 +03:00
machka paslaandkavore 29222be1fa .env.example and docker-compose upd 2026-01-17 16:01:29 +03:00
machka pasla 08aca3b28d Add LOG_LEVEL env config and fix Nalogo receipt trigger 2026-01-14 16:03:21 +03:00
machka paslaandGitHub 658139d607 Merge pull request #148 from machka-pasla/lknpd
Lknpd
2026-01-14 14:54:31 +03:00
machka paslaandGitHub 1d4abf7977 Merge branch 'dev' into lknpd 2026-01-14 14:54:11 +03:00
3252a8 90ab186a7c Add platega payment request logging 2026-01-07 22:42:00 +03:00
machka pasla 62e8bc08d5 version update 2026-01-06 12:22:30 +03:00
3252a8 dc725ccd68 Update locales 2026-01-05 23:51:33 +03:00
3252a8 d88aee03bb Add user agreement and privacy policy links 2026-01-05 23:42:51 +03:00
machka pasla 475a30caf4 added my nalog management 2026-01-05 19:43:40 +03:00
machka paslaandGitHub c1593d7868 Merge pull request #145 from tdlcompiler/crypt4-fix 2025-12-30 07:00:41 +03:00
Denis Kartsev 0dbcfe3770 removed crypt4 prefix
panel api already returns a link with the prefix
2025-12-30 04:58:34 +03:00
machka pasla e3ffb87cfc updated crypt 2025-12-29 14:51:50 +03:00
machka pasla b4218ec6a7 Идемпотентная обработка вебхуков YooKassa 2025-12-26 13:44:51 +03:00
machka paslaandGitHub 48f8656ef8 Merge pull request #143 from machka-pasla/dev
bug fix in platega
2025-12-21 11:56:12 +03:00
machka pasla cee8491b3d bug fix 2025-12-21 11:55:49 +03:00
machka paslaandGitHub 4bc3f3b4b3 Merge pull request #142 from machka-pasla/dev
added severpay, platega, happ crypt4 support
2025-12-20 17:42:07 +03:00
machka pasla adb1105621 bug fix 2025-12-20 14:40:47 +03:00
machka pasla c8fd79e4ba shorten hwid 2025-12-20 09:41:10 +03:00
machka pasla 52bc9f71cc bug fix 2025-12-11 22:28:51 +03:00
machka pasla a1d3db0e16 added happ crypt4 links 2025-12-11 22:17:43 +03:00
machka pasla d0e34126e1 upd 2025-12-11 16:13:22 +03:00
machka pasla c96ebea077 upd 2025-12-11 13:51:54 +03:00
machka pasla f00a72ad15 yookassa fix 2025-12-11 12:59:39 +03:00
machka pasla 22171eb66f added gb packets selling 2025-12-11 12:47:32 +03:00
machka pasla 061fdeb72b removed tribute 2025-12-11 09:49:31 +03:00
machka pasla 8eba23574b some yookassa changes 2025-12-09 22:32:10 +03:00
machka pasla 7b650a74a6 severpay fix 2025-12-09 20:48:16 +03:00
machka pasla 805bd4cf60 readme upd 2025-12-09 20:21:46 +03:00
machka pasla 56a32d493f refactor 2025-12-09 20:17:46 +03:00
machka pasla b24a685066 added severpay.io 2025-12-09 19:50:14 +03:00
machka pasla fb06fbd0e1 added platega.io 2025-12-07 20:24:24 +03:00
machka pasla 719369057f fix privacy error 2025-12-04 12:13:17 +03:00
machka paslaandGitHub cea9d98155 Update and rename docker-compose-removed-server.yml to docker-compose-remote-server.yml 2025-11-28 13:16:02 +03:00
machka paslaandGitHub 6cf15e0698 Merge pull request #139
added external squad
2025-11-19 11:30:56 +03:00
machka pasla 2971b9ad2e added external squad 2025-11-19 11:30:34 +03:00
machka paslaandGitHub 3e86fc76b6 Merge pull request #135 from machka-pasla/dev
upd
2025-11-13 23:04:43 +03:00
machka paslaandGitHub 829a18715a Merge pull request #134 from machka-pasla/revert-133-revert-132-fix/sync-admin-duplicate-subscriptions
Revert "Revert "Устранены дубли активных записей, ошибка уникальности и улучшена идемпотентность""
2025-11-13 23:04:13 +03:00
machka paslaandGitHub 844c8e12a7 Revert "Revert "Устранены дубли активных записей, ошибка уникальности и улучшена идемпотентность"" 2025-11-13 23:03:56 +03:00
machka paslaandGitHub ef75f905f4 Merge pull request #133 from machka-pasla/revert-132-fix/sync-admin-duplicate-subscriptions
Revert "Устранены дубли активных записей, ошибка уникальности и улучшена идемпотентность"
2025-11-13 23:01:58 +03:00
machka paslaandGitHub cd816cbe5c Revert "Устранены дубли активных записей, ошибка уникальности и улучшена идемпотентность" 2025-11-13 23:01:46 +03:00
machka paslaandGitHub 2b0fa3a314 Merge pull request #132 from orryxvpn/fix/sync-admin-duplicate-subscriptions
Устранены дубли активных записей, ошибка уникальности и улучшена идемпотентность
2025-11-13 23:01:30 +03:00
machka paslaandGitHub ead990e61c Merge pull request #129 from 3252a8/feature/ref-link
Ref link improvement
2025-11-13 23:00:43 +03:00
orryxvpnandGitHub eb6e343e4c Refactor perform_sync function for readability 2025-11-13 21:30:15 +05:00
3252a8 caf5b88eef Use random string in ref link instead of tg ID
- Enable old links with tg id by default in .env with LEGACY_REFS=true
2025-11-11 22:32:30 +03:00
machka paslaandGitHub 183bef070d Merge pull request #124 from machka-pasla/dev
yookassa bug fix and other
2025-11-10 15:49:45 +03:00
machka pasla 643bf5fabb fix bug 2025-11-07 20:47:09 +03:00
machka pasla 7d446d5f64 upd promo bug 2025-11-06 19:03:34 +03:00
machka paslaandGitHub 73ce689d5e Merge pull request #120 from streletskiy/feature/user-profile-links
Feature/user profile links
2025-11-03 17:27:49 +03:00
Bogdan Strielecki 5791f2e58c Add user profile links
User profile buttons quickly opens user's telegram profile

Add user profile link button to log messages
Add user profile link button to user card in admin panel
Add referrer profile link button to log messages
Add referrer profile link button to user card in admin panel
2025-11-03 16:00:50 +03:00
machka pasla 69cc2cbe29 upd 2025-11-02 15:16:27 +03:00
machka pasla 7df2e127a4 fix 2025-10-31 22:41:48 +03:00
machka pasla d503cf7f7f YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING param added 2025-10-31 21:38:41 +03:00
machka paslaandGitHub c0d70030c3 Merge pull request #117 from machka-pasla/dev
Dev
2025-10-30 20:46:31 +03:00
machka paslaandGitHub 4e7c36dbf7 Merge pull request #115 from streletskiy/feature/paged-users-list
Paged list of all users
2025-10-30 17:57:00 +03:00
Bogdan Strielecki 3b0b88e9a0 Add admin button for delete user from bot and panel 2025-10-30 17:55:08 +03:00
Bogdan Strielecki 207e7751cd Add paged list of all users to admin panel 2025-10-28 15:13:19 +03:00
machka paslaandGitHub c65b608f0b Merge pull request #114 from machka-pasla/dev
more payment info in admin, docker compose for other servers, fixed channel sub check
2025-10-28 14:40:59 +03:00
machka pasla cff8368b7c docker hub push 2025-10-28 14:39:47 +03:00
machka paslaandGitHub 2f367bbd6b Merge pull request #112 from streletskiy/feature/display-user-income
Additional payment info in admin panel user card
2025-10-28 14:31:47 +03:00
machka pasla c3d6324ec6 fixed channel sub check 2025-10-27 15:58:59 +03:00
Bogdan Strielecki 4800a2da80 Additional payment info in admin panel user card
- total income
- total income from all user referrals
2025-10-27 13:38:37 +03:00
machka paslaandGitHub 70a0cd44a1 Merge pull request #111 from freize/main
An example Docker file for deploying a bot on a separate server
2025-10-25 20:02:43 +03:00
machka paslaandGitHub 545c9c7d07 Merge pull request #109 from Snowy-Fluffy/custom-payment-id
Добавление возможности выставлять ID способа оплаты для FreeKassa в env
2025-10-25 20:02:14 +03:00
FreizeandGitHub cd60f496fa Update docker-compose-removed-server.yml 2025-10-25 19:27:57 +03:00
FreizeandGitHub f883065bb9 Add Docker Compose configuration for removed server
This configuration file was created as an example showing the installation of the bot on a separate server.
2025-10-25 19:24:50 +03:00
Snowy-Fluffy b71bd71d8d allows you to set custom payment id for fk 2025-10-24 20:45:43 +03:00
Snowy-Fluffy 0524d24b13 set custom payment id for fk 2025-10-24 20:21:35 +03:00
machka paslaandGitHub cdfd94c814 Merge pull request #108 from machka-pasla/dev
fixed locales and traffic after adding days
2025-10-24 10:34:25 +03:00
machka pasla 6513681125 fixed locales and traffic after adding days 2025-10-24 10:33:10 +03:00
machka paslaandGitHub f321fd04cf Merge pull request #107
fixed ad
2025-10-21 09:45:32 +03:00
machka pasla baede17adf fixed ad 2025-10-21 09:44:21 +03:00
machka paslaandGitHub 17bf8720a3 Merge pull request #105 from machka-pasla/dev
fixed my devices keyboard
2025-10-20 22:28:36 +03:00
machka pasla 280aced20e fixed my devices keyboard 2025-10-20 22:26:50 +03:00
machka paslaandGitHub d672032201 Merge pull request #104 from machka-pasla/dev
Added freekassa, HWID Devices managment, mandatory sub to channel
2025-10-18 22:45:02 +03:00
machka pasla e826c4309d locales 2025-10-18 22:43:38 +03:00
machka pasla 9f525dc7da hwid devices integration 2025-10-18 22:39:56 +03:00
machka paslaandGitHub b880032b9b Merge pull request #103 from gldkru/feature/sub-devices-managment
Add 'My Devices' feature to user subscription management
2025-10-18 21:32:48 +03:00
Kirill Gladkikh 8f9484b6ec Add 'My Devices' feature to user subscription management
- Implemented a new command handler for displaying user devices.
- Added functionality to disconnect devices from the user's account.
- Updated subscription service to retrieve and manage device information.
- Enhanced inline keyboard to include device management options.
- Added new translations for device-related messages in English and Russian locales.
2025-10-18 02:27:19 +03:00
machka pasla c3622d9c2b channel require and db update 2025-10-17 09:36:25 +03:00
machka pasla 51ffbbfa1d pay from saved card 2025-10-16 22:04:04 +03:00
machka paslaandGitHub 394e8dcc6c Merge pull request #100 from zerodata731/freekassa-codex
Freekassa codex
2025-10-14 09:38:47 +03:00
machka paslaandGitHub 5f3fc13c2b Merge branch 'dev' into freekassa-codex 2025-10-14 09:38:39 +03:00
raufakchurin 4d43c9cf0f ORDER ID added to SUccess info 2025-10-14 11:28:41 +05:00
raufakchurin c891122064 before review 2025-10-14 10:31:55 +05:00
raufakchurin 59d07314e2 Удалили все личшние кроме СБП 2025-10-14 09:44:04 +05:00
machka pasla c438672ced fix 2025-10-13 10:16:30 +03:00
machka pasla 49c532e4db bruh 2025-10-13 10:06:58 +03:00
machka paslaandGitHub d50324d098 Merge pull request #99 from zerodata731/freekassa-codex
v_0.0
2025-10-13 10:05:45 +03:00
raufakchurin 52893a0629 v0.1 2025-10-12 15:27:48 +05:00
machka pasla 664cea447c errors when broadcast fix 2025-10-11 16:59:44 +03:00
machka pasla 5f50cfeaee broadcast real time logs 2025-10-11 16:53:26 +03:00
raufakchurin bdade47758 v_0.0 2025-10-11 18:06:49 +05:00
machka paslaandGitHub a3406451cb Merge pull request #98 from machka-pasla/dev
auto renew message with trial fix
2025-10-10 20:05:26 +03:00
machka pasla 9938671af9 auto renew message fix 2025-10-10 20:04:50 +03:00
machka paslaandGitHub 1d57c9be21 Merge pull request #96 from machka-pasla/dev
name filtration
2025-10-09 21:11:17 +03:00
machka pasla fde0f1b135 username filtration 2025-10-09 21:08:01 +03:00
Machka PaslaandGitHub 67df164498 Merge pull request #94 from machka-pasla/dev
tribute grace fix
2025-09-28 19:24:45 +03:00
machka-pasla 48e666f605 grace fix 2025-09-28 23:24:16 +07:00
machka-pasla 3cb15fa254 Add recent actions title to user management translations in English and Russian locales. 2025-09-16 15:19:36 +03:00
machka-pasla 2100949470 Add referral statistics to user card formatting. Updated user management to include referral service and added new translations for invited friends and purchased subscriptions in English and Russian locales. 2025-09-16 15:14:22 +03:00
Machka PaslaandGitHub c2a475b062 Merge pull request #90 from machka-pasla/dev
Bugfixes
2025-09-12 13:15:04 +03:00
machka-pasla 585cbe8fb6 Refactor subscription retrieval logic to limit results to the most recent active subscription. Updated query execution to return the first scalar result for improved performance. 2025-09-12 12:57:35 +03:00
machka-pasla a243ddcac8 Refactor ads deletion callback data parsing to improve clarity and maintainability. Updated the way campaign ID and back page are extracted from the callback data. 2025-09-11 23:05:57 +03:00
machka-pasla 357d7caf26 Refactor i18n handling in ads deletion prompt to use direct gettext calls. This improves language support and error messaging consistency. 2025-09-11 23:03:41 +03:00
machka-pasla 2713b3bae5 Add ad campaign deletion functionality with confirmation prompts and error handling. Updated inline keyboards and translations for delete actions. Enhanced subscription command handler to include fallback for config link URL. 2025-09-11 22:58:12 +03:00
Machka PaslaandGitHub 2215ee8ad2 Merge pull request #89 from machka-pasla/dev
Автопродление подписки при платеже через ЮКассу, микрофиксы и прочее-прочее
2025-09-11 09:58:05 +03:00
Machka PaslaandGitHub 1b6acff6d4 Merge pull request #88 from ProtoPes/add-dockerignore
Добавление файла .dockerignore
2025-09-08 22:36:03 +05:00
Machka PaslaandGitHub 25d9cc7446 Update .dockerignore
оставил локали, потому что не всегда идет прокидывание локалей через докер вольюм
2025-09-08 22:35:43 +05:00
ProtoPes e1e73ec736 Added ".dockerignore" to reduce the size of a docker image 2025-09-08 19:07:46 +03:00
Machka PaslaandGitHub cc21089f18 Merge pull request #84 from freize/patch-1
Correction of translation into Russian.
2025-09-07 23:10:20 +05:00
Machka PaslaandGitHub 46120210f1 Merge pull request #85 from freize/patch-2
Correction of the English translation.
2025-09-07 23:09:40 +05:00
Machka PaslaandGitHub df43283971 Update .env.example 2025-09-07 23:08:10 +05:00
Machka PaslaandGitHub a51b20ee54 Merge pull request #86 from freize/patch-3
Update .env.example
2025-09-07 23:06:26 +05:00
Machka PaslaandGitHub 0915022b52 Merge branch 'dev' into patch-3 2025-09-07 23:05:49 +05:00
FreizeandGitHub 11f5ec4a5b Update .env.example
Comments for bot configuration. Just in case.
2025-09-07 20:58:50 +03:00
FreizeandGitHub d187a79be6 Correction of the English translation. 2025-09-07 20:09:22 +03:00
FreizeandGitHub 7b05ad4cb1 Correction of translation into Russian. 2025-09-07 20:07:45 +03:00
machka-pasla 79ac5cb797 Enhance error handling and logging in CryptoPayService payment processing
- Added error handling for payment record creation and updates, ensuring database transactions are rolled back on failure.
- Implemented logging for errors during payment record creation and updates, improving traceability and debugging capabilities.
- Committed changes to ensure that payment records are properly persisted or rolled back in case of exceptions.
2025-09-07 12:55:54 +05:00
machka-pasla c208541525 Add autorenew cancellation handler and logging for subscription service
- Implemented a new callback handler to allow users to cancel auto-renewal of their subscriptions via a webhook button.
- Added logging to track subscription status and auto-renew settings during webhook checks, improving monitoring and debugging capabilities.
- Enhanced user feedback with appropriate alerts for unsupported actions and confirmation of updates to subscription settings.
2025-09-05 22:10:56 +03:00
machka-pasla 2b592e8abe Implement auto-renew confirmation and cancellation features in subscription module
- Added confirmation popups for enabling and disabling auto-renewal, enhancing user interaction and clarity.
- Introduced inline keyboard options for auto-renewal management, allowing users to confirm their choices easily.
- Updated notification messages to inform users about upcoming automatic charges and provide cancellation options.
- Enhanced localization files to include new strings for auto-renewal features in both English and Russian.
2025-09-05 22:02:52 +03:00
machka-pasla 43de4cc08b Add cryptocurrency and stars payment handlers to subscription module
- Implemented new callback handlers for cryptocurrency and stars payments, enhancing payment options for users.
- Integrated error handling and user feedback mechanisms for payment processes, improving overall user experience.
- Updated the pre-checkout and successful payment handling to accommodate new payment methods, ensuring seamless transaction processing.
- Organized imports for better modularity and clarity in the payments module.
2025-09-05 15:46:50 +03:00
machka-pasla 9a6ff6f636 Refactor pagination logic in admin ads list keyboard
- Updated the pagination row to only display when multiple pages are available, improving the user interface for ad navigation.
- Enhanced the inline keyboard structure for better clarity and usability in the admin panel's ads management section.
2025-09-05 15:17:01 +03:00
machka-pasla 8113908a98 Implement pagination and detailed views for ad campaigns in admin panel
- Added pagination support for the ads list, allowing admins to navigate through multiple pages of campaigns.
- Introduced detailed views for individual ad campaigns, displaying comprehensive statistics and information.
- Enhanced the database access layer with new methods for counting and listing campaigns with pagination.
- Updated localization files to include new strings for the ads overview and campaign details.
- Improved the inline keyboard structure for better navigation within the ads management interface.
2025-09-05 15:08:52 +03:00
machka-pasla 81155c9151 Add ads creation action to admin panel
- Introduced a new action for starting the ad creation process in the admin panel, enhancing ad management capabilities.
- Organized imports related to ad handling for better modularity and clarity in the codebase.
2025-09-05 15:00:47 +03:00
machka-pasla 7fa670c1e8 Add ads menu handler to admin panel actions
- Introduced a new action for displaying the ads menu in the admin panel, enhancing the admin interface for ad management.
- Improved the organization of imports related to ad handling, ensuring better modularity in the codebase.
2025-09-05 14:58:20 +03:00
machka-pasla d94430c602 Enhance ad creation flow with state-based filtering
- Introduced StateFilter to the ads creation flow, ensuring that messages are processed only when the bot is in specific admin states.
- Improved the organization of imports by removing redundant imports and clarifying state management for ad-related interactions.
2025-09-05 14:55:54 +03:00
machka-pasla c6292a1c8e Add ad campaign management features and enhance user attribution
- Introduced ad campaign management functionality, including the ability to create campaigns and attribute users based on ad parameters.
- Updated user command handlers to process new ad-related parameters and log user interactions with ad campaigns.
- Enhanced admin interfaces with new keyboard options for managing ads and displaying campaign information.
- Added database models for ad campaigns and attributions, improving data management for advertising features.
- Updated localization files to support new ad-related strings in both English and Russian.
2025-09-05 14:53:32 +03:00
machka-pasla d7d8409919 Refactor subscription command handler to improve user experience
- Updated the subscription command handler to dynamically adjust inline keyboard options based on user subscription status.
- Enhanced the mini-app connect button functionality, allowing access to additional features when the URL is configured.
- Improved the layout of inline keyboard buttons to prioritize new options, streamlining user interaction in subscription management.
2025-09-04 20:46:20 +03:00
machka-pasla 8f603ad51c Enhance subscription command handler with dynamic inline keyboard options
- Added support for a mini-app connect button in the subscription command handler, allowing users to access additional features if the URL is configured.
- Implemented an auto-renew toggle button and payment methods management button, improving user interaction based on subscription status and settings.
- Refactored inline keyboard construction to prepend new buttons above the existing markup, enhancing the user experience in subscription management.
2025-09-04 20:20:27 +03:00
machka-pasla 6f3b727cc7 Update .env.example to include YooKassa autopayments configuration
- Added new environment variables for enabling/disabling YooKassa autopayments, enhancing payment method management.
- Updated comments to clarify the relationship between autopayments and receipt fields, improving documentation for developers.
2025-09-04 20:08:37 +03:00
machka-pasla b7a723a088 Implement YooKassa autopayments feature toggle across payment and subscription handlers
- Introduced a global setting for enabling or disabling YooKassa autopayments, allowing for better control over payment method management and subscription renewals.
- Updated various handlers to check the autopayments setting before executing payment-related logic, ensuring that features are only available when enabled.
- Enhanced error handling to notify users when autopayments are disabled, improving user experience and clarity in payment operations.
- Refactored receipt generation logic to derive fields based on the autopayments setting, streamlining configuration management.
2025-09-04 20:03:47 +03:00
machka-pasla 807e3dadd3 Update database setup to include simple migrations and enhance .gitignore
- Added the `run_simple_migrations` function call in the database initialization process to ensure any missing columns are added during setup.
- Updated the .gitignore file to exclude the `models_old.py` file, improving project cleanliness.
2025-09-04 19:52:57 +03:00
machka-pasla f11ec6a676 Add re-exports for backward compatibility in subscription module
- Re-exported commonly used entrypoints from the core module to maintain backward compatibility.
- Ensured that existing functionality remains accessible after recent refactoring efforts in the subscription handling codebase.
2025-09-04 19:29:13 +03:00
machka-pasla d83f64c5fe Refactor subscription handling and payment method management
- Updated the user router to import the subscription router directly, preparing for future package separation.
- Removed the outdated subscription handler file, streamlining the codebase and improving maintainability.
- Introduced a new method for deleting user payment methods by provider ID, enhancing flexibility in payment management.
- Improved error handling and clarity in payment method operations, ensuring a smoother user experience.
2025-09-04 19:21:14 +03:00
machka-pasla 72eaed991a Improve payment method filtering logic in subscription history handler
- Added support for distinguishing between internal method IDs and direct provider payment method IDs, enhancing the accuracy of payment method filtering.
- Introduced a flag to track if a payment method filter was requested, allowing for clearer handling of cases where the method cannot be resolved.
- Updated error handling to ensure that empty payment histories are displayed appropriately when a filter is requested but cannot be matched.
2025-09-04 19:06:20 +03:00
machka-pasla 868c48ccb9 Remove outdated comments and fallback logic in payment method history handler
- Eliminated unnecessary comments regarding auto-renewal payment methods to enhance code clarity.
- Removed fallback logic for including auto-renew entries, streamlining the filtering process for user payments.
2025-09-04 19:00:03 +03:00
machka-pasla 2b855ed11b Refactor payment method ID handling in subscription history
- Updated the payment method history handler to improve the extraction of payment method IDs from callback data, enhancing reliability in filtering and navigation.
- Changed variable names for clarity, ensuring better readability and maintainability of the code.
- Improved error handling during ID extraction to prevent potential issues when no payment history is available.
2025-09-04 18:53:58 +03:00
machka-pasla 2bc7bea29d Enhance payment method history filtering and YooKassa integration
- Updated the payment method history handler to support filtering by specific saved payment methods, improving user experience when viewing payment logs.
- Increased the limit of recent payment logs retrieved from 10 to 30 for better visibility.
- Enhanced the YooKassa service to include detailed payment method information, including card details and last four digits, improving clarity in payment history.
- Refactored error handling in the YooKassa service to ensure robust fetching of payment information.
2025-09-04 18:49:31 +03:00
machka-pasla b6a2bf2329 Implement YooMoney payment method enhancements and localization updates
- Added functionality to save multiple YooMoney payment methods, marking the first entry as default.
- Refactored payment method display logic to standardize the presentation of YooMoney wallet details across various handlers.
- Introduced new localization strings for improved clarity in user-facing messages regarding payment methods.
- Enhanced error handling during payment method operations to ensure smoother user experience.
2025-09-04 18:42:48 +03:00
machka-pasla ba8be7c3f9 Standardize YooMoney wallet display across payment handlers and localization
- Updated the display logic for YooMoney wallet in payment and subscription handlers to ensure consistent naming and avoid leaking sensitive account information.
- Introduced a new localization string for wallet display, enhancing clarity in user-facing messages.
- Refactored relevant sections to improve overall code maintainability and user experience.
2025-09-04 18:26:15 +03:00
machka-pasla cb464059d9 Enhance payment method display for YooMoney integration
- Updated the logic for displaying payment method details, specifically for YooMoney, to include account number handling and last four digits extraction.
- Improved the consistency of payment method display across different handlers, ensuring a clearer user experience.
- Refactored relevant sections in both payment and subscription handlers to accommodate the new display logic.
2025-09-04 18:18:36 +03:00
machka-pasla a5aecb0f86 Enhance payment method display logic and localization updates
- Improved the logic for displaying payment method details, including card type and last four digits, to provide a clearer user experience.
- Updated localization strings to reflect changes in payment method terminology, ensuring consistency across user-facing messages.
- Refactored payment method handlers to utilize the new display logic, enhancing the overall management of payment methods.
2025-09-04 18:14:32 +03:00
machka-pasla cf7f0eae51 Refactor payment method ID extraction for improved reliability
- Updated the logic for extracting payment method IDs from callback data to handle cases with multiple segments, ensuring more robust parsing.
- Enhanced the consistency of ID retrieval across different payment method handlers, improving overall code reliability and user experience.
2025-09-04 18:00:04 +03:00
machka-pasla 850f135bf8 Implement multi-card deletion for payment methods with enhanced error handling
- Added functionality to delete specific payment methods based on their ID, supporting multi-card management.
- Improved error handling during deletion processes, including rollback mechanisms for database transactions.
- Updated user notifications to reflect the success or failure of deletion attempts, ensuring a consistent user experience.
- Refactored the response messages to include updated lists of remaining payment methods after deletion.
2025-09-04 17:52:21 +03:00
machka-pasla 793412c472 Enhance payment method history handling and navigation
- Added a new keyboard function to facilitate navigation back to specific payment method details.
- Updated the payment method history handler to utilize the new back navigation option, improving user experience when no payment history is available.
- Enhanced error handling for extracting payment method IDs from callback data to ensure smoother navigation.
2025-09-04 17:44:31 +03:00
machka-pasla a510ab1021 Refactor payment methods management to utilize a paginated list view
- Updated the payment methods management handler to directly build and display a paginated list of user payment methods, enhancing user experience.
- Removed legacy support for single card checks and deprecated keyboard functions in favor of the new list view.
- Improved localization handling for payment method titles and added fallback options for missing data.
2025-09-04 17:32:27 +03:00
machka-pasla 50cea15c92 Enhance localization for user-facing messages across payment and subscription services
- Updated various services to utilize the user's database language for all user-facing messages, improving the localization experience.
- Refactored message retrieval logic in payment processing, subscription management, and notification handling to ensure consistency in language usage.
- Added comments to clarify the purpose of language settings in relevant sections of the code.
2025-09-04 17:24:24 +03:00
machka-pasla 6b5828ea51 Implement multi-card payment method management and enhance user notifications
- Added support for saving multiple payment methods, allowing users to bind and manage their cards effectively.
- Introduced a new paginated list view for displaying saved payment methods, improving user experience.
- Enhanced user notifications for successful binding of payment methods, including localized messages.
- Updated the database models and data access layer to accommodate multi-card functionality.
- Refactored existing payment method handlers to integrate with the new multi-card system.
2025-09-04 17:11:18 +03:00
machka-pasla 257597ccb7 Enhance payment processing and user detail updates in YooKassa integration
- Added handling for 'waiting_for_capture' event in the YooKassa webhook to manage bind-only payment flows, including saving payment methods and canceling authorizations.
- Introduced a new method in the YooKassaService to cancel payments, improving error handling and logging for payment cancellations.
- Updated user detail synchronization to conditionally update descriptions only when they differ from the current panel state, enhancing efficiency.
2025-09-04 16:30:45 +03:00
Machka PaslaandGitHub dba82cfb97 Create LICENSE
MIT
2025-09-04 15:04:53 +03:00
machka-pasla e0e2cde9a7 Remove unused import for PaymentMethodType in yookassa_service.py to clean up code and improve readability. 2025-09-03 19:56:31 +03:00
machka-pasla 5b98e1a40c Implement payment methods management and binding functionality
- Added new handlers for managing payment methods, including viewing, binding, and deleting payment methods.
- Introduced new inline keyboard options for payment method management in user interactions.
- Enhanced the YooKassa service to support card binding with minimal payment requirements.
- Updated localization files to include new messages related to payment methods and their management.
2025-09-03 18:07:18 +03:00
machka-pasla 0f69823192 Refactor subscription fetching logic in toggle_autorenew_handler
- Updated the subscription retrieval process to directly fetch the Subscription model by ID, improving clarity and reducing potential import cycle issues.
- Enhanced code readability by adding a comment to clarify the purpose of the subscription fetch operation.
2025-09-03 16:32:18 +03:00
machka-pasla 38f3b1ad74 Enhance localization and fallback logic in i18n middleware
- Improved the effective language determination process by adding robust fallbacks to ensure a valid language is always used.
- Updated the gettext method to include explicit fallback to English if the requested language data is unavailable.
- Corrected a typo in the Russian localization for auto-renewal messaging to ensure clarity in user notifications.
2025-09-03 16:28:02 +03:00
machka-pasla 9b4dab84ad Enhance auto-renewal messaging and payment processing logic
- Introduced a new flag for auto-renew subscriptions to streamline messaging and avoid redundant configuration links.
- Updated localization files to include a new message for auto-renewal notifications in both English and Russian.
- Improved error handling and logging in the payment processing flow to ensure clarity and reliability during subscription renewals.
2025-09-03 10:44:20 +03:00
machka-pasla c607b6ef85 Enhance payment processing for auto-renew subscriptions
- Updated the payment processing logic to handle cases where payment_db_id may be absent for auto-renewal scenarios, ensuring idempotent creation of payment records using provider payment IDs.
- Implemented error handling for ensuring payment records and backfilling yookassa_payment_id, improving reliability in processing auto-renewal webhooks.
- Enhanced logging for better traceability of payment record creation and updates.
2025-09-03 10:32:39 +03:00
machka-pasla 09f0de78c6 Refactor subscription renewal process to utilize panel webhook
- Removed the recurring billing task from the bot, shifting the auto-renew functionality to the panel webhook service, which now triggers renewals 24 hours before expiry.
- Updated service dependencies to wire the subscription service with the panel webhook for seamless renewal handling.
- Adjusted the Subscription model to enable auto-renew by default, enhancing subscription management.
2025-09-03 10:22:02 +03:00
machka-pasla 6a3b4a6947 Safely extract and serialize payment method details in YooKassa webhook handling
- Enhanced the yookassa_webhook_route to safely extract payment method details, including card information, from the payment notification.
- Implemented error handling to log exceptions during serialization, improving reliability and debugging capabilities.
- Updated the payment processing dictionary to use the newly structured payment method data.
2025-09-03 10:12:15 +03:00
machka-pasla d6f707d386 Implement auto-renew subscription feature and enhance payment method handling
- Added a recurring billing task to automatically charge users one day before subscription expiry, improving subscription management.
- Introduced a new UserBilling model to store saved payment methods for off-session charges, enhancing user experience.
- Updated YooKassa service to support saving payment methods and capturing payments for auto-renewals.
- Enhanced subscription handling to toggle auto-renew settings and provide user feedback through localized messages.
- Improved error handling and logging for payment method persistence and subscription renewal processes.
2025-09-03 09:40:42 +03:00
Machka PaslaandGitHub dace4c2938 Merge pull request #82 from machka-pasla/dev
I HATE tribute
2025-09-01 21:26:30 +03:00
machka-pasla b69c2ab18d Enhance PanelWebhookService to support panel expiry updates and auto-renew payments
- Updated the PanelWebhookService to accept a new PanelApiService dependency for managing panel user details.
- Implemented functionality to update panel expiry upon subscription renewal, ensuring users maintain access to services.
- Added error handling and logging for both panel expiry updates and auto-renew payment record creation, improving reliability and user feedback.
2025-09-01 21:23:55 +03:00
machka-pasla 56fc88c10e Refactor trial confirmation and activation handlers to utilize new keyboard options
- Updated the trial confirmation and activation handlers to use a dynamic reply markup based on activation status, improving user interaction.
- Enhanced localization for trial activation details in both English and Russian, providing clearer messaging about trial status and connection instructions.
- Ensured consistency in the user experience across different scenarios by integrating the new keyboard options.
2025-08-30 13:29:42 +03:00
machka-pasla e74f801aec Add new error messages and subscription details in English and Russian localization
- Introduced additional error messages for improved user feedback, including service unavailability and payment gateway errors.
- Added subscription details localization to enhance user experience, providing information on subscription status and traffic usage.
- Ensured consistency in messaging across both English and Russian locales.
2025-08-29 13:09:01 +03:00
machka-pasla acf5f060a6 Enhance error handling in subscription response methods
- Implemented try-except blocks around callback answer methods to prevent exceptions from disrupting user interactions.
- Ensured consistent use of the `answer` method across various subscription-related handlers, improving reliability in user notifications.
2025-08-29 12:27:13 +03:00
machka-pasla fb2672732d Improve error handling in user interaction responses
- Refactored the handling of callback answers and message responses to include try-except blocks, ensuring that exceptions are caught and logged without disrupting the user experience.
- Updated message sending methods to utilize the `answer` method consistently, enhancing the reliability of user notifications in various scenarios.
2025-08-29 12:10:00 +03:00
machka-pasla f4730c3ceb Enhance user description synchronization with Telegram profile
- Added functionality to update user descriptions on the panel during synchronization, incorporating Telegram fields such as username, first name, and last name.
- Implemented error handling and logging for failed description updates in both admin sync and profile sync processes.
- Updated user creation and details update methods in the subscription service to include user descriptions, improving consistency across user data management.
2025-08-29 11:05:21 +03:00
machka-pasla e6b3a7c66f Add trial activation details in English and Russian localization
- Introduced a new message for trial activation details, providing users with information about the duration, validity, and configuration link.
- Enhanced user experience by ensuring clarity and consistency in both English and Russian locales.
2025-08-29 10:51:40 +03:00
machka-pasla 24cf193f3c Refactor broadcast and user logs messages for localization support
- Updated the broadcast confirmation message to utilize localization for dynamic content, enhancing multilingual support.
- Modified user logs display to incorporate localization for the title, improving consistency across languages.
- Added new localization entries in both English and Russian for better user experience and clarity.
2025-08-28 22:03:48 +03:00
machka-pasla 85bf0fb6fd Enhance promo export functionality with English localization
- Added support for exporting promo activations and all promo codes with captions and CSV headers in English.
- Updated the promo export handlers to ensure consistent English messaging for user notifications and CSV content.
- Introduced new localization entries for English in the locales files to support the changes.
2025-08-28 21:52:36 +03:00
machka-pasla b576d04501 Add welcome messages in English and Russian localization
- Introduced a new "welcome" message for users in both English and Russian, enhancing the user experience with personalized greetings.
2025-08-28 21:46:59 +03:00
machka-pasla 97268b9906 Add subscription expiration notifications in English and Russian
- Introduced new localized messages for notifying users about upcoming subscription expirations at 72, 48, and 24 hours.
- Added notifications for expired subscriptions and a message for cancelled Tribute subscriptions, enhancing user communication regarding their subscription status.
2025-08-28 21:41:40 +03:00
machka-pasla 1091b7b846 Update bot functionality and localization improvements
- Added new entries to .gitignore for backup JSON files.
- Enhanced user statistics in inline mode to include active users today.
- Improved message handling in broadcast functionality to differentiate between text and media messages, ensuring proper parameter usage.
- Updated payment handling to reflect pagination information in English and Russian.
- Refactored promo management to utilize localized button texts for better user experience.
2025-08-28 21:24:31 +03:00
Machka PaslaandGitHub f191c1813e Merge pull request #79 from ProtoPes/add-referral-check
Added check for referral user, small refactoring
2025-08-26 20:38:45 +03:00
ProtoPes e78f9bf291 Added check for referral user, small refactoring
With regular expressions we can be sure that the data is already valid
2025-08-26 20:28:50 +03:00
Machka PaslaandGitHub 5c74a08ed8 Merge pull request #78 from machka-pasla/dev
Implement user creation and synchronization enhancements in admin syn…
2025-08-26 18:14:40 +03:00
machka-pasla 14950fd559 Implement user creation and synchronization enhancements in admin sync handler
- Added functionality to create new users during synchronization if they are not found in the local database and have a valid Telegram ID.
- Introduced logging for newly created users to improve tracking and debugging.
- Enhanced synchronization statistics to include the count of newly created users, with localization support for both English and Russian.
- Updated the details of synchronization status to reflect additional statistics, improving clarity in admin reports.
2025-08-26 18:11:52 +03:00
Machka PaslaandGitHub 711b9a2487 Merge pull request #77 from machka-pasla/dev
Update broadcast and some other small bugs 🪲
2025-08-25 14:13:27 +03:00
machka-pasla 6e7eb6acfd Enhance message sending functionality to filter unsupported parameters
- Introduced a `SUPPORTED_PARAMS` dictionary to define valid parameters for each message type.
- Added a `filter_kwargs` utility function to filter out unsupported parameters based on the content type.
- Updated `send_message_by_type`, `send_message_via_queue`, and `send_direct_message` functions to utilize the new filtering logic, ensuring only valid parameters are passed during message sending.
- Improved handling for unknown content types by sending a default text message.
2025-08-25 13:49:31 +03:00
machka-pasla b42fae8772 Refactor message handling in broadcast and user management to utilize new utility functions
- Introduced `get_message_content` and `send_message_by_type` utility functions to streamline content type handling and message sending for various media types.
- Updated `process_broadcast_message_handler` and `process_direct_message_handler` to leverage these new functions, reducing code duplication and improving maintainability.
- Enhanced error handling for empty messages and improved message formatting with admin signatures.
2025-08-25 13:44:37 +03:00
machka-pasla f707662125 Refactor bot initialization and routing logic to enforce webhook mode requirement
- Updated the bot's startup logic to require a configured WEBHOOK_BASE_URL, exiting if not set, and logging appropriate error messages.
- Simplified the decision-making process for running the AIOHTTP server, ensuring it only runs in webhook mode.
- Enhanced the router configuration to filter updates for private chats, improving message handling security.
2025-08-25 13:24:30 +03:00
machka-pasla 60c6e0e961 Refactor subscription duration calculation in PanelWebhookService and SubscriptionService
- Introduced a utility function `add_months` to handle subscription duration calculations based on calendar months instead of a fixed 30-day period.
- Updated the auto-renewal logic in `PanelWebhookService` to use the new function for extending subscription end dates.
- Adjusted the duration calculation in `SubscriptionService` to derive the end date after a specified number of months, improving accuracy in subscription management.
2025-08-25 12:30:44 +03:00
machka-pasla b23b75b72e Refactor username update logic in SubscriptionService to prevent overwriting Telegram usernames
- Removed the conditional logic that updated the local user's username with the panel username, ensuring that the Telegram username remains unchanged.
- Added a comment to clarify the purpose of the update, focusing on maintaining the linkage to the panel UUID.
2025-08-25 12:24:30 +03:00
machka-pasla c69f02f7c0 Enhance direct message handling in user management to support multiple content types
- Updated the process_direct_message_handler to determine the content type of incoming messages (text, photo, video, etc.) and send them accordingly to the target user.
- Added error handling for empty messages and invalid HTML content, improving user feedback.
- Included admin signature in messages, ensuring consistent formatting across different content types.
2025-08-20 17:12:40 +03:00
machka-pasla f22e359684 Refactor user creation logic in DAL to support race-safe inserts and return creation status
- Updated the create_user function to use PostgreSQL upsert for concurrent user creation, preventing IntegrityError.
- Modified the function to return a tuple containing the user object and a boolean indicating if the user was newly created.
- Adjusted the start_command_handler to log user registration only if a new user was created, improving logging clarity.
2025-08-20 15:49:12 +03:00
machka-pasla d2402fea77 Add preview message functionality for multiple content types in broadcast handler
- Implemented preview message sending for various content types (text, photo, video, animation, document, audio, voice, sticker, video_note) in the process_broadcast_message_handler.
- Added error handling for invalid HTML content in broadcast messages, improving user feedback and experience.
- Enhanced the confirmation prompt to provide a concise message preview without duplicating text.
2025-08-20 15:39:22 +03:00
machka-pasla 9b8ddb39da Enhance broadcast message handling to support multiple content types
- Updated the process_broadcast_message_handler to determine the content type of incoming messages (text, photo, video, etc.) and store relevant data in the state.
- Implemented new methods in MessageQueueManager for queuing various media types, improving the flexibility of the broadcast system.
- Adjusted confirmation prompts to provide a concise message preview, enhancing user experience.
- Added localization for the new confirmation prompt in both English and Russian.
2025-08-20 15:30:06 +03:00
Machka PaslaandGitHub 459b655ae3 Merge pull request #76 from machka-pasla/dev
add new referral param and broadcast for groups of users
2025-08-19 14:54:30 +03:00
machka-pasla d81ab4137d Update payment processing logic to disable skipping active subscriptions
- Modified the payment processing functions across multiple services to set skip_if_active_before_payment to False, ensuring that active subscriptions are not skipped during payment processing.
- This change enhances the handling of user subscriptions and improves overall payment logic consistency.
2025-08-19 12:26:56 +03:00
Machka PaslaandGitHub cb6ae1e052 Update README.md 2025-08-18 15:22:29 +03:00
machka-pasla f4e2ae5fbd Enhance expired subscription handling in PanelWebhookService
- Updated the _handle_expired_subscription method to return a boolean indicating whether an auto-renewal was performed.
- Adjusted the handle_event method to suppress expiration notifications if an auto-renewal occurs, improving user experience and notification management.
2025-08-17 18:18:31 +03:00
machka-pasla 157c3a7c61 Add referral bonus configuration and enhance payment processing logic
- Introduced a new environment variable REFERRAL_ONE_BONUS_PER_REFEREE to control referral bonus application.
- Updated referral bonus application logic to skip bonuses for users with active subscriptions at the time of payment.
- Enhanced payment processing functions across multiple services to include current payment ID and skip logic for active users.
- Added a new database method to count succeeded payments for users, improving referral bonus eligibility checks.
2025-08-17 17:27:33 +03:00
machka-pasla ef9ebc1918 Refactor startup and shutdown handlers in bot initialization for compatibility with aiogram event signature
- Wrapped the startup and shutdown handlers to ensure they conform to the aiogram event signature, allowing for proper argument handling.
- Updated the web server to access dispatcher workflow data directly, preventing sequence protocol issues and enhancing stability.
2025-08-17 12:13:08 +03:00
machka-pasla 87664a7735 Remove unused router inclusion from dispatcher setup to streamline bot initialization 2025-08-17 12:08:42 +03:00
machka-pasla 3d58f60a4d Refactor bot initialization and service registration for improved modularity
- Integrated new service building functions to streamline bot initialization, enhancing maintainability and clarity.
- Updated the dispatcher setup to include core services dynamically, reducing redundancy in service registration.
- Added support for multiple broadcast targets in the admin handler, allowing for more flexible message distribution.
- Enhanced localization for new broadcast target options, improving user experience in both English and Russian.
- Implemented new database access methods to retrieve user IDs based on subscription status, optimizing broadcast logic.
2025-08-17 12:05:06 +03:00
machka-pasla a75a1f483c Add ProfileSyncMiddleware to keep user profile data updated in the database
- Integrated ProfileSyncMiddleware to ensure that user profile information (username, first_name, last_name) remains current in the database.
- Updated get_enhanced_user_statistics to use timezone-aware datetime for accurate SQL queries, preventing naive/aware comparison issues.
- Clarified comments in the user statistics function for better understanding of active user metrics and subscription handling.
2025-08-15 23:40:23 +03:00
Machka PaslaandGitHub 70d472e71c Merge pull request #71 from machka-pasla/dev
i hate tribute
2025-08-09 22:32:58 +03:00
machka-pasla 13a9e58e27 Refactor response handling in TributeService for improved clarity and consistency
- Introduced helper functions for generating standardized JSON responses, enhancing readability and maintainability.
- Updated response handling for various error conditions to return structured JSON instead of plain text.
- Acknowledged missing user ID with an "ignored" status to prevent unnecessary retries, improving webhook processing reliability.
2025-08-09 22:18:06 +03:00
machka-pasla 7219a6ac30 Refactor provider payment ID generation in TributeService for improved uniqueness
- Updated the logic to generate a unique, idempotent provider payment ID based on explicit event/payment identifiers or a combination of subscription ID and a hash of the raw payload.
- This change ensures better handling of webhook events and prevents potential conflicts in payment identification.
2025-08-09 21:29:04 +03:00
Machka PaslaandGitHub dbb27ee9ca Merge pull request #70 from machka-pasla/dev
Broadcast fix
2025-08-09 10:00:30 +03:00
machka-pasla 4e58fda4a5 Update broadcast message handler to enforce HTML parsing and disable web page previews
- Changed the parse_mode to "HTML" for broadcast messages to ensure proper formatting.
- Added disable_web_page_preview option to enhance message presentation and control over content display.
2025-08-09 09:51:21 +03:00
machka-pasla 8eb5daada5 Add HTML validation for broadcast messages in admin handler
- Implemented a preliminary check for HTML validity in broadcast messages by attempting to send a test message before processing.
- Added error handling for invalid HTML, providing user feedback in both English and Russian.
- Updated localization files to include new error messages for invalid HTML input.
2025-08-09 09:46:33 +03:00
machka-pasla 4c28d3868c Implement validation for broadcast message input in admin handler
- Updated the broadcast message handler to trim whitespace from the input text and added a check for empty messages.
- If the message is empty, an error response is sent to prompt the user for valid input, enhancing user experience and preventing empty broadcasts.
2025-08-09 09:43:02 +03:00
Machka PaslaandGitHub 08810eb2e0 Merge pull request #69 from machka-pasla/dev
Update promo_edit_field_handler to clarify callback data structure
2025-08-07 23:57:59 +03:00
machka-pasla b5d3b7b9c7 Update promo_edit_field_handler to clarify callback data structure
- Changed the variable name from 'i18n' to 'action' for better readability and understanding of the callback data being processed.
- Ensured that the promo ID and field to edit are still correctly extracted and updated in the state management.
2025-08-07 23:55:43 +03:00
Machka PaslaandGitHub 6330d57c60 Merge pull request #68 from machka-pasla/dev
Tribute hotfix, payments logs
2025-08-07 23:51:55 +03:00
machka-pasla 149ff057a9 Refactor subscription synchronization logic for improved handling
- Enhanced the subscription syncing process to prioritize concrete subscription UUIDs for updates and creations, ensuring idempotency.
- Implemented atomic updates for existing subscriptions and streamlined the creation of new subscriptions when a UUID is available.
- Improved logging for subscription updates and creations to provide clearer feedback on sync actions.
- Added handling for cases where no subscription UUID is present, avoiding unnecessary record creation.
2025-08-07 23:47:19 +03:00
machka-pasla bb7641bb74 Improve amount conversion in TributeService to handle minor units
- Updated the amount handling logic to convert minor currency units (kopecks/cents) to major units before persisting.
- Added error handling for invalid amount inputs to ensure robustness in processing payment data.
- Ensured that the amount is rounded to two decimal places for accurate representation in the system.
2025-08-07 23:39:48 +03:00
machka-pasla 18f65ea493 Refactor notification handling and streamline router registration
- Replaced legacy notification functions with a unified NotificationService for better maintainability and clarity.
- Updated the main bot router registration to utilize a root router, simplifying the inclusion of user and admin routes.
- Removed unused middleware and helper functions to enhance code cleanliness and focus on essential components.
- Improved localization by adding new error messages for user interactions.
2025-08-07 23:30:32 +03:00
machka-pasla 5853b9da63 Enhance payment identifier normalization in TributeService
- Updated the TributeService to normalize provider payment identifiers, prioritizing true payment identifiers over subscription IDs for better uniqueness.
- Implemented fallback logic to append timestamps to subscription IDs, preventing deduplication of renewals.
- Expanded the list of successful charge events to include various payment-related events, improving event handling consistency.
2025-08-07 23:02:23 +03:00
machka-pasla 91cfe0baf3 Add payments feature to admin panel
- Integrated payments functionality into the admin panel by adding a new payments router and corresponding handlers.
- Updated the admin panel actions to include a view payments option, enhancing admin capabilities.
- Implemented new database functions to retrieve successful payment counts and details for export.
- Enhanced localization with new strings for payments management in both English and Russian.
2025-08-07 22:48:09 +03:00
machka-pasla 194f1b9e49 Enhance recent payment log retrieval to filter by succeeded status
- Updated the `get_recent_payment_logs_with_user` function to include a filter for payments with a 'succeeded' status, improving the relevance of retrieved payment logs.
- Adjusted the query structure for better readability and maintainability.
2025-08-07 19:00:27 +03:00
machka-pasla df15cfd25e Refactor promo handler functions to include session management
- Updated the promo_delete_handler, promo_edit_select_handler, and promo_edit_field_handler functions to accept an AsyncSession parameter, improving database interaction consistency.
- Enhanced the handling of expired subscriptions in the PanelWebhookService by modifying notification logic to only send messages if enabled, ensuring better control over user notifications.
- Added an import for the 'and_' function in payment_dal.py to support more complex query conditions.
2025-08-07 18:46:19 +03:00
machka-pasla 57e693fa37 Refactor synchronization messaging for clarity and simplicity
- Updated synchronization messages to provide simpler, more concise feedback to admins during the sync process.
- Replaced detailed sync status messages with straightforward notifications for success, failure, and errors.
- Enhanced localization for new message formats to improve user experience across languages.
2025-08-06 22:26:07 +03:00
Machka PaslaandGitHub 60ea6fff0d Merge pull request #67 from machka-pasla/dev
fix bug
2025-08-06 20:22:20 +03:00
machka-pasla 3cee4b243a Refactor details string handling in statistics display
- Simplified the logic for displaying synchronization details by removing the character limit, ensuring full visibility of the details or defaulting to "N/A" when not available.
- Improved code readability by streamlining the assignment of the details string.
2025-08-06 20:19:45 +03:00
machka-pasla a42f80160b Refactor broadcast confirmation prompt and enhance sync notification handling
- Updated the broadcast confirmation prompt to display the full message instead of a truncated preview.
- Improved error handling in the sync process by removing character limits on error details and ensuring comprehensive logging.
- Added a notification feature to inform admins about the panel synchronization status, including success and failure details.
- Enhanced localization for the broadcast confirmation prompt and added new log messages for sync notifications.
2025-08-06 19:04:03 +03:00
Machka PaslaandGitHub 74548527f1 Merge pull request #66 from machka-pasla/dev
autosync and bug fixes
2025-08-06 18:51:27 +03:00
Machka PaslaandGitHub f8e3bee52a Update .env.example 2025-08-06 18:49:21 +03:00
machka-pasla 649528e165 Refactor bot username retrieval in bulk promo code creation
- Updated the logic to retrieve the bot username dynamically, enhancing the accuracy of CSV links for promo codes.
- Improved error handling to log failures in fetching the bot username, ensuring better traceability.
- Adjusted the promo code service initialization in the start command handler to use the message context, improving consistency in bot interactions.
2025-08-06 18:03:14 +03:00
machka-pasla 859263dc2d Enhance bulk promo code creation with CSV export functionality
- Added the ability to generate and send a CSV file containing created promo codes, improving data accessibility for admins.
- Updated success messages to inform users about the CSV file and the number of created promo codes.
- Refactored the promo code creation logic to include detailed validity information and links for activation in the CSV output.
- Improved localization for bulk promo creation messages to enhance user experience.
2025-08-06 17:58:44 +03:00
machka-pasla a126c05365 Refactor variable assignment in promo detail retrieval
- Updated the variable assignment for the status emoji in the `get_promo_detail_text_and_keyboard` function to improve clarity and consistency in the code.
- Enhanced readability by using more descriptive variable names, aligning with recent refactoring efforts in the promo management handler.
2025-08-06 17:49:06 +03:00
machka-pasla d0c09f9e06 Enhance promo code creation and management logging
- Added logging for successful promo code creation, improving traceability of actions.
- Updated success message formatting to use a new variable for validity display, enhancing clarity for users.
- Refactored variable names in the promo management handler for better readability and consistency.
2025-08-06 17:41:58 +03:00
machka-pasla 9f7171a5c3 Refactor admin sync process for enhanced user and subscription tracking
- Updated the `perform_sync` function to improve tracking of users and subscriptions during synchronization.
- Introduced additional counters for detailed logging, including users without Telegram IDs and those not found in the database.
- Enhanced error handling and logging to provide clearer insights into the synchronization process and outcomes.
- Improved the summary details returned after synchronization, offering a comprehensive overview of the sync results.
2025-08-06 17:21:25 +03:00
machka-pasla 990b08cfdc Enhance subscription handling in admin sync process
- Added logic to retrieve the subscription UUID from the panel user data, improving the accuracy of subscription records.
- Updated the subscription creation process to use the actual subscription UUID when available, with fallback to the user UUID.
- Enhanced logging to provide clearer information on which UUID is being used for each user during synchronization.
2025-08-06 17:00:44 +03:00
machka-pasla 359a3c46a4 Implement panel synchronization functionality in admin handler
- Added a new `perform_sync` function to handle the synchronization of users and subscriptions from the admin panel.
- Enhanced error handling and logging during the sync process, providing detailed feedback on the synchronization status.
- Updated the `sync_command_handler` to utilize the new `perform_sync` function, improving code organization and readability.
- Improved messaging for sync results, including success and error details, to enhance admin user experience.
2025-08-06 16:46:52 +03:00
machka-pasla 00ffec13d8 Implement message queue management and automatic sync on bot startup
- Added initialization of the message queue manager during bot startup, enhancing message handling capabilities.
- Implemented automatic synchronization of the admin panel on startup, providing real-time updates and improved reliability.
- Updated admin handlers to utilize the message queue for broadcasting messages, improving efficiency and error handling.
- Introduced a new command for admins to check the status of message queues, enhancing monitoring and management capabilities.
- Enhanced localization for new features and messages related to queue management and synchronization.
2025-08-06 16:39:35 +03:00
Machka PaslaandGitHub 5c8099168d Merge pull request #63 from machka-pasla/dev
Dev
2025-08-06 01:24:06 +03:00
machka-pasla 4675141974 Enhance promo code management in admin panel with detailed views and export functionality
- Introduced a new method to retrieve detailed promo code information, improving the clarity of promo management.
- Added functionality to export promo code activations to CSV, enhancing data accessibility for admins.
- Streamlined the handling of promo code edits and improved user prompts for better interaction.
- Updated localization files to reflect new messages and ensure consistency across the admin panel.
2025-08-06 01:21:47 +03:00
machka-pasla 67f31d62e4 Refactor promo code management in admin panel for improved clarity and localization
- Simplified the handling of promo codes by consolidating text messages and improving localization for better user experience.
- Updated the structure of promo details and management views, enhancing readability and consistency in the admin panel.
- Removed deprecated text and streamlined the activation status messages, ensuring accurate and clear communication for admins.
2025-08-06 01:14:26 +03:00
Machka PaslaandGitHub c969d52ce1 Update README.md 2025-08-05 23:50:11 +03:00
machka-pasla 3bd29511e1 Refactor README.md for improved clarity and localization
- Translated the README.md content from English to Russian, enhancing accessibility for Russian-speaking users.
- Streamlined the structure of the README to better highlight key features, setup instructions, and project organization.
- Updated sections on user and admin functionalities, payment methods, and project technologies to reflect recent changes and improve user understanding.
2025-08-05 23:49:08 +03:00
Machka PaslaandGitHub 0c31256fd1 Merge pull request #62 from machka-pasla/dev
2.0.0 update
2025-08-05 23:36:31 +03:00
machka-pasla 04e45096be Update .env.example and settings.py for tribute payment configuration
- Revised the .env.example file to include updated settings for Telegram bot, database, and payment gateways, enhancing clarity for configuration.
- Added new fields for managing tribute payment notifications in settings.py, allowing for better control over cancellation notifications.
- Improved organization of payment method settings and localization options for a more streamlined user experience.
2025-08-05 23:33:46 +03:00
machka-pasla e537203209 Add auto-renewal and cancellation handling for tribute subscriptions
- Implemented a new method to handle expired tribute subscriptions, automatically renewing them if no cancellation was received.
- Added functionality to manage tribute subscription cancellations, setting a grace period and notifying users accordingly.
- Updated localization files to include messages for auto-renewal and cancellation notifications in both English and Russian, enhancing user communication.
2025-08-05 23:21:12 +03:00
machka-pasla 283fc6bad4 Update localization for user ban and unban messages in English and Russian
- Added new messages for successful ban and unban actions, as well as error messages for banning and unbanning users.
- Included a warning message for when a user is not banned, enhancing clarity in the admin panel's user management features.
2025-08-05 15:35:55 +03:00
machka-pasla a46e5a43d6 Add user ban and unban functionality with prompts in admin panel
- Implemented handlers for banning and unbanning users, allowing admins to input user IDs or usernames.
- Added prompts for user input during ban and unban processes, enhancing user experience and clarity.
- Updated localization for ban and unban messages in both English and Russian, ensuring consistency and improved communication.
- Refactored user display logic in the banned users list to use updated attributes, enhancing data accuracy.
2025-08-05 15:31:03 +03:00
machka-pasla 9cf5a93625 Refactor promo code handling in admin panel and clean up unused methods
- Replaced the promo_codes module with a new promo management structure, enhancing organization and clarity in the admin panel.
- Updated admin panel handlers to utilize the new promo management methods, improving the flow of promo code creation and management.
- Removed deprecated methods from payment and subscription data access layers, streamlining the codebase and improving maintainability.
- Ensured localization files are updated to reflect changes in promo management messaging, enhancing user experience.
2025-08-05 14:55:55 +03:00
machka-pasla c5f41a261b Update promo code success message and enhance subscription service settings
- Revised the success message for promo code creation to include updated variable names for clarity in the response.
- Added a new setting to control notification behavior for Tribute payments, improving customization options for subscription management.
- Updated Russian localization for admin panel buttons to enhance clarity and consistency in user interface elements.
2025-08-05 14:43:19 +03:00
machka-pasla 74719669fb Fix activation date reference and update localization for promo code toggle success message
- Changed the reference from `created_at` to `activated_at` in the promo activations handler to accurately reflect the activation date.
- Updated the localization files for both English and Russian to remove HTML tags from the promo code toggle success message, enhancing clarity.
2025-08-04 18:16:59 +03:00
machka-pasla c8831718ed Add promo code toggle and activations handling in admin panel
- Implemented a new handler to toggle the active status of promo codes, allowing admins to activate or deactivate codes easily.
- Added functionality to display activations for specific promo codes, enhancing visibility into their usage.
- Updated localization files to include new messages related to promo status changes and activations in both English and Russian, improving admin experience.
2025-08-04 18:10:33 +03:00
machka-pasla ad8f05e20b Refactor notification service to streamline admin notifications
- Removed the functionality to send suspicious promo attempt notifications to admins, simplifying the notification process.
- Updated comments in the code to reflect the changes made in the notification handling logic.
2025-08-04 14:32:53 +03:00
machka-pasla b53e6a2cc3 Update .env.example and enhance notification service for suspicious promo attempts
- Revised the .env.example file to include new configuration options for Telegram bot settings, database credentials, and payment gateways, ensuring clarity and ease of use.
- Implemented a new method in the NotificationService to notify admins of suspicious promo code attempts, enhancing security and monitoring capabilities.
- Updated localization files to include messages related to suspicious promo code attempts in both English and Russian, improving admin awareness and response.
2025-08-04 14:29:01 +03:00
machka-pasla bcd95fb56c Update localization files for admin panel buttons and sections
- Revised button labels in English and Russian localization files for improved clarity and conciseness, including changes to promo management, user management, and navigation buttons.
- Ensured consistency in terminology across both languages to enhance user experience in the admin panel.
2025-08-03 22:39:15 +03:00
machka-pasla ce041758f1 Enhance referral program features and localization updates
- Implemented referral statistics retrieval to provide users with insights on invited friends and subscription purchases.
- Updated referral command handler to include new statistics in the response message, improving user engagement.
- Revised localization files for both English and Russian to reflect changes in the referral program messaging, enhancing clarity and user experience.
2025-08-03 22:27:41 +03:00
machka-pasla b13883c039 Enhance promo code editing functionality in admin panel
- Implemented a four-step process for editing promo codes, allowing admins to update code, bonus days, max activations, and validity period with improved user prompts.
- Added new states to manage each step of the editing process, ensuring better flow and validation.
- Updated localization files to include new messages and prompts for the editing steps in both English and Russian, enhancing user experience.
2025-08-03 22:12:49 +03:00
machka-pasla fc48cd7a09 Update inline system statistics localization for clarity and consistency
- Revised the inline system statistics description in both English and Russian to include updated terminology and additional metrics, enhancing user understanding.
- Ensured consistency across localization files by aligning the descriptions in both languages.
2025-08-03 22:06:22 +03:00
machka-pasla 9688b0f773 Update localization files for enhanced system and user statistics
- Revised inline system statistics message to reflect updated terminology and additional metrics, including active and disabled users, memory usage, and traffic statistics.
- Enhanced inline user statistics message for clarity and added a new description field for better user understanding.
- Updated Russian localization to match the changes made in the English version, ensuring consistency across languages.
2025-08-03 22:00:24 +03:00
machka-pasla 52c4f83dd1 Enhance inline and admin statistics features
- Updated inline user statistics message to provide clearer information about bot usage and user status.
- Refactored system statistics retrieval to utilize the PanelApiService for improved data accuracy and detail, including memory usage and bandwidth statistics.
- Simplified the success message for bulk promo code creation by removing the codes list from the message, ensuring clarity.
- Enhanced localization files to reflect changes in success messages and added new labels for better user experience.
2025-08-03 21:50:44 +03:00
machka-pasla 23aa3934e4 Enhance inline query handling and admin statistics retrieval
- Updated inline query handler to include settings for thumbnail URLs, improving the customization of inline results.
- Refactored admin statistics retrieval to utilize new data structures for user status and bandwidth statistics, enhancing clarity and accuracy in reporting.
- Removed deprecated help result creation code to streamline the inline query functionality.
- Added new settings for inline mode thumbnail URLs to improve maintainability and flexibility in the codebase.
2025-08-03 21:37:39 +03:00
machka-pasla ebca408227 Add promo management action to admin panel and remove panel statistics retrieval
- Introduced a new action for managing promo codes within the admin panel, enhancing admin capabilities.
- Removed the panel statistics retrieval code from the statistics handler to streamline functionality and reduce complexity.
- Updated localization files to reflect changes related to promo management.
2025-08-03 21:27:46 +03:00
machka-pasla a5b408d204 Enhance promo code management and user statistics features
- Introduced a unified promo code management system, allowing admins to view and manage both active and inactive promo codes with detailed information.
- Updated the promo code detail view to include status indicators and management options for editing, toggling status, and viewing activation history.
- Enhanced user statistics display by restructuring the information layout and adding new labels for clarity.
- Improved localization files to support new messages and labels related to promo code management and user statistics.
- Implemented error handling for suspicious promo code attempts, integrating a notification system for better admin awareness.
2025-08-03 21:09:03 +03:00
machka-pasla aa97618165 Refactor notification service and enhance user management features
- Replaced legacy payment notification functions with a new NotificationService for improved code organization and maintainability.
- Updated user management handlers to utilize the new notification system for payment confirmations.
- Enhanced user card formatting with localized labels for better clarity and user experience.
- Introduced context management for the PanelApiService to ensure proper session handling.
- Updated localization files to include new labels and messages for user management and notifications.
2025-08-03 18:58:50 +03:00
machka-pasla 973e97c54e Refactor promo code creation process and remove unnecessary handlers
- Updated the promo code creation flow to a four-step process, enhancing user guidance and validation.
- Removed the optional description step from the promo code creation, streamlining the process.
- Updated localization files to reflect changes in the promo code creation steps and success messages.
- Enhanced error handling and logging for panel statistics retrieval in the admin statistics handler.
2025-08-03 18:44:12 +03:00
machka-pasla 4f6822f8e4 Enhance promo code management and add bulk creation functionality
- Refactored the promo code creation process into a step-by-step flow, improving user guidance and validation.
- Introduced bulk promo code creation with a structured multi-step process, allowing admins to create multiple codes efficiently.
- Added new states for managing the promo code creation process and bulk operations.
- Implemented panel statistics retrieval to provide admins with insights into system performance and user activity.
- Updated localization files to support new messages and prompts related to promo code management and statistics.
2025-08-03 18:20:47 +03:00
machka-pasla 0f26c9bdba Implement user subscription deletion and banned users management
- Replaced the trial reset functionality to delete all user subscriptions instead of deactivating them, ensuring a complete reset of trial eligibility.
- Added a new method to retrieve banned users from the database, enhancing the admin's ability to manage user bans.
- Updated user management handlers to utilize the new subscription deletion and banned users retrieval features.
- Enhanced localization files to support new messages related to banned users.
2025-08-03 17:51:37 +03:00
machka-pasla 39ba57951c Add field validation for optional integer settings
- Introduced a field validator for LOG_CHAT_ID and LOG_THREAD_ID to convert empty strings to None, ensuring proper handling of optional integer fields.
- This enhancement improves data integrity and validation within the settings configuration.
2025-08-03 17:37:29 +03:00
machka-pasla ffa61f673d Add Docker Buildx setup to development workflow
- Integrated Docker Buildx setup step in the GitHub Actions workflow for enhanced multi-platform builds.
- This addition streamlines the Docker image building process, improving efficiency and flexibility.
2025-08-03 17:32:03 +03:00
machka-pasla f0bbe90f80 Add Docker build workflow and enhance admin functionalities
- Introduced a new GitHub Actions workflow for building and pushing the development Docker image.
- Added inline mode handling for user interactions, allowing users to share referral links and view statistics.
- Enhanced admin functionalities with new sections for user management, statistics, and promo code management.
- Implemented CSV export for logs and improved notification services for various events, including new user registrations and payment notifications.
- Updated localization files to support new features and commands.
2025-08-03 15:59:37 +03:00
Machka PaslaandGitHub 35d798a429 Merge pull request #59 from machka-pasla/e4i3yn-codex/update-api-service-and-.env-variables
Update API service for squads
2025-07-29 15:31:49 +03:00
Machka Pasla b34eb733b9 Update API service for new squads API 2025-07-29 15:31:26 +03:00
Machka PaslaandGitHub fcd456dce9 Merge pull request #58 from machka-pasla/7was2g-codex/check-aiohttp-and-asyncio-interactions
Close services and update event loop usage
2025-07-29 14:45:50 +03:00
Machka Pasla 8246e9a590 Close sessions for all services on shutdown 2025-07-29 14:44:55 +03:00
Machka PaslaandGitHub 14f244ab79 tribute logger 2025-07-27 21:30:21 +03:00
Machka PaslaandGitHub 956ec5ea71 Update requirements.txt 2025-07-25 20:25:20 +03:00
Machka PaslaandGitHub 2ef420ff47 Merge pull request #49 from machka-pasla/dependabot/pip/aiohttp-3.12.14
Bump aiohttp from 3.10.11 to 3.12.14
2025-07-25 20:23:55 +03:00
Machka PaslaandGitHub 7de1cdf9d6 Merge pull request #54 from machka-pasla/codex/use-fiat-asset-instead-of-crypto-asset
Support fiat invoices in Crypto Pay
2025-07-25 13:16:00 +03:00
Machka Pasla 860c2298bd Support fiat invoices in Crypto Pay 2025-07-25 13:15:46 +03:00
Machka PaslaandGitHub 2eff9300d3 Merge pull request #53 from machka-pasla/por3bg-codex/integrate-crypto-bot-with-crypto-pay-api
Fix CryptoPay import path
2025-07-25 12:53:48 +03:00
Machka PaslaandGitHub 1bd09be005 Merge branch 'main' into por3bg-codex/integrate-crypto-bot-with-crypto-pay-api 2025-07-25 12:53:41 +03:00
Machka Pasla ce571f6566 Fix CryptoPay import 2025-07-25 12:52:51 +03:00
Machka PaslaandGitHub 856129fce2 Merge pull request #52 from machka-pasla/codex/integrate-crypto-bot-with-crypto-pay-api
Add Crypto Pay integration
2025-07-25 12:17:36 +03:00
Machka Pasla b67f659746 Refine CryptoBot labels and .env layout 2025-07-25 12:16:39 +03:00
Machka PaslaandGitHub a102903814 hotfix 2025-07-22 21:58:19 +03:00
dependabot[bot]andGitHub 7f6cca2f59 Bump aiohttp from 3.10.11 to 3.12.14
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.11 to 3.12.14.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.11...v3.12.14)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.12.14
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 07:25:39 +00:00
Machka PaslaandGitHub 3e6ef9de32 Merge pull request #48 from machka-pasla/l9vj54-codex/refactor-payment-success-messages
Refine payment success flow
2025-07-21 10:15:16 +03:00
Machka Pasla 9cf395c103 Use full payment messages and remove unused locales 2025-07-21 10:14:59 +03:00
Machka PaslaandGitHub 69429dce81 Merge pull request #47 from machka-pasla/codex/update-purchase-confirmation-messages
Integrate referral info into purchase message
2025-07-20 21:05:49 +03:00
Machka Pasla d3bfe774c7 Refine purchase and promo messages 2025-07-20 21:04:52 +03:00
Machka PaslaandGitHub e2df8a01c8 Merge pull request #46 from machka-pasla/codex/improve-ux-after-purchase-confirmation
Improve subscription purchase flow
2025-07-20 13:11:30 +03:00
Machka Pasla dbeee31081 Improve post-purchase message 2025-07-20 13:10:44 +03:00
Machka PaslaandGitHub aa5a68c32c Merge pull request #45 from machka-pasla/codex/refactor-subscription-notifications-style
Update subscription expiration notifications
2025-07-12 10:02:00 +07:00
Machka Pasla a7b4863889 Add subscribe reminder buttons and update notifications 2025-07-12 10:01:25 +07:00
Machka PaslaandGitHub 16a00efcc9 Merge pull request #44 from machka-pasla/codex/log-event-and-telegramid-if-available
Log panel webhook event and telegram ID
2025-07-12 00:36:33 +07:00
Machka Pasla 2779e62e43 Log panel webhook events 2025-07-12 00:35:41 +07:00
Machka PaslaandGitHub d7fdcc25cd Merge pull request #43 from machka-pasla/7ql8c7-codex/fix-webhook-without-telegramid-warning
Fix webhook user extraction and notification logic
2025-07-12 00:24:48 +07:00
Machka PaslaandGitHub 24c4bbc27e Merge branch 'main' into 7ql8c7-codex/fix-webhook-without-telegramid-warning 2025-07-12 00:24:40 +07:00
Machka Pasla 5b08ad5449 Fix panel webhook user data parsing 2025-07-12 00:22:35 +07:00
Machka PaslaandGitHub fa321426b8 Merge pull request #42 from machka-pasla/codex/fix-webhook-without-telegramid-warning
Handle panel webhook data variant
2025-07-12 00:06:28 +07:00
Machka Pasla a4839ffc63 Fix panel webhook user data parsing 2025-07-12 00:06:11 +07:00
Machka PaslaandGitHub 0aa38d3806 Merge pull request #41 from machka-pasla/qrbsms-codex/add-user-notifications-for-expiration-webhooks
Handle panel webhook variants
2025-07-11 23:41:38 +07:00
Machka Pasla c7b124ad4c Handle panel webhook variants and remove Tribute cancel 2025-07-11 23:40:33 +07:00
Machka PaslaandGitHub b5b740611c Merge pull request #39 from machka-pasla/codex/add-promo-code-activation-notifications
Add promo activation and tribute/star payment notifications
2025-07-11 19:29:29 +07:00
Machka Pasla 68459a7075 Add admin notifications for promos and non-YK payments 2025-07-11 19:28:45 +07:00
Machka PaslaandGitHub 0e84a480fc Merge pull request #38 from machka-pasla/codex/fix-unhandled-telegrambadrequest-exception
Fix HTML name escaping
2025-07-09 14:34:06 +07:00
Machka Pasla 1ac368354d Escape user names in HTML messages 2025-07-09 14:33:53 +07:00
Machka PaslaandGitHub 831e61cd99 Merge pull request #37 from machka-pasla/codex/refactor-payment-logic-for-subscription-periods
Update Tribute webhook period parsing
2025-07-08 19:09:57 +07:00
Machka Pasla 981d63c53e Update Tribute webhook period parsing 2025-07-08 19:09:43 +07:00
Machka PaslaandGitHub 4d8cf45bda Merge pull request #31 from machka-pasla/codex/remove-auto-renew-for-attribute
Disable tribute auto-renew and add admin alerts
2025-07-06 13:31:14 +07:00
Machka Pasla df987fc52a Remove tribute auto renew and add admin notifications 2025-07-06 13:31:04 +07:00
Machka PaslaandGitHub 07cb68457f Merge pull request #30 from machka-pasla/codex/уточнить-работу-автопродления-с-tribute
Implement auto-renewal for Tribute
2025-07-05 00:05:51 +07:00
Machka Pasla 1e666f90d3 Improve Tribute auto-renew 2025-07-05 00:05:33 +07:00
Machka PaslaandGitHub 6241f766a2 Merge pull request #29 from machka-pasla/codex/add-description-support-for-/start-command
Fix mini app registration
2025-07-04 22:45:19 +07:00
Machka Pasla 451581f792 Fix mini app registration 2025-07-04 22:43:04 +07:00
Machka PaslaandGitHub a015946b20 Merge pull request #28 from machka-pasla/codex/add-description-support-for-/start-command
Enable mini app via API without menu change
2025-07-04 22:35:48 +07:00
Machka Pasla d1d701bff3 feat: allow /start description and keep menu default 2025-07-04 22:35:13 +07:00
Machka PaslaandGitHub ea50567a42 Merge pull request #26 from machka-pasla/codex/добавить-поддержку-mini-app-для-подписки
Support subscription mini app
2025-07-04 20:45:59 +07:00
Machka Pasla a8b62ccc03 Add subscription mini app support 2025-07-04 20:45:27 +07:00
Machka PaslaandGitHub 0e03d72dd3 Merge pull request #25 from machka-pasla/codex/refactor-.env-variables-for-consistency
Fix panel user sync and clarify traffic settings
2025-06-29 00:53:44 +03:00
Machka Pasla aa5eb74d75 Rename user traffic env vars 2025-06-29 00:52:27 +03:00
Machka PaslaandGitHub fd2d0dd570 Merge pull request #24 from machka-pasla/codex/fix-typeerror-with-unsupported-operand-types-for
Fix admin state filter
2025-06-27 20:58:44 +03:00
Machka Pasla abaaf65da9 Fix state filter in promo codes handler 2025-06-27 20:58:00 +03:00
Machka PaslaandGitHub 699964adc0 Merge pull request #23 from machka-pasla/dev
Dev
2025-06-27 20:44:01 +03:00
Machka PaslaandGitHub 74b5587c77 Merge pull request #22 from machka-pasla/codex/refactor-code-for-readability-and-consistency
Add promo management and unify user lookup
2025-06-27 20:43:25 +03:00
Machka Pasla d196801e2e Add promo management and unified user lookup 2025-06-27 20:38:03 +03:00
Machka PaslaandGitHub c12958e9a2 Merge pull request #21 from machka-pasla/codex/fix-activeuserinbounds-filter-in-api-request
Fix default inbounds not applied on user creation
2025-06-27 14:29:46 +03:00
Machka Pasla f4bb61dd1b Use default inbound UUIDs when creating panel user 2025-06-27 14:29:16 +03:00
Machka PaslaandGitHub 24a7e6cc6a Merge pull request #20 from machka-pasla/codex/fix-payment-error-with-tribute
Fix Tribute duplicate payment error
2025-06-27 00:17:27 +03:00
Machka Pasla c6f5e06eba Handle duplicate Tribute payments 2025-06-27 00:17:09 +03:00
Machka PaslaandGitHub ca35567012 Fix typo 2025-06-26 01:36:43 +03:00
Machka PaslaandGitHub cdefe27f89 Merge pull request #18 from machka-pasla/codex/implement-user-creation-and-subscription-updates
Improve panel user handling and promo code activation
2025-06-26 01:28:36 +03:00
Machka Pasla 7200d6e342 Handle missing panel users and allow promo codes 2025-06-26 01:26:55 +03:00
Machka PaslaandGitHub fd4b9b828b Merge pull request #16 from machka-pasla/codex/refactor-panel-unreachable-handling
Fix panel unreachable handling
2025-06-26 00:59:03 +03:00
Machka Pasla b2a0ea5bd2 Remove panel unreachable placeholders and enhance sync 2025-06-26 00:57:48 +03:00
Machka PaslaandGitHub b97774c5b5 Update README.md 2025-06-25 22:15:23 +03:00
Machka PaslaandGitHub d5b4f18dbc Merge pull request #14 from machka-pasla/dev
Move tribute webhook route to service
2025-06-24 19:18:34 +03:00
Machka PaslaandGitHub a94ee8b554 Merge pull request #13 from machka-pasla/codex/locate-usage-of-handlers/webhooks-folder
Move tribute webhook route to service
2025-06-24 19:15:09 +03:00
Machka Pasla 7a90e24427 Move tribute webhook route to service 2025-06-24 19:12:17 +03:00
Machka PaslaandGitHub 891f9354ac Merge pull request #12 from machka-pasla/dev
Tribute and TG stars payment
2025-06-24 10:34:36 +03:00
Machka PaslaandGitHub 89709d6626 Merge pull request #10 from machka-pasla/3eerd1-codex/update-payment-methods-variables-and-readme.md
Add payment method toggles and new pricing variables
2025-06-24 01:36:31 +03:00
Machka Pasla eea6afbcc7 Refactor payment logic into dedicated services 2025-06-24 01:35:20 +03:00
Machka PaslaandGitHub 99539542ca Merge pull request #9 from machka-pasla/codex/update-payment-methods-variables-and-readme.md
Add payment method toggles and new pricing variables
2025-06-24 01:18:49 +03:00
Machka Pasla 6e8f810cbd feat: add payment method toggles and new pricing vars 2025-06-24 01:18:30 +03:00
Machka PaslaandGitHub c7294ddaba Merge pull request #8 from machka-pasla/codex/fix-unsuccessful-payment-notification-and-nonetype-error
Fix payment success notifications
2025-06-24 00:40:14 +03:00
Machka Pasla cb82637b8e Fix payment notifications 2025-06-23 23:59:17 +03:00
Machka Pasla aa20ac9a18 Notify users after Tribute payment 2025-06-23 23:42:24 +03:00
Machka PaslaandGitHub 53464ab25c Merge pull request #3 from machka-pasla/codex/locate-and-fix-apscheduler-duplicate-notifications
Fix duplicate scheduler instances
2025-06-23 20:13:03 +03:00
Machka Pasla 1e0e883c6e Prevent duplicate scheduler startup 2025-06-23 20:12:44 +03:00
machka d14afc5720 locales update 2025-05-23 10:28:08 +00:00
machka 24706388c3 user_keyboards.py update 2025-05-23 10:16:53 +00:00
machka df9e36928c Broadcast HTML fix and subscription keyboards improvements 2025-05-21 19:30:25 +00:00
machka c245ccb55b requirements.txt update 2025-05-21 15:45:41 +00:00
machka ba348eb639 Logger and docker-compose.yml update 2025-05-21 12:23:11 +00:00
machka f6ce39f7c1 Dockerfile update №2 2025-05-21 11:14:13 +00:00
machka 4732a97611 Dockerfile update 2025-05-21 11:04:05 +00:00
machka 0fe29c19b1 ru.json update 2025-05-21 07:59:38 +00:00
machka 7da42c9e25 README update 2 2025-05-21 07:55:38 +00:00
machka e490769044 README update 2025-05-21 07:53:23 +00:00
machka dc8ba3ce06 Exclude readme.md from docker-publish.yml 2025-05-20 19:23:26 +00:00
612 changed files with 293849 additions and 7875 deletions
+142
View File
@@ -0,0 +1,142 @@
# Git
.gitignore
.github
.gitattributes
LICENSE
README.md
.env
.env.*
!.env.example
scratch_*.py
scratch/
.claude/
*.local.*
node_modules/
frontend/node_modules/
docs-site/node_modules/
docs-site/.astro/
docs-site/dist/
docs-site/src/content/docs/
deploy/compose/docker-compose-dev.yml
data/*
!data/tariffs.example.json
!data/locales-overrides.example.json
# CI
.codeclimate.yml
.travis.yml
.taskcluster.yml
# Docker
docker-compose.yml
deploy/compose/*.yml
.dockerignore
tmp/
# WebApp build artifacts (regenerated inside Docker)
bot/app/web/templates/subscription_webapp.css
bot/app/web/templates/subscription_webapp.js
bot/app/web/templates/subscription_webapp.min.*.js
bot/app/web/templates/subscription_webapp.*.css
bot/app/web/templates/subscription_webapp.min.*.js.br
bot/app/web/templates/subscription_webapp.min.*.js.gz
bot/app/web/templates/subscription_webapp.*.css.br
bot/app/web/templates/subscription_webapp.*.css.gz
bot/app/web/templates/subscription_webapp_admin.css
bot/app/web/templates/subscription_webapp_admin.js
bot/app/web/templates/subscription_webapp_admin.min.*.js
bot/app/web/templates/subscription_webapp_admin.*.css
bot/app/web/templates/subscription_webapp_admin.min.*.js.br
bot/app/web/templates/subscription_webapp_admin.min.*.js.gz
bot/app/web/templates/subscription_webapp_admin.*.css.br
bot/app/web/templates/subscription_webapp_admin.*.css.gz
backend/bot/app/web/templates/subscription_webapp.css
backend/bot/app/web/templates/subscription_webapp.js
backend/bot/app/web/templates/subscription_webapp.min.*.js
backend/bot/app/web/templates/subscription_webapp.*.css
backend/bot/app/web/templates/subscription_webapp.min.*.js.br
backend/bot/app/web/templates/subscription_webapp.min.*.js.gz
backend/bot/app/web/templates/subscription_webapp.*.css.br
backend/bot/app/web/templates/subscription_webapp.*.css.gz
backend/bot/app/web/templates/subscription_webapp_admin.css
backend/bot/app/web/templates/subscription_webapp_admin.js
backend/bot/app/web/templates/subscription_webapp_admin.min.*.js
backend/bot/app/web/templates/subscription_webapp_admin.*.css
backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.br
backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.gz
backend/bot/app/web/templates/subscription_webapp_admin.*.css.br
backend/bot/app/web/templates/subscription_webapp_admin.*.css.gz
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Virtual environment
.env*
.venv/
venv/
# PyCharm
.idea
# Python mode for VIM
.ropeproject
**/.ropeproject
# Vim swap files
**/*.swp
# VS Code
.vscode/
+59 -62
View File
@@ -1,78 +1,75 @@
# Telegram Bot Token and Admin IDs
# Minimal bootstrap env.
# Most product settings are configured later in Web App admin:
# Admin -> System -> Settings, Admin -> System -> Tariffs, Admin -> Appearance.
# Full reference: docs/env-vars.md
# Telegram bot token from @BotFather.
# Example: 1234567890:AA...
BOT_TOKEN=your_bot_token_here
ADMIN_IDS=comma_separated_admin_ids
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_HOST=remnawave-tg-shop-db
POSTGRES_PORT=5432
POSTGRES_DB=postgres
# Telegram numeric user IDs allowed to open the admin panel.
# Use commas for several admins, for example: 123456789,987654321
ADMIN_IDS=123456789
# Localization and Display
DEFAULT_LANGUAGE="ru" # or "en"
DEFAULT_CURRENCY_SYMBOL="RUB" # e.g., RUB, USD, EUR
# Public HTTPS base URL of the backend webhook server.
# Telegram, payment providers and Remnawave call webhook endpoints under this domain.
# This is usually the backend/API domain, not the Mini App frontend domain.
# Example: https://bot.yourdomain.tld
WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld
# External Links
SUPPORT_LINK=https://t.me/your_support_link
SERVER_STATUS_URL=https://status.yourdomain.tld/status/your_service
TERMS_OF_SERVICE_URL=https://example.com/tos
# PostgreSQL user created by Docker Compose and used by the backend.
POSTGRES_USER=remnawave_minishop
# YooKassa Payment Gateway Configuration
YOOKASSA_SHOP_ID=your_shop_id
YOOKASSA_SECRET_KEY=your_secret_key
YOOKASSA_WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld
YOOKASSA_RETURN_URL=https://t.me/your_bot
YOOKASSA_DEFAULT_RECEIPT_EMAIL=your_email@example.com
YOOKASSA_VAT_CODE=1
YOOKASSA_PAYMENT_MODE=full_prepayment
YOOKASSA_PAYMENT_SUBJECT=payment
# PostgreSQL password. Change it before production deploy.
POSTGRES_PASSWORD=change_me
# Telegram Webhook Configuration (Optional)
# If unset, bot will use polling while YooKassa uses webhooks.
TELEGRAM_WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld
# PostgreSQL database name created by Docker Compose.
POSTGRES_DB=remnawave_minishop
# Subscription Prices (integer values)
PRICE_1_MONTH=150
PRICE_3_MONTHS=300
PRICE_6_MONTHS=500
PRICE_12_MONTHS=900
# Enables the Web App and the Web App admin panel.
# Keep True for the first setup. If set to False, the admin UI is unavailable
# until you change it back to True in .env and restart the app.
WEBAPP_ENABLED=True
# Subscription Expiration Notifications
SUBSCRIPTION_EXPIRATION_NOTIFICATION_DAYS=7
SUBSCRIPTION_NOTIFICATION_HOUR_UTC=13
SUBSCRIPTION_NOTIFICATION_MINUTE_UTC=0
# Stable secret for Web App sessions.
# Generate with: openssl rand -hex 32
# If empty, sessions are invalidated on every restart.
WEBAPP_SESSION_SECRET=
# Referral Bonus Days
REFERRAL_BONUS_DAYS_1_MONTH=3
REFERRAL_BONUS_DAYS_3_MONTHS=7
REFERRAL_BONUS_DAYS_6_MONTHS=15
REFERRAL_BONUS_DAYS_12_MONTHS=30
REFEREE_BONUS_DAYS_1_MONTH=1
REFEREE_BONUS_DAYS_3_MONTHS=3
REFEREE_BONUS_DAYS_6_MONTHS=7
REFEREE_BONUS_DAYS_12_MONTHS=15
# Stable Telegram webhook secret_token.
# Generate with: openssl rand -hex 32
# If empty, a new token can be generated on process start.
WEBHOOK_SECRET_TOKEN=
# Panel API Configuration
PANEL_API_URL=http://your_panel_api_url/api
PANEL_API_KEY=your_panel_api_key
# Public HTTPS URL of the Mini App frontend, with trailing slash.
# This URL is opened by Telegram buttons and BotFather Mini App settings.
# Do not put /api or webhook paths here.
# Example: https://app.yourdomain.tld/
SUBSCRIPTION_MINI_APP_URL=https://app.yourdomain.tld/
# Default settings for NEW panel users
PANEL_USER_DEFAULT_EXPIRE_DAYS=1
PANEL_USER_DEFAULT_TRAFFIC_BYTES=0
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY="NO_RESET"
# Remnawave panel API URL. Usually the panel domain plus /api.
# Example: https://panel.yourdomain.tld/api
PANEL_API_URL=https://panel.yourdomain.tld/api
# Default Inbounds for Panel Users (Optional, comma-separated UUIDs)
PANEL_USER_DEFAULT_INBOUND_UUIDS=uuid1,uuid2,uuid3
# Remnawave API key with permissions to manage users, subscriptions and squads.
# Keep this secret. It can be overridden later in the admin panel if needed.
PANEL_API_KEY=
# Trial Settings
TRIAL_ENABLED=True
TRIAL_DURATION_DAYS=5
TRIAL_TRAFFIC_LIMIT_GB=0 # 0 for unlimited
TRIAL_TRAFFIC_STRATEGY="NO_RESET"
# Shared secret for validating incoming Remnawave webhooks.
# Create or set this secret in Remnawave Panel, then paste the same value here
# or into Admin -> System -> Settings -> Remnawave Panel.
# In Remnawave Panel, set WEBHOOK_URL to WEBHOOK_BASE_URL + /webhook/panel,
# for example: https://app.example.com/webhook/panel
PANEL_WEBHOOK_SECRET=
# Web Server Settings (for handling webhooks)
WEB_SERVER_HOST="0.0.0.0"
# Host port that publishes the backend webhook server from Docker Compose.
# Your reverse proxy should route WEBHOOK_BASE_URL traffic to this port.
WEB_SERVER_PORT=8080
# Admin Panel Log Pagination
LOGS_PAGE_SIZE=10
# Host port that publishes the frontend nginx from Docker Compose.
# Your reverse proxy should route SUBSCRIPTION_MINI_APP_URL traffic to this port.
FRONTEND_PORT=8082
# Reverse proxy IPs/CIDRs trusted for X-Forwarded-For.
# Keep loopback for local proxy; add your proxy network if needed.
TRUSTED_PROXIES=127.0.0.1,::1
+9
View File
@@ -0,0 +1,9 @@
.gitattributes text eol=lf
*.sh text eol=lf
.github/workflows/*.yml text eol=lf
deploy/docker/frontend/*.sh text eol=lf
frontend/src/*.js text eol=lf
frontend/src/**/*.js text eol=lf
frontend/src/**/*.svelte text eol=lf
frontend/scripts/*.mjs text eol=lf
frontend/scripts/**/*.mjs text eol=lf
+117
View File
@@ -0,0 +1,117 @@
name: Docker build & push (reusable)
# Reusable workflow that builds the three image targets defined in
# deploy/docker/Dockerfile (backend, worker, frontend) and optionally pushes
# them to both ghcr.io and Docker Hub under the 3252a8/ namespace.
#
# Called by:
# - docker-dev.yml (tag_mode: dev, push: true) on pushes to dev
# - docker-release.yml (tag_mode: release, push: true) on pushes to main
# - ci.yml (tag_mode: dev, push: false) on pull requests
on:
workflow_call:
inputs:
push:
description: "Push the built images to the registries"
type: boolean
default: true
tag_mode:
description: "Tagging strategy: 'dev' or 'release'"
type: string
required: true
# No permissions block here on purpose: a reusable workflow cannot request more
# than its caller grants, so the token scope is set by each caller
# (docker-dev.yml / docker-release.yml grant packages: write to push; ci.yml
# only needs contents: read for a no-push build).
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: backend
image: remnawave-minishop-backend
- target: worker
image: remnawave-minishop-worker
- target: frontend
image: remnawave-minishop-frontend
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history + tags: the Dockerfile's version-builder stage runs
# `git describe --tags` against the copied .git tree.
fetch-depth: 0
- name: Resolve release version
id: version
if: inputs.tag_mode == 'release'
run: |
# On a tag push github.ref_name is the tag (e.g. v3.4.5); for a
# manual workflow_dispatch on a branch, fall back to the latest tag.
if [ "${{ github.ref_type }}" = "tag" ]; then
raw="${{ github.ref_name }}"
else
raw="$(git describe --tags --abbrev=0 2>/dev/null)"
fi
version="${raw#v}"
if [ -z "$version" ]; then
echo "::error::No git tag found to derive the release version from"
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "Release version: ${version}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: inputs.push
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
if: inputs.push
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
3252a8/${{ matrix.image }}
ghcr.io/3252a8/${{ matrix.image }}
tags: |
type=raw,value=dev,enable=${{ inputs.tag_mode == 'dev' }}
type=sha,prefix=dev-,format=short,enable=${{ inputs.tag_mode == 'dev' }}
type=raw,value=latest,enable=${{ inputs.tag_mode == 'release' }}
type=raw,value=${{ steps.version.outputs.version }},enable=${{ inputs.tag_mode == 'release' }}
- name: Build${{ inputs.push && ' & push' || '' }} ${{ matrix.image }}
uses: docker/build-push-action@v6
with:
context: .
file: deploy/docker/Dockerfile
target: ${{ matrix.target }}
platforms: linux/amd64
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# The Dockerfile's version-builder appends a "-<branch>" suffix to the
# internal version string for non-main builds. Force "main" on release
# (the ref is the tag, not a branch) so release images stay un-suffixed.
build-args: |
GITHUB_REF_NAME=${{ inputs.tag_mode == 'release' && 'main' || github.ref_name }}
cache-from: type=gha,scope=${{ matrix.target }}
cache-to: type=gha,mode=max,scope=${{ matrix.target }}
provenance: false
+64
View File
@@ -0,0 +1,64 @@
name: PR checks
# Runs on pull requests into main (typically from dev) and into dev (typically
# from feature/* branches): lint + format checks and a no-push image build to
# prove the Docker images still build.
on:
pull_request:
branches: [main, dev]
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint & format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ruff
run: pip install "ruff>=0.8.0"
- name: Ruff lint (Python)
run: ruff check .
- name: Ruff format check (Python)
run: ruff format --check .
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install frontend deps
run: npm ci
working-directory: frontend
- name: ESLint (frontend)
run: npm run lint
working-directory: frontend
- name: Prettier check (frontend)
run: npm run format:check
working-directory: frontend
build:
name: Docker build
uses: ./.github/workflows/_docker-build-push.yml
with:
push: false
tag_mode: dev
+27
View File
@@ -0,0 +1,27 @@
name: Dependency review
# On PRs into main/dev, flag any newly added dependency that has a known
# vulnerability or an incompatible license before it gets merged.
on:
pull_request:
branches: [main, dev]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Dependency review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
comment-summary-in-pr: on-failure
+25
View File
@@ -0,0 +1,25 @@
name: Dev images
# On every push to the dev branch, build all three images and push them to
# ghcr.io and Docker Hub tagged `dev` and `dev-<short-sha>`.
on:
push:
branches: [dev]
workflow_dispatch:
concurrency:
group: docker-dev-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
jobs:
build-push:
uses: ./.github/workflows/_docker-build-push.yml
with:
push: true
tag_mode: dev
secrets: inherit
-42
View File
@@ -1,42 +0,0 @@
name: Build and Publish multi-arch Docker Image
on:
push:
branches:
- main
tags:
- 'v*.*.*'
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU (для эмуляции arm64 на x86)
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
+28
View File
@@ -0,0 +1,28 @@
name: Release images
# Build all three images and push them to ghcr.io and Docker Hub tagged
# `latest` and the release version (the pushed tag with its leading `v`
# stripped, e.g. v3.4.5 -> 3.4.5). Triggered only when a new v* tag is pushed,
# so images are built once per release rather than on every commit to main.
on:
push:
tags:
- "v*"
workflow_dispatch:
concurrency:
group: docker-release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
packages: write
jobs:
build-push:
uses: ./.github/workflows/_docker-build-push.yml
with:
push: true
tag_mode: release
secrets: inherit
+96
View File
@@ -0,0 +1,96 @@
name: Security
# Audits the full dependency set (pip-audit, npm audit) and runs a Trivy
# filesystem scan (dependencies + Dockerfile/IaC misconfig). Trivy results are
# uploaded to the Security -> Code scanning tab.
#
# pip-audit / npm audit are informational (continue-on-error): they surface
# upstream/transitive advisories that aren't necessarily fixable in a given PR,
# so they report in the logs without blocking merges. The PR gate for newly
# introduced vulnerable deps is dependency-review.yml.
on:
pull_request:
branches: [main, dev]
push:
branches: [main, dev]
schedule:
- cron: "27 4 * * 1" # weekly, Monday 04:27 UTC
workflow_dispatch:
concurrency:
group: security-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
python-audit:
name: pip-audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install pip-audit
run: pip install pip-audit
- name: Audit Python dependencies
continue-on-error: true
run: pip-audit -r backend/requirements.txt
npm-audit:
name: npm audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install frontend deps
run: npm ci
working-directory: frontend
- name: Audit npm dependencies
continue-on-error: true
run: npm audit --audit-level=high
working-directory: frontend
trivy:
name: Trivy filesystem scan
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Trivy
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: fs
scan-ref: .
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
ignore-unfixed: true
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
category: trivy-fs
+66
View File
@@ -3,6 +3,66 @@ bot_database.sqlite3
# Игнорировать файлы окружения
.env
.env.*
!.env.example
docker-compose-dev.yml
scratch_*.py
scratch/
*.local.*
node_modules/
.git/
# Documentation site build artifacts
docs-site/.astro/
docs-site/dist/
docs-site/public/demo/runtime/
docs-site/src/content/docs/
# WebApp build artifacts (regenerated by `npm run build:webapp` / Docker build)
bot/app/web/templates/subscription_webapp.css
bot/app/web/templates/subscription_webapp.js
bot/app/web/templates/subscription_webapp.min.*.js
bot/app/web/templates/subscription_webapp.*.css
bot/app/web/templates/subscription_webapp.min.*.js.br
bot/app/web/templates/subscription_webapp.min.*.js.gz
bot/app/web/templates/subscription_webapp.*.css.br
bot/app/web/templates/subscription_webapp.*.css.gz
bot/app/web/templates/subscription_webapp_admin.css
bot/app/web/templates/subscription_webapp_admin.js
bot/app/web/templates/subscription_webapp_admin.min.*.js
bot/app/web/templates/subscription_webapp_admin.*.css
bot/app/web/templates/subscription_webapp_admin.min.*.js.br
bot/app/web/templates/subscription_webapp_admin.min.*.js.gz
bot/app/web/templates/subscription_webapp_admin.*.css.br
bot/app/web/templates/subscription_webapp_admin.*.css.gz
bot/app/web/templates/subscription_webapp_docs_demo.css
bot/app/web/templates/subscription_webapp_docs_demo.js
bot/app/web/templates/subscription_webapp_docs_demo.*.css
bot/app/web/templates/subscription_webapp_docs_demo.*.css.br
bot/app/web/templates/subscription_webapp_docs_demo.*.css.gz
backend/bot/app/web/templates/subscription_webapp.css
backend/bot/app/web/templates/subscription_webapp.js
backend/bot/app/web/templates/subscription_webapp.min.*.js
backend/bot/app/web/templates/subscription_webapp.*.css
backend/bot/app/web/templates/subscription_webapp.min.*.js.br
backend/bot/app/web/templates/subscription_webapp.min.*.js.gz
backend/bot/app/web/templates/subscription_webapp.*.css.br
backend/bot/app/web/templates/subscription_webapp.*.css.gz
backend/bot/app/web/templates/subscription_webapp_admin.css
backend/bot/app/web/templates/subscription_webapp_admin.js
backend/bot/app/web/templates/subscription_webapp_admin.min.*.js
backend/bot/app/web/templates/subscription_webapp_admin.*.css
backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.br
backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.gz
backend/bot/app/web/templates/subscription_webapp_admin.*.css.br
backend/bot/app/web/templates/subscription_webapp_admin.*.css.gz
backend/bot/app/web/templates/subscription_webapp_docs_demo.css
backend/bot/app/web/templates/subscription_webapp_docs_demo.js
backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css
backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css.br
backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css.gz
tmp
.claude
# Игнорировать кэш Python
__pycache__/
@@ -14,3 +74,9 @@ __pycache__/
# Игнорировать Docker артефакты (если вдруг)
*.log
*.pid
locales/ru_backup.json
locales/en_backup.json
db/models_old.py
data/*
!data/tariffs.example.json
!data/locales-overrides.example.json
-11
View File
@@ -1,11 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-2026 machka-pasla, 3252a8 and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+113 -186
View File
@@ -1,223 +1,150 @@
# Remnawave Minishop
# Remnawave Subscription Sales Telegram Bot
![Remnawave Minishop](docs/remnawave-minishop.webp)
This Telegram bot is designed to automate the sale and management of subscriptions for a **Remnawave panel**. It integrates with the Remnawave API for user and subscription management and uses YooKassa for processing payments.
Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи и управления подписками панели [Remnawave](https://docs.rw/). Бот обрабатывает регистрацию, оплату, продление, пробный период, промокоды, рефералов и поддержку в чате. Web App показывает ссылку подключения, срок действия, трафик, оплату, устройства и вход по Telegram Mini Apps `initData`, Telegram OAuth / OpenID Connect и одноразовому email-коду.
## ✨ Features
Проект является переработанным форком [kavore/remnawave-tg-shop](https://github.com/kavore/remnawave-tg-shop). Для переноса данных из прежнего стека и других ботов используйте [раздел миграций](docs/migrations/index.md).
* **User Interaction:**
* User registration with language selection (English/Russian).
* Display of main menu with available actions via inline keyboards.
* Ability for users to view their current subscription status, expiration date, and configuration link.
* Trial subscription system for new users (configurable, manual activation via button).
* Promo code system for users to apply discounts or bonuses.
* Referral program for users to earn bonus subscription days.
* **Subscription Management:**
* Handles subscription purchases for various periods (1, 3, 6, 12 months).
* Integrates with **YooKassa** for payment processing, including fiscal receipt data.
* Automatic subscription activation/extension upon successful payment.
* Link and syncs users with a **Remnawave panel** account, primarily matching by Telegram ID.
* Updates user status, expiration dates, traffic limits, and inbounds on the Remnawave panel.
* **Admin Panel:**
* Protected by `ADMIN_IDS` (supports multiple administrators).
* **Statistics:** View bot usage (total users, banned, active subscriptions), recent payments, and panel sync status.
* **User Management:**
* Ban/Unban users by Telegram ID or @username (updates local DB and panel).
* View a paginated list of banned users.
* View a "user card" with detailed information and unban option.
* **Broadcast:** Send messages to all users, users with active subscriptions, or users with expired subscriptions.
* **Promo Codes:** Create and view promo codes (bonus days, activation limits, validity).
* **Panel Sync:** Manually trigger synchronization of users and subscriptions from the Remnawave panel to the bot's database, matching by Telegram ID.
* **Activity Logs:** View a paginated list of all user actions (messages, commands, callbacks) or logs for a specific user.
* **Notifications:**
* Automated daily notifications to users about expiring subscriptions (via APScheduler).
* Notifications to users and inviters upon successful referral bonus application.
* Notifications to admin(s) about suspicious promo code input attempts.
* **Security & Technical:**
* Uses parameterized queries to prevent SQL injection.
* Proactive check for suspicious input in promo code field (notifies admin).
* Middleware for checking banned users on every interaction.
* Middleware for logging user actions.
* Webhook support for Telegram and YooKassa for efficient updates.
* Configurable via `.env` file.
* Dockerized for easy deployment.
## Возможности
## 🚀 Technologies Used
Для пользователей:
* **Python 3.11**
* **Aiogram 3.x:** Asynchronous Telegram Bot Framework
* **aiohttp:** For running the webhook server
* **aiosqlite:** Asynchronous SQLite database interaction
* **YooKassa SDK:** For payment processing
* **APScheduler:** For scheduled tasks (e.g., notifications)
* **Pydantic:** For settings management (loading from `.env`)
* **Docker & Docker Compose:** For containerization and deployment
- регистрация с выбором русского или английского языка;
- просмотр статуса подписки, даты окончания, ссылки подключения и трафика;
- покупка подписок, пакетов трафика, обычная и premium-докупка трафика, докупка устройств по настроенному каталогу тарифов;
- Web App / Mini App с входом через Telegram или email;
- встроенные инструкции установки в Mini App: личный экран `/install` и публичная ссылка `/s/<token>` для передачи инструкции;
- пробный период, промокоды и реферальная программа;
- оплата через YooKassa, FreeKassa, Platega, SeverPay, Wata, CryptoPay, Heleket и Telegram Stars;
- тикеты поддержки в Web App и внешняя ссылка на поддержку;
- раздел "Мои устройства" при включенном `MY_DEVICES_SECTION_ENABLED`.
## ⚙️ Setup and Configuration
Для администраторов:
### Prerequisites
- админ-панель для пользователей из `ADMIN_IDS` (только при входе через Telegram, не для аккаунтов только с email);
- статистика пользователей, подписок, платежей и синхронизации с Remnawave;
- список пользователей с поиском, фильтрами и колонкой premium-трафика;
- блокировка пользователей, поддержка через тикеты, рассылки, промокоды, логи действий и настройка разрешенных параметров приложения поверх `.env`;
- редактор JSON-каталога тарифов с моделями на срок/по трафику, Internal Squads, premium-сквадами и HWID-пакетами;
- настройки инструкций подключения: чтение конфига Subscription Page из Remnawave Panel, опциональное JSON-переопределение и переключатель поведения кнопок бота;
- ручная синхронизация пользователей и подписок с панелью.
* Docker and Docker Compose installed.
* A running instance of a Remnawave panel.
* A Telegram Bot Token.
* A YooKassa Shop ID and Secret Key.
## Документация
### Configuration Steps
- [Входная страница документации](docs/index.md) - маршрут по установке, настройке, платежам, админке и диагностике.
- [Развертывание](docs/getting-started/deployment.md) - Docker Compose, Caddy, Nginx, Pangolin/Newt и запуск без обратного прокси.
- [Настройка окружения](docs/getting-started/configuration.md) - bootstrap `.env` и рекомендуемая настройка через Web App админку.
- [Переменные `.env`](docs/configuration/env-vars.md) - полный справочник всех env-ключей по разделам.
- [Бэкапы и восстановление](docs/features/backups.md) - автоматические архивы, Telegram-отправка и restore через админку.
- [Тарифы](docs/features/tariffs.md) - каталог тарифов, модели на срок и по трафику, обычные и premium-докупки, premium-сквады, смена тарифа, HWID-лимиты и обработка трафика.
- [Админ-панель](docs/features/admin-panel.md) - права доступа, настройки, редактор тарифов, premium-сквады и сохранение JSON-каталога.
- [Веб-приложение / Mini App](docs/features/web-app.md) - отдельный порт, домен, инструкции установки и реферальные ссылки.
- [Telegram-авторизация](docs/features/telegram-auth.md) и [вход по email](docs/features/email-login.md) - настройка BotFather/OAuth и SMTP-логина.
- [Поддержка пользователей / тикеты](docs/features/support.md) - тикеты в Mini App, входящий список админки, уведомления, лимиты и внешняя ссылка поддержки.
- [Темы Web App](docs/features/webapp-themes.md) - кастомные темы, настройка внешнего вида, логотипы, CSS/ассеты и пайплайн создания новой темы.
- [Миграции](docs/migrations/index.md) - готовые сценарии переноса с других ботов; сейчас описан `remnawave-tg-shop`.
- [Миграция с remnawave-tg-shop](docs/migrations/remnawave-tg-shop.md) - готовый сценарий для legacy-стека.
1. **Clone the Repository:**
```bash
git clone https://github.com/machka-pasla/remnawave-tg-shop
cd remnawave-tg-shop
```
## Совместимость
2. **Create an `.env` File:**
Copy the `env.example` file to `.env` and fill in your specific values:
```bash
cp .env.example .env
nano .env
```
Key variables to configure in `.env`:
* `BOT_TOKEN`: Your Telegram Bot Token from BotFather.
* `ADMIN_IDS`: Comma-separated list of your Telegram User IDs for admin access (e.g., `12345678,98765432`). **Crucial for bot management.**
* `DEFAULT_LANGUAGE`: Default language for new users (e.g., `ru` or `en`).
* `DEFAULT_CURRENCY_SYMBOL`: e.g., `RUB`, `USD`.
* `SUPPORT_LINK`: (Optional) URL for a support chat/contact (e.g., `https://t.me/your_support`).
* `SERVER_STATUS_URL`: (Optional) URL to a server status page (e.g., Uptime Kuma).
* **YooKassa Settings:**
* `YOOKASSA_SHOP_ID`: Your shop ID from YooKassa.
* `YOOKASSA_SECRET_KEY`: Your secret key from YooKassa.
* `YOOKASSA_WEBHOOK_BASE_URL`: Your publicly accessible HTTPS base URL where the bot will listen for YooKassa webhooks (e.g., `https://your.domain.com`). The full path will be `{YOOKASSA_WEBHOOK_BASE_URL}/webhook/yookassa`.
* `YOOKASSA_RETURN_URL`: (Optional) URL user is redirected to after payment, often `https://t.me/your_bot_username`.
* `YOOKASSA_DEFAULT_RECEIPT_EMAIL`: **Important for 54-FZ (Russian fiscalization).** A default email for sending fiscal receipts.
* `YOOKASSA_VAT_CODE`: VAT code for items in receipt (e.g., `1` for "No VAT". Consult YooKassa documentation and tax advisor).
* `YOOKASSA_PAYMENT_MODE`: e.g., `full_prepayment`.
* `YOOKASSA_PAYMENT_SUBJECT`: e.g., `service`.
* `TELEGRAM_WEBHOOK_BASE_URL`: (Optional) If you want Telegram updates via webhook. Can be the same as `YOOKASSA_WEBHOOK_BASE_URL`. If not set, the bot will use polling for Telegram updates.
* `PRICE_X_MONTH`: Prices for different subscription durations.
* **Panel API Settings:**
* `PANEL_API_URL`: Full URL to your Remnawave panel's API (e.g., `http://localhost:3000/api` or `https://panel.yourdomain.com/api`).
* `PANEL_API_KEY`: API Key for authenticating with the Remnawave panel.
* `PANEL_USER_DEFAULT_INBOUND_UUIDS`: (Optional) Comma-separated list of inbound UUIDs from your panel to assign to users. If empty, `activateAllInbounds: true` (panel default) is used for new users.
* `TRIAL_ENABLED`, `TRIAL_DURATION_DAYS`, `TRIAL_TRAFFIC_LIMIT_GB`: Settings for the trial period.
* `WEB_SERVER_HOST`, `WEB_SERVER_PORT`: Host and port for the bot's internal webhook server.
* `LOGS_PAGE_SIZE`: For admin panel log pagination.
Интеграция с API панели Remnawave (вебхуки, пользователи, подписки, статистика в админке и т.д.) **протестирована** на панели Remnawave версии **`> 2.7.0`**. Более старые версии могут работать частично или не работать из‑за изменений в API.
3. **Locales:**
* Translation files are in the `locales/` directory (`en.json`, `ru.json`). Ensure they are present and correctly formatted. The `bot_database.sqlite3` and `locales` directory will be mounted as volumes in Docker. `locales` mounting is optional.
## Стек
4. **Build and Run with Docker Compose:**
```bash
docker compose up --build -d
```
This command will build the Docker image (if it doesn't exist or if `Dockerfile` changed) and start the `vpn-shop` service in detached mode.
Сборка и runtime задаются **deploy/docker/Dockerfile** и **docker-compose.yml**; точные версии пакетов — в **backend/requirements.txt** и **frontend/package.json**.
5. **Webhook Setup (Important if using webhooks):**
* **Reverse Proxy (Nginx, Caddy, etc.):** You need a reverse proxy to handle incoming HTTPS traffic, manage SSL certificates (e.g., from Let's Encrypt), and forward requests to your bot's container.
* Forward requests for `https://{YOOKASSA_WEBHOOK_BASE_URL_domain}/webhook/yookassa` to `http://vpn-shop:{WEB_SERVER_PORT}/webhook/yookassa` (where `vpn-shop` is the service name in `docker-compose.yml`).
* If using Telegram webhooks, forward requests for `https://{TELEGRAM_WEBHOOK_BASE_URL_domain}/<YOUR_BOT_TOKEN>` to `http://vpn-shop:{WEB_SERVER_PORT}/<YOUR_BOT_TOKEN>`.
* **Telegram Webhook Registration:** The bot attempts to set its Telegram webhook URL on startup if `TELEGRAM_WEBHOOK_BASE_URL` is configured in `.env`. Check the bot logs to confirm if this was successful. You can also manually check using the Telegram Bot API method `getWebhookInfo`.
| Слой | Технологии |
| --- | --- |
| Backend | Python **3.12**, [aiogram](https://docs.aiogram.dev/) 3.x (Telegram), **aiohttp** (HTTP и Web App), **SQLAlchemy** 2 async, **asyncpg**, **Pydantic** / pydantic-settings, **httpx**, платёжные SDK (в т.ч. YooKassa, aiocryptopay), **PyJWT** |
| Данные | **PostgreSQL** **17** (сервис `postgres` в Compose) и **Redis** **7** (сервис `redis`) |
| Сборка Web App | **Node.js** **22**, **Svelte** **5**, **Vite**, **Tailwind CSS** 4; артефакты попадают в шаблоны `backend/bot/app/web/templates/` |
6. **Database:**
* A SQLite database file (`bot_database.sqlite3`) will be created in your project root (or wherever you map the volume). The schema is initialized automatically on the first run if the file doesn't exist.
Локальная разработка без Docker возможна при установленных Python 3.12, PostgreSQL и (для пересборки фронта) Node 22; типичный сценарий — всё через Compose.
7. **Viewing Logs:**
```bash
docker compose logs -f vpn-shop
```
## Быстрый старт
## 🐳 Docker Setup
Требования:
### `Dockerfile`
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
# Consider adding build arguments for proxy if needed in your environment
# ARG HTTP_PROXY
# ARG HTTPS_PROXY
# ENV http_proxy=$HTTP_PROXY
# ENV https_proxy=$HTTPS_PROXY
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Ensure main.py is executable if needed, though python command handles it
# RUN chmod +x main.py
CMD ["python", "main.py"]
- Docker и Docker Compose;
- рабочая панель Remnawave версии **`> 2.7.0`** (см. раздел «Совместимость»);
- токен Telegram-бота;
- публичные домены для webhook и Mini App.
```bash
git clone https://github.com/3252a8/remnawave-minishop
cd remnawave-minishop
cp .env.example .env
nano .env
docker compose up -d --build
docker compose logs -f backend worker frontend
```
### `docker-compose.yml`
Минимально заполните в `.env`:
YAML
- `BOT_TOKEN` - токен Telegram-бота;
- `ADMIN_IDS` - Telegram ID администраторов через запятую;
- `WEBHOOK_BASE_URL` - публичный URL вебхуков;
- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` - доступы PostgreSQL;
- `WEBAPP_ENABLED=True` - включает Web App и админку для первого входа;
- `WEBAPP_SESSION_SECRET`, `WEBHOOK_SECRET_TOKEN` - стабильные секреты;
- `SUBSCRIPTION_MINI_APP_URL` - публичный HTTPS URL Mini App/frontend, например `https://app.domain.com/`;
- `PANEL_API_URL`, `PANEL_API_KEY`, `PANEL_WEBHOOK_SECRET` - доступ к Remnawave;
- остальные настройки удобнее задать в Web App админке.
```
services:
vpn-shop:
build: .
container_name: vpn-shop
hostname: vpn-shop
networks:
- remnawave-network # Ensure this external network exists or define it
volumes:
- ./bot_database.sqlite3:/app/bot_database.sqlite3
# - ./locales:/app/locales
restart: unless-stopped
# Optionally, expose ports if you are not using a shared Docker network
# and want to access the bot's webserver directly (not recommended for production without a reverse proxy)
# ports:
# - "8080:8080"
В Remnawave Panel укажите `WEBHOOK_URL` как публичный адрес Minishop с путем `/webhook/panel`, например `https://app.example.com/webhook/panel`. Секрет вебхука задается в самой Remnawave Panel; это же значение вставьте в `PANEL_WEBHOOK_SECRET` в `.env` или в **Система -> Настройки -> Remnawave Panel** в админке.
networks:
remnawave-network:
external: true # Assumes 'remnawave-network' is an existing external Docker network
# If not, you might want to define it here or use a default bridge.
После первого входа в админку настройте тарифы, платежные провайдеры, внешний вид, поддержку, уведомления и инструкции подключения через UI. Инструкции установки включены по умолчанию, читают Subscription Page config из Remnawave Panel и при проблемах с конфигом откатываются к обычной ссылке подключения. Полный справочник env-переменных: [docs/configuration/env-vars.md](docs/configuration/env-vars.md).
Для каталога тарифов используется `TARIFFS_CONFIG_PATH` со значением по умолчанию `data/tariffs.json`. Пример формата лежит в [data/tariffs.example.json](data/tariffs.example.json), подробности - в [docs/features/tariffs.md](docs/features/tariffs.md).
В compose-примерах `/app/data` монтируется из папки `./data` рядом с `docker-compose.yml`. Заранее создайте каталог и отдайте его пользователю контейнера. Это нужно для сохранения `data/tariffs.json`, каталога тем `data/themes`, кеша логотипа Web App и animated emoji:
```bash
mkdir -p data/themes data/webapp-logo data/webapp-emoji
touch data/locales-overrides.json
chown -R 10001:10001 data
chmod -R u+rwX data
```
**Note on `remnawave-network`:** The `docker-compose.yml` assumes an external network named `remnawave-network`. If this network doesn't exist or you want the bot on a different network (e.g., a default bridge or a new one defined in this compose file), you'll need to adjust the `networks` section. If the Remnawave panel is also running in Docker on the same host, putting them on the same user-defined network allows them to communicate using service names.
## Полезные команды
## 🛠️ Project Structure (Overview)
```bash
# Локальная сборка и запуск
docker compose up -d --build
```
.
├── bot/
│ ├── filters/ # Custom Aiogram filters (e.g., AdminFilter)
│ ├── handlers/ # Message and callback query handlers (admin and user)
│ ├── keyboards/ # Inline and reply keyboard generators
│ ├── middlewares/ # Custom Aiogram middlewares (i18n, ban check, logger)
│ ├── services/ # Business logic (payments, subscriptions, panel API interaction)
│ ├── states/ # FSM states
│ └── main_bot.py # Core bot logic, dispatcher setup, startup/shutdown
├── config/
│ └── settings.py # Pydantic settings model
├── db/
│ └── database.py # Database schema, connection, and CRUD functions
├── locales/ # Localization files (en.json, ru.json)
├── .env.example # Example environment variables
├── .env # Your local environment variables (ignored by git)
├── Dockerfile # Instructions to build the Docker image
├── docker-compose.yml # Docker Compose configuration
├── requirements.txt # Python dependencies
└── main.py # Main entry point to run the bot
# Логи приложения
docker compose logs -f backend worker frontend
# Рекомендуемый продакшен-вариант с Caddy
cd deploy/examples/caddy # или nginx, newt, no-proxy
cp .env.example .env
nano .env
docker compose up -d
# Запуск из готового образа с конкретным тегом
IMAGE_TAG=3.1.0 docker compose up -d
```
## 🤝 Contributing
Для продакшен-запуска удобнее брать готовые папки из [`deploy/examples`](deploy/examples), а читать каноничные инструкции в [docs/getting-started/deployment.md](docs/getting-started/deployment.md). Предпочтительный вариант для обычного публичного сервера - Caddy: он сам выпускает и продлевает HTTPS-сертификаты. В папках рядом с compose лежат только конфиги и короткие ссылки на документацию.
Contributions are welcome!
Имена образов для релизов:
## 🔮 Future Enhancements
- `ghcr.io/3252a8/remnawave-minishop-backend`
- `ghcr.io/3252a8/remnawave-minishop-worker`
- `ghcr.io/3252a8/remnawave-minishop-frontend`
- `docker.io/3252a8/remnawave-minishop-backend`
- `docker.io/3252a8/remnawave-minishop-worker`
- `docker.io/3252a8/remnawave-minishop-frontend`
- More detailed analytics for admin.
- Support for different payment methods.
- Advanced promo code types (e.g., percentage discounts).
Сборка и публикация сразу в GHCR и Docker Hub:
## Donations (pls)
- Russian and international cards [LINK](https://t.me/tribute/app?startapp=dqdg)
```bash
docker login ghcr.io
docker login docker.io
IMAGE_TAG=v3.4.3 bash scripts/docker-build-push-images.sh
```
## Поддержать проект
- Crypto: `USDT/Other ERC-20 0xeD506D44aae634fEc0E01C8835744fBedb7B2a44 (Ethereum/Polygon/Gnosis)`
+12
View File
@@ -0,0 +1,12 @@
import logging
import os
import sys
def configure_logging() -> None:
level = getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO)
logging.basicConfig(
level=level,
stream=sys.stdout,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,50 @@
from typing import Dict
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from sqlalchemy.orm import sessionmaker
try:
from aiogram.fsm.storage.redis import RedisStorage
except ModuleNotFoundError: # pragma: no cover - dependency is installed in Docker image
RedisStorage = None # type: ignore[assignment]
from bot.middlewares.action_logger_middleware import ActionLoggerMiddleware
from bot.middlewares.ban_check_middleware import BanCheckMiddleware
from bot.middlewares.channel_subscription import ChannelSubscriptionMiddleware
from bot.middlewares.db_session import DBSessionMiddleware
from bot.middlewares.i18n import I18nMiddleware, get_i18n_instance
from bot.middlewares.profile_sync import ProfileSyncMiddleware
from config.settings import Settings
def build_dispatcher(
settings: Settings, async_session_factory: sessionmaker
) -> tuple[Dispatcher, Bot, Dict]:
storage = (
RedisStorage.from_url(settings.REDIS_URL)
if settings.REDIS_URL and RedisStorage is not None
else MemoryStorage()
)
default_props = DefaultBotProperties(parse_mode=ParseMode.HTML)
bot = Bot(token=settings.BOT_TOKEN, default=default_props)
dp = Dispatcher(storage=storage, settings=settings, bot_instance=bot)
i18n_instance = get_i18n_instance(path="locales", default=settings.DEFAULT_LANGUAGE)
dp["i18n_instance"] = i18n_instance
dp["async_session_factory"] = async_session_factory
dp.update.outer_middleware(DBSessionMiddleware(async_session_factory))
dp.update.outer_middleware(I18nMiddleware(i18n=i18n_instance, settings=settings))
dp.update.outer_middleware(ProfileSyncMiddleware())
dp.update.outer_middleware(BanCheckMiddleware(settings=settings, i18n_instance=i18n_instance))
dp.update.outer_middleware(
ChannelSubscriptionMiddleware(settings=settings, i18n_instance=i18n_instance)
)
dp.update.outer_middleware(ActionLoggerMiddleware(settings=settings))
return dp, bot, {"i18n_instance": i18n_instance}
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,88 @@
from aiogram import Bot
from sqlalchemy.orm import sessionmaker
from bot.middlewares.i18n import JsonI18n
from bot.payment_providers import (
ServiceFactoryContext,
build_provider_configs,
build_provider_services,
)
from bot.services.email_auth_service import EmailAuthService
from bot.services.lknpd_service import LknpdService
from bot.services.notification_service import NotificationService
from bot.services.panel_api_service import PanelApiService
from bot.services.panel_webhook_service import PanelWebhookService
from bot.services.promo_code_service import PromoCodeService
from bot.services.referral_service import ReferralService
from bot.services.subscription_service import SubscriptionService
from bot.services.support_service import SupportService
from config.settings import Settings
def build_core_services(
settings: Settings,
bot: Bot,
async_session_factory: sessionmaker,
i18n: JsonI18n,
bot_username_for_default_return: str,
):
panel_service = PanelApiService(settings)
subscription_service = SubscriptionService(settings, panel_service, bot, i18n)
referral_service = ReferralService(settings, subscription_service, bot, i18n)
promo_code_service = PromoCodeService(settings, subscription_service, bot, i18n)
email_auth_service = EmailAuthService(settings, i18n)
notification_service = NotificationService(
bot,
settings,
i18n,
session_factory=async_session_factory,
email_auth_service=email_auth_service,
bot_username=bot_username_for_default_return,
)
support_service = SupportService(
async_session_factory,
settings,
bot,
i18n,
notification_service,
email_auth_service,
)
panel_webhook_service = PanelWebhookService(
bot, settings, i18n, async_session_factory, panel_service
)
provider_configs = build_provider_configs()
payment_services = build_provider_services(
ServiceFactoryContext(
settings=settings,
bot=bot,
async_session_factory=async_session_factory,
i18n=i18n,
bot_username_for_default_return=bot_username_for_default_return,
subscription_service=subscription_service,
referral_service=referral_service,
provider_configs=provider_configs,
)
)
lknpd_service = LknpdService(
settings.LKNPD_INN,
settings.LKNPD_PASSWORD,
api_url=settings.LKNPD_API_URL,
)
# These attachments are critical for auto-renew and panel pre-expiry hooks.
subscription_service.yookassa_service = payment_services.get("yookassa_service")
panel_webhook_service.subscription_service = subscription_service
services = {
"panel_service": panel_service,
"subscription_service": subscription_service,
"referral_service": referral_service,
"promo_code_service": promo_code_service,
"notification_service": notification_service,
"email_auth_service": email_auth_service,
"support_service": support_service,
"panel_webhook_service": panel_webhook_service,
"lknpd_service": lknpd_service,
}
services.update(payment_services)
return services
+1
View File
@@ -0,0 +1 @@
+64
View File
@@ -0,0 +1,64 @@
"""Compatibility facade for the admin Mini App API."""
# ruff: noqa: I001
from bot.app.web.admin_api_impl import (
_runtime as _runtime,
ads as _ads,
auth as _auth,
backups as _backups,
broadcast as _broadcast,
common as _common,
logs as _logs,
panel as _panel,
payments as _payments,
promos as _promos,
routes as _routes,
settings as _settings,
stats as _stats,
support as _support,
sync as _sync,
tariffs as _tariffs,
themes as _themes,
translations as _translations,
users as _users,
)
_MODULES = (
_runtime,
_auth,
_common,
_stats,
_users,
_payments,
_promos,
_logs,
_support,
_broadcast,
_sync,
_ads,
_backups,
_settings,
_tariffs,
_themes,
_translations,
_panel,
_routes,
)
_NAMESPACE = {}
for _module in _MODULES:
_NAMESPACE.update(
{
_name: _value
for _name, _value in vars(_module).items()
if not _name.startswith("__") and _name != "annotations"
}
)
for _module in _MODULES:
vars(_module).update(_NAMESPACE)
globals().update(_NAMESPACE)
__all__ = sorted(_name for _name in _NAMESPACE if not _name.startswith("__"))
@@ -0,0 +1 @@
"""Domain modules for the admin Mini App API."""
@@ -0,0 +1,67 @@
# ruff: noqa: F401,F403,F405,I001
"""HTTP API powering the admin section of the subscription Mini App.
All routes require an authenticated webapp session (cookie or Bearer
token) AND the resolved Telegram user id must appear in
``settings.ADMIN_IDS``. Authorization is enforced via the
``_require_admin_user_id`` helper, never trusted from the client.
"""
from __future__ import annotations
import csv
import io
import json
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import parse_qsl, urlsplit, urlunsplit
from aiohttp import web
from pydantic import ValidationError
from sqlalchemy import Float, and_, case, cast, or_, select
from sqlalchemy import func as sa_func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import sessionmaker
from bot.app.web.admin_settings_manifest import (
manifest_payload,
)
from bot.infra.webhook_queue import enqueue_webhook_event
from bot.services.referral_service import ReferralService
from bot.services.settings_override_service import (
current_value,
update_overrides,
)
from bot.utils import MessageContent, send_message_via_queue
from bot.utils.message_queue import get_queue_manager
from config.settings import Settings
from config.tariffs_config import TariffsConfig
from db.dal import (
ad_dal,
app_settings_dal,
locale_overrides_dal,
message_log_dal,
panel_sync_dal,
payment_dal,
promo_code_dal,
subscription_dal,
user_dal,
)
from db.models import (
AdCampaign,
MessageLog,
Payment,
PromoCode,
Subscription,
User,
UserTelegramAvatar,
)
logger = logging.getLogger(__name__)
# ─── Auth ──────────────────────────────────────────────────────────
__all__ = [name for name in globals() if not name.startswith("__")]
+69
View File
@@ -0,0 +1,69 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
async def admin_ads_list_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
campaigns = await ad_dal.list_campaigns(session)
totals = await ad_dal.get_totals(session)
results = []
for campaign in campaigns:
try:
stats = await ad_dal.get_campaign_stats(session, campaign.ad_campaign_id)
except Exception:
stats = {}
results.append(_serialize_ad(campaign, stats))
return _ok({"campaigns": results, "totals": totals})
async def admin_ad_create_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
payload = await _read_json(request)
source = str(payload.get("source") or "").strip()
start_param = str(payload.get("start_param") or "").strip()
cost = float(payload.get("cost") or 0.0)
if not source or not start_param:
return _error(400, "invalid_payload")
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
existing = await ad_dal.get_campaign_by_start_param(session, start_param)
if existing:
return _error(409, "duplicate_start_param")
campaign = await ad_dal.create_campaign(
session,
source=source,
start_param=start_param,
cost=cost,
)
await session.commit()
await session.refresh(campaign)
return _ok({"campaign": _serialize_ad(campaign)})
async def admin_ad_toggle_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
campaign_id = int(request.match_info["campaign_id"])
payload = await _read_json(request)
is_active = bool(payload.get("is_active", True))
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
ok = await ad_dal.toggle_campaign_active(session, campaign_id, is_active)
if not ok:
return _error(404, "not_found")
await session.commit()
return _ok({})
async def admin_ad_delete_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
campaign_id = int(request.match_info["campaign_id"])
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
ok = await ad_dal.delete_campaign(session, campaign_id)
if not ok:
return _error(404, "not_found")
await session.commit()
return _ok({})
@@ -0,0 +1,58 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
def _require_admin_user_id(request: web.Request) -> int:
"""Return the authenticated user id, or raise 401/403 for non-admins."""
from bot.app.web.session import extract_authenticated_user_id
settings: Settings = request.app["settings"]
user_id = extract_authenticated_user_id(request)
if not user_id:
raise web.HTTPUnauthorized(
text=json.dumps({"ok": False, "error": "unauthorized"}),
content_type="application/json",
)
admin_ids = settings.ADMIN_IDS or []
db_user_telegram_id = request.get("admin_telegram_id")
if db_user_telegram_id is None:
raise web.HTTPForbidden(
text=json.dumps({"ok": False, "error": "forbidden"}),
content_type="application/json",
)
if int(db_user_telegram_id) not in {int(x) for x in admin_ids}:
raise web.HTTPForbidden(
text=json.dumps({"ok": False, "error": "forbidden"}),
content_type="application/json",
)
return int(user_id)
@web.middleware
async def admin_auth_middleware(request: web.Request, handler):
"""Resolve the Telegram id of the current user and stash it on the request.
Doing this once per request lets every admin route call
``_require_admin_user_id`` without re-querying the DB.
"""
if not request.path.startswith("/api/admin"):
return await handler(request)
from bot.app.web.session import extract_authenticated_user_id
user_id = extract_authenticated_user_id(request)
if user_id:
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
db_user = await user_dal.get_user_by_id(session, user_id)
if db_user and db_user.telegram_id:
request["admin_telegram_id"] = int(db_user.telegram_id)
elif db_user:
# No telegram_id yet (email-only user) — can't be an admin
request["admin_telegram_id"] = None
return await handler(request)
@@ -0,0 +1,182 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
import secrets
import subprocess
from bot.infra.redis import redis_lock
from bot.services.backup_restore_service import (
BACKUP_UPLOAD_MAX_BYTES,
BackupArchiveError,
BackupArchiveInfo,
BackupRestoreError,
BackupRestoreService,
)
from bot.services.backup_worker import BackupWorker
def _backup_archive_payload(archive) -> Dict[str, Any]:
return archive.to_payload()
async def _read_uploaded_backup_file(request: web.Request) -> BackupArchiveInfo:
settings: Settings = request.app["settings"]
service = BackupRestoreService(settings)
backup_dir = service.backup_dir()
temp_path: Optional[Path] = None
reader = await request.multipart()
try:
async for part in reader:
if part.name != "file":
continue
original_filename = part.filename or "backup.zip"
temp_path = backup_dir / f".upload-{secrets.token_urlsafe(12)}.zip.tmp"
size = 0
with temp_path.open("wb") as handle:
while True:
chunk = await part.read_chunk(size=1024 * 1024)
if not chunk:
break
size += len(chunk)
if size > BACKUP_UPLOAD_MAX_BYTES:
raise BackupArchiveError("Backup archive is too large")
handle.write(chunk)
if size <= 0:
raise BackupArchiveError("Uploaded archive is empty")
archive = service.import_uploaded_archive(temp_path, original_filename)
temp_path = None
return archive
finally:
if temp_path is not None and temp_path.exists():
try:
temp_path.unlink()
except OSError:
logger.warning("Failed to remove temporary backup upload %s", temp_path)
raise BackupArchiveError("file field is required")
async def admin_backups_list_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
try:
service = BackupRestoreService(settings)
archives = service.list_archives()
except OSError as exc:
logger.exception("Failed to list backup archives")
return _error(500, "backup_list_failed", str(exc))
return _ok(
{
"backup_dir": str(service.backup_dir()),
"archives": [_backup_archive_payload(archive) for archive in archives],
}
)
async def admin_backups_upload_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
content_type = (request.headers.get("Content-Type") or "").lower()
if not content_type.startswith("multipart/form-data"):
return _error(400, "invalid_backup_archive", "multipart file upload is required")
try:
archive = await _read_uploaded_backup_file(request)
except BackupArchiveError as exc:
return _error(400, "invalid_backup_archive", str(exc))
except OSError as exc:
logger.exception("Failed to save uploaded backup archive")
return _error(500, "backup_upload_failed", str(exc))
return _ok({"archive": _backup_archive_payload(archive)})
async def admin_backups_create_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
bot = request.app["bot"]
session_factory = request.app.get("async_session_factory")
worker = BackupWorker(settings, bot, session_factory=session_factory)
ttl_seconds = max(
60,
int(
max(
getattr(settings, "BACKUP_LOCK_TTL_SECONDS", 7200) or 7200,
getattr(settings, "BACKUP_PG_DUMP_TIMEOUT_SECONDS", 1800) or 1800,
)
),
)
try:
async with redis_lock(settings, "backup-worker", ttl_seconds=ttl_seconds) as acquired:
if not acquired:
return _error(409, "backup_create_busy", "Backup or restore is already running")
await worker.refresh_settings()
result = await worker.create_and_send_backup(backup_type="manual")
archive = BackupRestoreService(settings).inspect_archive(result.archive_path)
except BackupArchiveError as exc:
return _error(400, "invalid_backup_archive", str(exc))
except (OSError, RuntimeError, subprocess.SubprocessError, TimeoutError) as exc:
logger.exception("Manual backup creation failed")
return _error(500, "backup_create_failed", str(exc))
except Exception as exc:
logger.exception("Manual backup creation failed")
return _error(500, "backup_create_failed", str(exc))
return _ok(
{
"result": result.to_payload(),
"archive": _backup_archive_payload(archive),
}
)
async def admin_backups_restore_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
payload = await _read_json(request)
archive_name = str(payload.get("archive_name") or "").strip()
restore_database = bool(payload.get("restore_database"))
restore_compose = bool(payload.get("restore_compose"))
confirm = bool(payload.get("confirm"))
if not confirm:
return _error(400, "restore_confirmation_required")
service = BackupRestoreService(settings)
ttl_seconds = max(
60,
int(
max(
getattr(settings, "BACKUP_LOCK_TTL_SECONDS", 7200) or 7200,
getattr(settings, "BACKUP_PG_RESTORE_TIMEOUT_SECONDS", 1800) or 1800,
)
),
)
try:
async with redis_lock(settings, "backup-worker", ttl_seconds=ttl_seconds) as acquired:
if not acquired:
return _error(409, "backup_restore_busy", "Backup or restore is already running")
result = await service.restore_archive(
archive_name,
restore_database=restore_database,
restore_compose=restore_compose,
)
except BackupArchiveError as exc:
return _error(400, "invalid_backup_archive", str(exc))
except BackupRestoreError as exc:
logger.exception("Backup restore failed")
return _error(500, "backup_restore_failed", str(exc))
except (OSError, subprocess.SubprocessError, TimeoutError) as exc:
logger.exception("Backup restore failed")
return _error(500, "backup_restore_failed", str(exc))
if result.database_restored:
try:
from db import database_setup
if database_setup.async_engine is not None:
await database_setup.async_engine.dispose()
except Exception:
logger.exception("Failed to dispose DB engine after backup restore")
return _ok({"result": result.to_payload()})
@@ -0,0 +1,54 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
async def admin_broadcast_route(request: web.Request) -> web.Response:
actor_id = _require_admin_user_id(request)
payload = await _read_json(request)
text = str(payload.get("text") or "").strip()
target = str(payload.get("target") or "all").strip().lower()
if not text:
return _error(400, "empty_text")
if target not in {"all", "active", "inactive"}:
target = "all"
queue_manager = get_queue_manager()
if not queue_manager:
return _error(503, "queue_unavailable")
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
if target == "active":
user_ids = await user_dal.get_user_ids_with_active_subscription(session)
elif target == "inactive":
user_ids = await user_dal.get_user_ids_without_active_subscription(session)
else:
user_ids = await user_dal.get_all_active_user_ids_for_broadcast(session)
sent = 0
failed = 0
for uid in user_ids:
try:
await send_message_via_queue(
queue_manager,
int(uid),
MessageContent(content_type="text", text=text),
parse_mode="HTML",
disable_web_page_preview=True,
)
sent += 1
except Exception as exc:
failed += 1
logger.debug("Broadcast queue failed for %s: %s", uid, exc)
await message_log_dal.create_message_log(
session,
{
"user_id": actor_id,
"event_type": "admin_broadcast_webapp",
"content": f"target={target} sent={sent} failed={failed} text={text[:120]}",
"is_admin_event": True,
},
)
return _ok({"queued": sent, "failed": failed, "target": target})
@@ -0,0 +1,409 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
def _ok(payload: Dict[str, Any], **extra) -> web.Response:
body = {"ok": True, **payload, **extra}
return web.json_response(body)
def _error(status: int, code: str, message: str = "") -> web.Response:
return web.json_response(
{"ok": False, "error": code, "message": message or code},
status=status,
)
async def _read_json(request: web.Request) -> Dict[str, Any]:
try:
data = await request.json()
return data if isinstance(data, dict) else {}
except Exception:
return {}
def _serialize_user(user: User) -> Dict[str, Any]:
return {
"user_id": int(user.user_id),
"telegram_id": int(user.telegram_id) if user.telegram_id else None,
"telegram_photo_url": user.telegram_photo_url,
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
"language_code": user.language_code,
"is_banned": bool(user.is_banned),
"registration_date": user.registration_date.isoformat() if user.registration_date else None,
"panel_user_uuid": user.panel_user_uuid,
"referral_code": user.referral_code,
"referred_by_id": int(user.referred_by_id) if user.referred_by_id else None,
}
def _premium_limit_bytes_from_subscription(sub: Subscription) -> int:
premium_bonus_bytes = int(getattr(sub, "premium_bonus_bytes", 0) or 0)
return (
int(sub.premium_baseline_bytes or 0)
+ int(sub.premium_topup_balance_bytes or 0)
+ int(getattr(sub, "premium_topup_used_bytes", 0) or 0)
+ premium_bonus_bytes
)
def _premium_traffic_list_payload(sub: Optional[Subscription]) -> Dict[str, Any]:
"""Premium traffic column when subscription has a finite premium quota (bytes > 0).
Note: ``Subscription.premium_is_limited`` in the DB means *quota exhausted* for panel
routing, not 'tariff includes premium traffic' — do not use it here.
"""
if sub is None:
return {"state": "none"}
if bool(getattr(sub, "premium_unlimited_override", False)):
return {
"state": "unlimited",
"unlimited": True,
"used_bytes": int(sub.premium_used_bytes or 0),
"limit_bytes": None,
"percent": None,
}
limit_bytes = _premium_limit_bytes_from_subscription(sub)
if limit_bytes <= 0:
return {"state": "none"}
used_bytes = int(sub.premium_used_bytes or 0)
ratio = float(used_bytes) / float(limit_bytes) if limit_bytes else 0.0
pct = int(max(0, min(100, round(ratio * 100))))
if ratio >= 1.0:
state = "critical"
elif ratio >= 0.85:
state = "warn"
else:
state = "good"
return {
"state": state,
"unlimited": False,
"used_bytes": used_bytes,
"limit_bytes": limit_bytes,
"percent": pct,
}
def _serialize_subscription(sub: Subscription) -> Dict[str, Any]:
premium_bonus_bytes = int(getattr(sub, "premium_bonus_bytes", 0) or 0)
regular_bonus_bytes = int(getattr(sub, "regular_bonus_bytes", 0) or 0)
regular_unlimited_override = bool(getattr(sub, "regular_unlimited_override", False))
premium_unlimited_override = bool(getattr(sub, "premium_unlimited_override", False))
premium_limit_bytes = _premium_limit_bytes_from_subscription(sub)
return {
"subscription_id": int(sub.subscription_id),
"panel_user_uuid": sub.panel_user_uuid,
"panel_subscription_uuid": sub.panel_subscription_uuid,
"start_date": sub.start_date.isoformat() if sub.start_date else None,
"end_date": sub.end_date.isoformat() if sub.end_date else None,
"duration_months": sub.duration_months,
"is_active": bool(sub.is_active),
"status_from_panel": sub.status_from_panel,
"traffic_limit_bytes": sub.traffic_limit_bytes,
"traffic_used_bytes": sub.traffic_used_bytes,
"tier_baseline_bytes": sub.tier_baseline_bytes,
"topup_balance_bytes": sub.topup_balance_bytes,
"premium_used_bytes": sub.premium_used_bytes,
"premium_limit_bytes": premium_limit_bytes,
"premium_baseline_bytes": sub.premium_baseline_bytes,
"premium_topup_balance_bytes": sub.premium_topup_balance_bytes,
"premium_topup_used_bytes": getattr(sub, "premium_topup_used_bytes", 0),
"premium_bonus_bytes": premium_bonus_bytes,
"regular_bonus_bytes": regular_bonus_bytes,
"regular_unlimited_override": regular_unlimited_override,
"premium_unlimited_override": premium_unlimited_override,
"premium_is_limited": bool(sub.premium_is_limited),
"tariff_key": sub.tariff_key,
"auto_renew_enabled": bool(sub.auto_renew_enabled),
"provider": sub.provider,
"is_throttled": bool(sub.is_throttled),
}
def _payment_traffic_gb_split(payment: Payment) -> Tuple[Optional[float], Optional[float]]:
"""For traffic purchases: ``(regular_gb, premium_gb)``. Other payments → (None, None)."""
if payment.purchased_gb is None:
return None, None
try:
gb = float(payment.purchased_gb)
except (TypeError, ValueError):
return None, None
sm = (payment.sale_mode or "").strip()
if not sm:
return None, None
base = sm.split("@", 1)[0].split("|", 1)[0].lower()
if base == "premium_topup":
return None, gb
if base in {"traffic", "traffic_package", "topup"}:
return gb, None
return None, None
def _user_display_label(
loaded_user: Any,
fallback_user_id: Optional[int],
*,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
username: Optional[str] = None,
email: Optional[str] = None,
) -> Optional[str]:
"""Human-facing name: TG profile name, else email, else user id."""
tid = getattr(loaded_user, "telegram_id", None)
if loaded_user is not None and tid is not None:
fn = (getattr(loaded_user, "first_name", None) or "").strip()
ln = (getattr(loaded_user, "last_name", None) or "").strip()
full = f"{fn} {ln}".strip()
if full:
return full
un = (getattr(loaded_user, "username", None) or "").strip()
if un:
return un if un.startswith("@") else f"@{un}"
elif loaded_user is not None:
email = (getattr(loaded_user, "email", None) or "").strip()
if email:
return email
fn = (first_name or "").strip()
ln = (last_name or "").strip()
full = f"{fn} {ln}".strip()
if full:
return full
un = (username or "").strip()
if un:
return un if un.startswith("@") else f"@{un}"
email_value = (email or "").strip()
if email_value:
return email_value
if fallback_user_id is None:
return None
return str(fallback_user_id)
def _payment_user_display_label(loaded_user: Any, payment_user_id: int) -> str:
label = _user_display_label(loaded_user, payment_user_id)
if label:
return label
return str(payment_user_id)
def _serialize_payment(payment: Payment) -> Dict[str, Any]:
# Avoid lazy-loading `payment.user` outside an active SQLAlchemy session.
# Some admin routes serialize payments after the session scope is closed.
telegram_id = None
loaded_user = payment.__dict__.get("user")
user_label = _payment_user_display_label(loaded_user, int(payment.user_id))
if loaded_user is not None:
tid = getattr(loaded_user, "telegram_id", None)
if tid is not None:
try:
telegram_id = int(tid)
except (TypeError, ValueError):
telegram_id = None
reg_gb, prem_gb = _payment_traffic_gb_split(payment)
return {
"payment_id": int(payment.payment_id),
"user_id": int(payment.user_id),
"user_label": user_label,
"telegram_id": telegram_id,
"traffic_regular_gb": reg_gb,
"traffic_premium_gb": prem_gb,
"provider": payment.provider,
"provider_payment_id": payment.provider_payment_id,
"amount": float(payment.amount),
"currency": payment.currency,
"status": payment.status,
"description": payment.description,
"subscription_duration_months": payment.subscription_duration_months,
"sale_mode": payment.sale_mode,
"tariff_key": payment.tariff_key,
"purchased_gb": payment.purchased_gb,
"purchased_hwid_devices": payment.purchased_hwid_devices,
"created_at": payment.created_at.isoformat() if payment.created_at else None,
}
def _serialize_promo(promo: PromoCode) -> Dict[str, Any]:
return {
"id": int(promo.promo_code_id),
"code": promo.code,
"bonus_days": int(promo.bonus_days),
"max_activations": int(promo.max_activations),
"current_activations": int(promo.current_activations or 0),
"is_active": bool(promo.is_active),
"valid_until": promo.valid_until.isoformat() if promo.valid_until else None,
"created_at": promo.created_at.isoformat() if promo.created_at else None,
"created_by_admin_id": int(promo.created_by_admin_id)
if promo.created_by_admin_id
else None,
}
def _serialize_ad(campaign: AdCampaign, totals: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return {
"id": int(campaign.ad_campaign_id),
"source": campaign.source,
"start_param": campaign.start_param,
"cost": float(campaign.cost or 0),
"is_active": bool(campaign.is_active),
"created_at": campaign.created_at.isoformat() if campaign.created_at else None,
"stats": totals or {},
}
def _serialize_log(entry: MessageLog) -> Dict[str, Any]:
author_user = entry.__dict__.get("author_user")
target_user = entry.__dict__.get("target_user")
user_id = int(entry.user_id) if entry.user_id is not None else None
target_user_id = int(entry.target_user_id) if entry.target_user_id is not None else None
return {
"log_id": int(entry.log_id),
"user_id": user_id,
"user_label": _user_display_label(
author_user,
user_id,
first_name=entry.telegram_first_name,
username=entry.telegram_username,
),
"telegram_username": entry.telegram_username,
"telegram_first_name": entry.telegram_first_name,
"email": getattr(author_user, "email", None),
"event_type": entry.event_type,
"content": entry.content,
"is_admin_event": bool(entry.is_admin_event),
"target_user_id": target_user_id,
"target_user_label": _user_display_label(target_user, target_user_id),
"timestamp": entry.timestamp.isoformat() if entry.timestamp else None,
}
def _tariffs_config_path(settings: Settings) -> Path:
return Path(settings.TARIFFS_CONFIG_PATH).expanduser()
def _tariffs_config_payload(config: TariffsConfig) -> Dict[str, Any]:
return config.model_dump(mode="json", exclude_none=True)
def _write_tariffs_config_file(path: Path, config: TariffsConfig) -> None:
data = _tariffs_config_payload(config)
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_suffix(f"{path.suffix}.tmp")
payload = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
try:
tmp_path.write_text(payload, encoding="utf-8")
tmp_path.replace(path)
except PermissionError:
# A docker-compose single-file bind mount can make /app/config
# unwritable while the mounted tariffs.json itself is writable.
# Fall back to updating the existing file in-place.
if tmp_path.exists():
try:
tmp_path.unlink()
except OSError:
pass
path.write_text(payload, encoding="utf-8")
def _webapp_themes_catalog_payload(config: Any) -> Dict[str, Any]:
return config.model_dump(mode="json", exclude_none=True)
def _panel_node_uuid_key(node: Dict[str, Any]) -> str:
uid = node.get("nodeUuid") or node.get("node_uuid") or node.get("uuid") or node.get("id")
return str(uid).strip().lower() if uid else ""
def _panel_node_users_online(node: Dict[str, Any]) -> Optional[int]:
uo = node.get("usersOnline")
if uo is None:
uo = node.get("users_online")
if uo is None:
uo = node.get("onlineUsers") or node.get("online_users")
if uo is None:
mg = node.get("metricGroups")
if isinstance(mg, dict):
uo = mg.get("onlineUsers") or mg.get("online_users")
if uo is None:
return None
try:
return int(uo)
except (TypeError, ValueError):
return None
def _panel_nodes_online_by_uuid(nodes_payload: Any) -> Dict[str, int]:
"""Build node_uuid(lower) -> usersOnline from GET /system/stats/nodes payload."""
out: Dict[str, int] = {}
raw_list: Optional[List[Any]] = None
if isinstance(nodes_payload, list):
raw_list = nodes_payload
elif isinstance(nodes_payload, dict):
raw_list = nodes_payload.get("nodes")
if raw_list is None:
raw_list = nodes_payload.get("items") or nodes_payload.get("data")
if not isinstance(raw_list, list):
return out
for n in raw_list:
if not isinstance(n, dict):
continue
key = _panel_node_uuid_key(n)
if not key:
continue
online = _panel_node_users_online(n)
if online is not None:
out[key] = online
return out
def _enrich_bandwidth_nodes_with_online(
bw: Any,
online_by_uuid: Dict[str, int],
online_by_name: Optional[Dict[str, int]] = None,
) -> None:
"""Attach usersOnline to topNodes/series (UUID and optional node name)."""
if not isinstance(bw, dict):
return
if not online_by_uuid and not online_by_name:
return
for key in ("topNodes", "series"):
arr = bw.get(key)
if not isinstance(arr, list):
continue
for item in arr:
if not isinstance(item, dict):
continue
if item.get("usersOnline") is not None:
continue
uid = item.get("uuid") or item.get("nodeUuid") or item.get("node_uuid")
if uid and online_by_uuid:
hit = online_by_uuid.get(str(uid).strip().lower())
if hit is not None:
item["usersOnline"] = hit
continue
if online_by_name:
nm = item.get("name")
if nm and isinstance(nm, str):
hitn = online_by_name.get(nm.strip().lower())
if hitn is not None:
item["usersOnline"] = hitn
def _build_admin_webapp_referral_link(
base_url: Optional[str], referral_code: Optional[str]
) -> Optional[str]:
"""Mirror of ``subscription_webapp._build_webapp_referral_link``.
Kept local to avoid a cross-module import cycle (subscription_webapp
imports admin_api).
"""
if not base_url or not referral_code:
return None
parts = urlsplit(base_url)
query = dict(parse_qsl(parts.query, keep_blank_values=True))
query["ref"] = f"u{referral_code}"
new_query = "&".join(f"{k}={v}" for k, v in query.items())
return urlunsplit((parts.scheme, parts.netloc, parts.path, new_query, parts.fragment))
@@ -0,0 +1,36 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
async def admin_logs_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
async_session_factory: sessionmaker = request.app["async_session_factory"]
page = max(0, int(request.query.get("page", 0) or 0))
page_size = min(200, max(1, int(request.query.get("page_size", 50) or 50)))
user_filter = request.query.get("user_id")
async with async_session_factory() as session:
if user_filter:
try:
user_id = int(user_filter)
except (TypeError, ValueError):
return _error(400, "invalid_user_id")
entries = await message_log_dal.get_user_message_logs(
session, user_id, page_size, page * page_size
)
total = await message_log_dal.count_user_message_logs(session, user_id)
else:
entries = await message_log_dal.get_all_message_logs(
session, page_size, page * page_size
)
total = await message_log_dal.count_all_message_logs(session)
return _ok(
{
"logs": [_serialize_log(entry) for entry in entries],
"page": page,
"page_size": page_size,
"total": int(total or 0),
}
)
@@ -0,0 +1,35 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
async def admin_panel_internal_squads_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
panel_service = request.app.get("panel_service")
if panel_service is None:
return _error(503, "panel_unavailable", "Panel service unavailable")
try:
squads = await panel_service.get_internal_squads()
except Exception as exc:
logger.exception("Failed to load internal squads from panel")
return _error(502, "panel_request_failed", str(exc))
if squads is None:
return _error(502, "panel_request_failed", "Unable to load internal squads")
items = []
for squad in squads:
if not isinstance(squad, dict):
continue
uuid = squad.get("uuid") or squad.get("id")
if not uuid:
continue
items.append(
{
"uuid": str(uuid),
"name": squad.get("name") or squad.get("title") or str(uuid),
"members_count": squad.get("membersCount")
or squad.get("usersCount")
or squad.get("members_count"),
"active_inbounds_count": squad.get("activeInboundsCount")
or squad.get("active_inbounds_count"),
}
)
return _ok({"squads": items})
@@ -0,0 +1,124 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
async def admin_payments_list_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
async_session_factory: sessionmaker = request.app["async_session_factory"]
page = max(0, int(request.query.get("page", 0) or 0))
page_size = min(100, max(1, int(request.query.get("page_size", 25) or 25)))
async with async_session_factory() as session:
from sqlalchemy.orm import selectinload
stmt = (
select(Payment)
.options(selectinload(Payment.user))
.order_by(Payment.created_at.desc())
.offset(page * page_size)
.limit(page_size)
)
rows = (await session.execute(stmt)).scalars().all()
total = await payment_dal.get_payments_count(session)
return _ok(
{
"payments": [_serialize_payment(p) for p in rows],
"page": page,
"page_size": page_size,
"total": int(total or 0),
}
)
async def admin_payment_detail_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
async_session_factory: sessionmaker = request.app["async_session_factory"]
try:
payment_id = int(request.match_info["payment_id"])
except (TypeError, ValueError):
return _error(400, "invalid_payment", "Invalid payment id")
async with async_session_factory() as session:
payment = await payment_dal.get_payment_by_db_id(session, payment_id)
if not payment:
return _error(404, "not_found", "Payment not found")
payload = _serialize_payment(payment)
payload.update(
{
"yookassa_payment_id": payment.yookassa_payment_id,
"idempotence_key": payment.idempotence_key,
"promo_code": (
payment.promo_code_used.code if payment.promo_code_used is not None else None
),
"updated_at": payment.updated_at.isoformat() if payment.updated_at else None,
}
)
return _ok({"payment": payload})
async def admin_payments_export_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
from sqlalchemy.orm import selectinload
stmt = (
select(Payment)
.options(selectinload(Payment.user))
.order_by(Payment.created_at.desc())
.limit(10000)
)
rows = (await session.execute(stmt)).scalars().all()
buffer = io.StringIO()
writer = csv.writer(buffer)
writer.writerow(
[
"payment_id",
"user_id",
"user_label",
"provider",
"provider_payment_id",
"amount",
"currency",
"status",
"description",
"duration_months",
"sale_mode",
"tariff_key",
"created_at",
]
)
for p in rows:
label = _payment_user_display_label(p.user, int(p.user_id)) if p.user else str(p.user_id)
writer.writerow(
[
p.payment_id,
p.user_id,
label,
p.provider,
p.provider_payment_id or "",
p.amount,
p.currency,
p.status,
p.description or "",
p.subscription_duration_months or "",
p.sale_mode or "",
p.tariff_key or "",
p.created_at.isoformat() if p.created_at else "",
]
)
response = web.Response(
body=buffer.getvalue().encode("utf-8-sig"),
content_type="text/csv",
charset="utf-8",
)
response.headers["Content-Disposition"] = 'attachment; filename="payments.csv"'
return response
@@ -0,0 +1,96 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
async def admin_promos_list_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
async_session_factory: sessionmaker = request.app["async_session_factory"]
page = max(0, int(request.query.get("page", 0) or 0))
page_size = min(100, max(1, int(request.query.get("page_size", 25) or 25)))
async with async_session_factory() as session:
promos = await promo_code_dal.get_all_promo_codes_with_details(
session, limit=page_size, offset=page * page_size
)
total = await promo_code_dal.get_promo_codes_count(session)
return _ok(
{
"promos": [_serialize_promo(p) for p in promos],
"page": page,
"page_size": page_size,
"total": int(total or 0),
}
)
async def admin_promo_create_route(request: web.Request) -> web.Response:
actor_id = _require_admin_user_id(request)
payload = await _read_json(request)
code = str(payload.get("code") or "").strip().upper()
bonus_days = int(payload.get("bonus_days") or 0)
max_activations = int(payload.get("max_activations") or 0)
valid_days = payload.get("valid_days")
if not code or bonus_days <= 0 or max_activations <= 0:
return _error(400, "invalid_payload")
valid_until = None
if valid_days:
try:
valid_until = datetime.now(timezone.utc) + timedelta(days=int(valid_days))
except (TypeError, ValueError):
return _error(400, "invalid_valid_days")
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
existing = await promo_code_dal.get_promo_code_by_code(session, code)
if existing:
return _error(409, "duplicate_code")
promo = await promo_code_dal.create_promo_code(
session,
{
"code": code,
"bonus_days": bonus_days,
"max_activations": max_activations,
"valid_until": valid_until,
"created_by_admin_id": actor_id,
"is_active": True,
},
)
await session.commit()
return _ok({"promo": _serialize_promo(promo)})
async def admin_promo_update_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
promo_id = int(request.match_info["promo_id"])
payload = await _read_json(request)
update_data: Dict[str, Any] = {}
if "is_active" in payload:
update_data["is_active"] = bool(payload["is_active"])
if "bonus_days" in payload and payload["bonus_days"] is not None:
update_data["bonus_days"] = int(payload["bonus_days"])
if "max_activations" in payload and payload["max_activations"] is not None:
update_data["max_activations"] = int(payload["max_activations"])
if not update_data:
return _error(400, "no_changes")
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
promo = await promo_code_dal.update_promo_code(session, promo_id, update_data)
if not promo:
return _error(404, "not_found")
await session.commit()
await session.refresh(promo)
return _ok({"promo": _serialize_promo(promo)})
async def admin_promo_delete_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
promo_id = int(request.match_info["promo_id"])
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
promo = await promo_code_dal.delete_promo_code(session, promo_id)
if not promo:
return _error(404, "not_found")
await session.commit()
return _ok({})
@@ -0,0 +1,82 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
def setup_admin_routes(app: web.Application) -> None:
router = app.router
router.add_get("/api/admin/me", admin_me_route)
router.add_get("/api/admin/stats", admin_stats_route)
router.add_get("/api/admin/users", admin_users_list_route)
router.add_get("/api/admin/users/{user_id:-?\\d+}", admin_user_detail_route)
router.add_get("/api/admin/users/{user_id:-?\\d+}/avatar", admin_user_avatar_route)
router.add_post("/api/admin/users/{user_id:-?\\d+}/ban", admin_user_ban_route)
router.add_post("/api/admin/users/{user_id:-?\\d+}/message", admin_user_message_route)
router.add_post(
"/api/admin/users/{user_id:-?\\d+}/message/preview", admin_user_message_preview_route
)
router.add_post(
"/api/admin/users/{user_id:-?\\d+}/telegram-profile-link",
admin_user_telegram_profile_link_route,
)
router.add_post("/api/admin/users/{user_id:-?\\d+}/reset-trial", admin_user_reset_trial_route)
router.add_post("/api/admin/users/{user_id:-?\\d+}/extend", admin_user_extend_route)
router.add_post(
"/api/admin/users/{user_id:-?\\d+}/premium-override",
admin_user_premium_override_route,
)
router.add_post(
"/api/admin/users/{user_id:-?\\d+}/regular-traffic-override",
admin_user_regular_traffic_override_route,
)
router.add_post(
"/api/admin/users/{user_id:-?\\d+}/traffic-grant",
admin_user_traffic_grant_route,
)
router.add_delete("/api/admin/users/{user_id:-?\\d+}", admin_user_delete_route)
router.add_get("/api/admin/payments", admin_payments_list_route)
router.add_get("/api/admin/payments/{payment_id:\\d+}", admin_payment_detail_route)
router.add_get("/api/admin/payments/export.csv", admin_payments_export_route)
router.add_get("/api/admin/promos", admin_promos_list_route)
router.add_post("/api/admin/promos", admin_promo_create_route)
router.add_patch("/api/admin/promos/{promo_id:\\d+}", admin_promo_update_route)
router.add_delete("/api/admin/promos/{promo_id:\\d+}", admin_promo_delete_route)
router.add_get("/api/admin/logs", admin_logs_route)
router.add_get("/api/admin/support/tickets", admin_support_tickets_route)
router.add_get("/api/admin/support/tickets/{id:\\d+}", admin_support_ticket_detail_route)
router.add_post(
"/api/admin/support/tickets/{id:\\d+}/messages",
admin_support_ticket_reply_route,
)
router.add_patch("/api/admin/support/tickets/{id:\\d+}", admin_support_ticket_patch_route)
router.add_post("/api/admin/support/tickets/{id:\\d+}/read", admin_support_ticket_read_route)
router.add_get("/api/admin/support/stats", admin_support_stats_route)
router.add_post("/api/admin/broadcast", admin_broadcast_route)
router.add_post("/api/admin/sync", admin_sync_route)
router.add_get("/api/admin/ads", admin_ads_list_route)
router.add_post("/api/admin/ads", admin_ad_create_route)
router.add_post("/api/admin/ads/{campaign_id:\\d+}/toggle", admin_ad_toggle_route)
router.add_delete("/api/admin/ads/{campaign_id:\\d+}", admin_ad_delete_route)
router.add_get("/api/admin/settings", admin_settings_get_route)
router.add_patch("/api/admin/settings", admin_settings_patch_route)
router.add_get("/api/admin/translations", admin_translations_get_route)
router.add_patch("/api/admin/translations", admin_translations_patch_route)
router.add_get("/api/admin/tariffs", admin_tariffs_get_route)
router.add_put("/api/admin/tariffs", admin_tariffs_save_route)
router.add_get("/api/admin/themes", admin_themes_get_route)
router.add_put("/api/admin/themes", admin_themes_save_route)
router.add_post("/api/admin/appearance/logo", admin_appearance_logo_upload_route)
router.add_post("/api/admin/appearance/favicon", admin_appearance_favicon_upload_route)
router.add_get("/api/admin/backups", admin_backups_list_route)
router.add_post("/api/admin/backups/create", admin_backups_create_route)
router.add_post("/api/admin/backups/upload", admin_backups_upload_route)
router.add_post("/api/admin/backups/restore", admin_backups_restore_route)
router.add_get("/api/admin/panel/internal-squads", admin_panel_internal_squads_route)
@@ -0,0 +1,105 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
from .webapp_runtime import refresh_webapp_runtime_after_settings_change
from config.subscription_guides_config import (
SubscriptionGuidesConfigError,
subscription_guides_admin_config_json,
)
async def admin_settings_get_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
overrides = await app_settings_dal.get_overrides_with_meta(session)
overrides_by_key = {entry["key"]: entry for entry in overrides}
fields = manifest_payload()
webhook_base_url = str(settings.WEBHOOK_BASE_URL or "").strip().rstrip("/")
sections: Dict[str, Dict[str, Any]] = {}
for field in fields:
key = field["key"]
section_id = field["section"]
if section_id not in sections:
sections[section_id] = {
"id": section_id,
"order": field["section_order"],
"fields": [],
}
override = overrides_by_key.get(key)
value = current_value(settings, key)
is_secret = bool(field.get("secret"))
overridden = bool(override)
source = None
read_error = None
if key == "SUBSCRIPTION_PAGE_CONFIG_JSON":
try:
value, source = subscription_guides_admin_config_json(settings)
overridden = source == "admin_json"
except SubscriptionGuidesConfigError as exc:
read_error = str(exc)
response_field = {
**field,
"value": "" if is_secret else value,
"overridden": overridden,
"updated_at": override.get("updated_at") if override else None,
}
if source:
response_field["source"] = source
if read_error:
response_field["read_error"] = read_error
if is_secret:
response_field["has_value"] = bool(value)
webhook_path = str(response_field.get("webhook_path") or "").strip()
if webhook_path:
if not webhook_path.startswith("/"):
webhook_path = f"/{webhook_path}"
response_field["webhook_path"] = webhook_path
response_field["webhook_base_url_configured"] = bool(webhook_base_url)
if webhook_base_url:
response_field["webhook_url"] = f"{webhook_base_url}{webhook_path}"
sections[section_id]["fields"].append(response_field)
ordered_sections = sorted(sections.values(), key=lambda s: s["order"])
return _ok({"sections": ordered_sections})
async def admin_settings_patch_route(request: web.Request) -> web.Response:
actor_id = _require_admin_user_id(request)
settings: Settings = request.app["settings"]
async_session_factory: sessionmaker = request.app["async_session_factory"]
payload = await _read_json(request)
updates = payload.get("updates") or {}
deletes = payload.get("deletes") or []
if not isinstance(updates, dict):
return _error(400, "invalid_updates")
if not isinstance(deletes, list):
return _error(400, "invalid_deletes")
if (
"SUBSCRIPTION_PAGE_CONFIG_JSON" in updates
and not str(updates.get("SUBSCRIPTION_PAGE_CONFIG_JSON") or "").strip()
):
updates = dict(updates)
updates.pop("SUBSCRIPTION_PAGE_CONFIG_JSON", None)
deletes = [*deletes, "SUBSCRIPTION_PAGE_CONFIG_JSON"]
result = await update_overrides(
settings,
async_session_factory,
updates=updates,
deletes=deletes,
actor_id=actor_id,
)
if not result.get("ok"):
return web.json_response(
{"ok": False, "error": "validation_failed", "errors": result.get("errors", {})},
status=400,
)
await refresh_webapp_runtime_after_settings_change(request, updates=updates, deletes=deletes)
return _ok({"applied": result.get("applied", 0), "reverted": result.get("reverted", 0)})
+168
View File
@@ -0,0 +1,168 @@
# ruff: noqa: F401,F403,F405,I001
import asyncio
from ._runtime import * # noqa: F403,F405
from .auth import _require_admin_user_id
from .common import _ok, _serialize_payment
from bot.utils.ttl_cache import AsyncTTLCache
_ADMIN_PANEL_STATS_CACHES: Dict[tuple[int, int], AsyncTTLCache] = {}
_ADMIN_DB_STATS_CACHES: Dict[tuple[int, int], AsyncTTLCache] = {}
async def admin_me_route(request: web.Request) -> web.Response:
user_id = _require_admin_user_id(request)
settings: Settings = request.app["settings"]
return _ok({}, user_id=user_id, admin_ids=list(settings.ADMIN_IDS or []))
async def admin_stats_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
async_session_factory: sessionmaker = request.app["async_session_factory"]
payload = dict(await _load_admin_db_stats(settings, async_session_factory))
panel_service = request.app.get("panel_service")
if panel_service is not None:
payload["panel"] = await _load_admin_panel_stats(request, settings, panel_service)
queue_manager = get_queue_manager()
if queue_manager:
try:
payload["queue"] = queue_manager.get_queue_stats()
except Exception: # pragma: no cover - defensive
payload["queue"] = None
payload["currency_symbol"] = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
return _ok(payload)
async def _load_admin_db_stats(
settings: Settings,
async_session_factory: sessionmaker,
) -> Dict[str, Any]:
cache = _admin_db_stats_cache(settings)
if cache is None:
return await _load_admin_db_stats_uncached(async_session_factory)
return await cache.get_or_load(
"db",
lambda: _load_admin_db_stats_uncached(async_session_factory),
)
async def _load_admin_db_stats_uncached(async_session_factory: sessionmaker) -> Dict[str, Any]:
async with async_session_factory() as session:
user_stats = await user_dal.get_enhanced_user_statistics(session)
financial_stats = await payment_dal.get_financial_statistics(session)
sync_status = await panel_sync_dal.get_panel_sync_status(session)
recent_payments = await payment_dal.get_recent_payment_logs_with_user(session, limit=10)
return {
"users": user_stats,
"financial": financial_stats,
"panel_sync": {
"status": sync_status.status if sync_status else "never_run",
"last_sync_time": sync_status.last_sync_time.isoformat()
if sync_status and sync_status.last_sync_time
else None,
"details": sync_status.details if sync_status else None,
"users_processed": sync_status.users_processed_from_panel if sync_status else 0,
"subscriptions_synced": sync_status.subscriptions_synced if sync_status else 0,
},
"recent_payments": [_serialize_payment(p) for p in recent_payments],
}
def _admin_db_stats_cache(settings: Settings) -> Optional[AsyncTTLCache]:
ttl_seconds = int(getattr(settings, "ADMIN_DB_STATS_CACHE_TTL_SECONDS", 5) or 0)
if ttl_seconds <= 0:
return None
cache_key = (id(settings), ttl_seconds)
cache = _ADMIN_DB_STATS_CACHES.get(cache_key)
if cache is None:
cache = AsyncTTLCache(
ttl_seconds=ttl_seconds,
settings=settings,
namespace="admin:db_stats",
)
_ADMIN_DB_STATS_CACHES[cache_key] = cache
return cache
async def _load_admin_panel_stats(
request: web.Request,
settings: Settings,
panel_service,
) -> Dict[str, Any]:
cache = _admin_panel_stats_cache(settings)
if cache is None:
return await _load_admin_panel_stats_uncached(panel_service)
return await cache.get_or_load("panel", lambda: _load_admin_panel_stats_uncached(panel_service))
def _admin_panel_stats_cache(settings: Settings) -> Optional[AsyncTTLCache]:
ttl_seconds = int(getattr(settings, "ADMIN_PANEL_STATS_CACHE_TTL_SECONDS", 15) or 0)
if ttl_seconds <= 0:
return None
cache_key = (id(settings), ttl_seconds)
cache = _ADMIN_PANEL_STATS_CACHES.get(cache_key)
if cache is None:
cache = AsyncTTLCache(
ttl_seconds=ttl_seconds,
settings=settings,
namespace="admin:panel_stats",
)
_ADMIN_PANEL_STATS_CACHES[cache_key] = cache
return cache
async def _load_admin_panel_stats_uncached(panel_service) -> Dict[str, Any]:
try:
today = datetime.now(timezone.utc).date()
start_d = today - timedelta(days=7)
system, bandwidth, nodes, nodes_bw, lookups = await asyncio.gather(
_safe_panel_call(panel_service.get_system_stats(), "system stats"),
_safe_panel_call(panel_service.get_bandwidth_stats(), "bandwidth stats"),
_safe_panel_call(panel_service.get_nodes_statistics(), "nodes stats"),
_safe_panel_call(
panel_service.get_nodes_bandwidth_usage(
start=start_d.isoformat(),
end=today.isoformat(),
top_nodes_limit=64,
),
"nodes bandwidth range",
),
_safe_panel_call(panel_service.get_nodes_online_lookups(), "nodes online lookups"),
)
panel_body: Dict[str, Any] = {
"system": system or {},
"bandwidth": bandwidth or {},
"nodes": nodes or {},
"nodes_bandwidth": nodes_bw or {},
}
if isinstance(lookups, dict):
try:
online_map = _panel_nodes_online_by_uuid(panel_body.get("nodes"))
for k, v in lookups.get("byUuid", {}).items():
online_map[k] = v
_enrich_bandwidth_nodes_with_online(
panel_body.get("nodes_bandwidth"),
online_map,
lookups.get("byName") or {},
)
except Exception as exc_merge: # pragma: no cover
logger.debug("Panel nodes online merge skipped: %s", exc_merge)
return panel_body
except Exception as exc:
logger.debug("Panel stats unavailable: %s", exc)
return {"error": "unavailable"}
async def _safe_panel_call(awaitable, label: str) -> Any:
try:
return await awaitable
except Exception as exc: # pragma: no cover - optional panel endpoints
logger.debug("Panel %s unavailable: %s", label, exc)
return None
@@ -0,0 +1,251 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, constr, field_validator
from bot.services.support_service import TicketNotFound
from db.dal import support_dal, user_dal
from db.models import SupportTicket, SupportTicketMessage
class AdminTicketReplyPayload(BaseModel):
model_config = ConfigDict(extra="ignore")
body: constr(min_length=1, max_length=4000)
is_internal_note: bool = False
@field_validator("body")
@classmethod
def _strip_body(cls, value: str) -> str:
stripped = value.strip()
if not stripped:
raise ValueError("empty_text")
return stripped
class AdminTicketPatchPayload(BaseModel):
model_config = ConfigDict(extra="ignore")
status: Optional[Literal["open", "awaiting_user", "awaiting_admin", "resolved", "closed"]] = (
None
)
priority: Optional[Literal["low", "normal", "high", "urgent"]] = None
category: Optional[Literal["billing", "technical", "account", "other"]] = None
assigned_admin_id: Optional[int] = None
def _validate_model_payload(model_cls, payload: Dict[str, Any]):
try:
return model_cls.model_validate(payload), None
except ValidationError:
return None, _error(400, "invalid_request", "Invalid request")
def _support_ticket_payload(ticket: SupportTicket) -> Dict[str, Any]:
return {
"ticket_id": ticket.ticket_id,
"user_id": ticket.user_id,
"subject": ticket.subject,
"category": ticket.category,
"priority": ticket.priority,
"status": ticket.status,
"assigned_admin_id": ticket.assigned_admin_id,
"last_message_at": ticket.last_message_at.isoformat() if ticket.last_message_at else None,
"last_message_role": ticket.last_message_role,
"unread_user_count": int(ticket.unread_user_count or 0),
"unread_admin_count": int(ticket.unread_admin_count or 0),
"created_at": ticket.created_at.isoformat() if ticket.created_at else None,
"updated_at": ticket.updated_at.isoformat() if ticket.updated_at else None,
"closed_at": ticket.closed_at.isoformat() if ticket.closed_at else None,
}
def _user_display_name(user) -> Optional[str]:
if not user:
return None
name = " ".join(
part.strip() for part in [user.first_name, user.last_name] if part and part.strip()
).strip()
return name or user.username or user.email or str(user.user_id)
def _support_message_payload(
message: SupportTicketMessage,
*,
authors: Optional[Dict[int, Any]] = None,
) -> Dict[str, Any]:
author = authors.get(message.author_user_id) if authors and message.author_user_id else None
return {
"message_id": message.message_id,
"ticket_id": message.ticket_id,
"author_role": message.author_role,
"author_user_id": message.author_user_id,
"author_name": _user_display_name(author),
"body": message.body,
"is_internal_note": bool(message.is_internal_note),
"created_at": message.created_at.isoformat() if message.created_at else None,
"read_by_user_at": message.read_by_user_at.isoformat() if message.read_by_user_at else None,
"read_by_admin_at": message.read_by_admin_at.isoformat()
if message.read_by_admin_at
else None,
}
def _admin_support_user_payload(user) -> Dict[str, Any]:
if not user:
return {}
return {
"user_id": user.user_id,
"telegram_id": user.telegram_id,
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
"telegram_photo_url": user.telegram_photo_url,
"is_banned": bool(user.is_banned),
"registration_date": user.registration_date.isoformat() if user.registration_date else None,
}
def _support_limit_offset(request: web.Request) -> tuple[int, int]:
limit = max(1, min(100, int(request.query.get("limit", 25) or 25)))
offset = max(0, int(request.query.get("offset", 0) or 0))
return limit, offset
async def admin_support_tickets_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
limit, offset = _support_limit_offset(request)
assigned_raw = request.query.get("assigned")
assigned_admin_id = None
if assigned_raw and assigned_raw not in {"all", "any"}:
assigned_admin_id = int(assigned_raw)
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
tickets = await support_dal.list_admin_tickets(
session,
status=request.query.get("status") or None,
priority=request.query.get("priority") or None,
category=request.query.get("category") or None,
assigned_admin_id=assigned_admin_id,
search=request.query.get("search") or None,
sort=request.query.get("sort") or "updated_desc",
limit=limit,
offset=offset,
)
return web.json_response(
{
"ok": True,
"tickets": [
{
**_support_ticket_payload(ticket),
"user": _admin_support_user_payload(getattr(ticket, "user", None)),
}
for ticket in tickets
],
}
)
async def admin_support_ticket_detail_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
ticket_id = int(request.match_info["id"])
async_session_factory: sessionmaker = request.app["async_session_factory"]
service = request.app["support_service"]
async with async_session_factory() as session:
ticket, messages = await support_dal.get_ticket(session, ticket_id, include_internal=True)
if not ticket:
return _error(404, "not_found", "Ticket not found")
user = await user_dal.get_user_by_id(session, ticket.user_id)
snapshot = await service.build_user_snapshot(user, session=session) if user else {}
author_ids = {m.author_user_id for m in messages if m.author_user_id is not None}
authors = {}
for author_id in author_ids:
author = await user_dal.get_user_by_id(session, author_id)
if author:
authors[author_id] = author
return web.json_response(
{
"ok": True,
"ticket": {
**_support_ticket_payload(ticket),
"user": _admin_support_user_payload(user),
},
"messages": [_support_message_payload(m, authors=authors) for m in messages],
"user_snapshot": snapshot,
}
)
async def admin_support_ticket_reply_route(request: web.Request) -> web.Response:
admin_id = _require_admin_user_id(request)
ticket_id = int(request.match_info["id"])
payload, error = _validate_model_payload(AdminTicketReplyPayload, await _read_json(request))
if error:
return error
try:
ticket, message = await request.app["support_service"].reply_as_admin(
admin_id,
ticket_id,
payload.body,
is_internal_note=payload.is_internal_note,
)
except TicketNotFound:
return _error(404, "not_found", "Ticket not found")
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
admin = await user_dal.get_user_by_id(session, admin_id)
return web.json_response(
{
"ok": True,
"ticket": _support_ticket_payload(ticket),
"message": _support_message_payload(
message, authors={admin_id: admin} if admin else {}
),
}
)
async def admin_support_ticket_patch_route(request: web.Request) -> web.Response:
admin_id = _require_admin_user_id(request)
ticket_id = int(request.match_info["id"])
payload, error = _validate_model_payload(AdminTicketPatchPayload, await _read_json(request))
if error:
return error
updates = payload.model_dump(exclude_unset=True)
try:
if updates.get("status") == "closed":
ticket = await request.app["support_service"].close_ticket(admin_id, ticket_id)
updates.pop("status", None)
if updates:
ticket = await request.app["support_service"]._update_and_audit(
admin_id,
ticket_id,
**updates,
)
else:
ticket = await request.app["support_service"]._update_and_audit(
admin_id,
ticket_id,
**updates,
)
except TicketNotFound:
return _error(404, "not_found", "Ticket not found")
return web.json_response({"ok": True, "ticket": _support_ticket_payload(ticket)})
async def admin_support_ticket_read_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
ticket_id = int(request.match_info["id"])
await request.app["support_service"].mark_read_as_admin(ticket_id)
return web.json_response({"ok": True})
async def admin_support_stats_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
stats = await support_dal.admin_stats(session)
return web.json_response({"ok": True, "stats": stats})
@@ -0,0 +1,16 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
async def admin_sync_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
queued = await enqueue_webhook_event(
settings,
"panel_sync",
{"requested_by": _require_admin_user_id(request)},
event_id=None,
)
if queued:
return _ok({"result": {"status": "queued"}})
return _error(503, "queue_unavailable")
@@ -0,0 +1,61 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
from .webapp_runtime import refresh_webapp_runtime_after_settings_change
async def admin_tariffs_get_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
path = _tariffs_config_path(settings)
try:
config = settings.tariffs_config
except Exception as exc:
logger.warning("Invalid tariffs config requested from admin UI: %s", exc)
return _error(400, "invalid_tariffs_config", str(exc))
if config is None:
return _ok(
{
"exists": path.exists(),
"path": str(path),
"catalog": {
"default_tariff": "",
"topup_packages_default": {"rub": [], "stars": []},
"tariffs": [],
},
}
)
return _ok(
{
"exists": True,
"path": str(path),
"catalog": _tariffs_config_payload(config),
}
)
async def admin_tariffs_save_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
payload = await _read_json(request)
catalog = payload.get("catalog") if "catalog" in payload else payload
if not isinstance(catalog, dict):
return _error(400, "invalid_payload", "catalog must be an object")
try:
config = TariffsConfig.model_validate(catalog)
except (ValidationError, ValueError) as exc:
return _error(400, "invalid_tariffs_config", str(exc))
path = _tariffs_config_path(settings)
try:
_write_tariffs_config_file(path, config)
except OSError as exc:
logger.exception("Failed to write tariffs config to %s", path)
return _error(500, "write_failed", str(exc))
await refresh_webapp_runtime_after_settings_change(request, updates={}, deletes=[])
return _ok({"exists": True, "path": str(path), "catalog": _tariffs_config_payload(config)})
@@ -0,0 +1,489 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
from .webapp_runtime import refresh_webapp_runtime_after_settings_change
import asyncio
import hashlib
import ipaddress
import shutil
import re
import socket
from aiohttp import ClientSession, ClientTimeout
from PIL import Image, ImageOps, UnidentifiedImageError
from config.webapp_themes_config import (
WebappThemesConfig,
ensure_webapp_core_themes,
resolved_webapp_themes_catalog,
write_webapp_theme_dir,
)
WEBAPP_LOGO_MAX_BYTES = 2 * 1024 * 1024
WEBAPP_UPLOADED_LOGO_DIR = Path(__file__).resolve().parents[5] / "data" / "webapp-logo" / "uploads"
WEBAPP_UPLOADED_LOGO_PATH = "/webapp-uploaded-logo"
WEBAPP_FAVICON_DIR = Path(__file__).resolve().parents[5] / "data" / "webapp-logo" / "favicons"
WEBAPP_FAVICON_PATH = "/webapp-favicon"
WEBAPP_EMOJI_CACHE_DIR = Path(__file__).resolve().parents[5] / "data" / "webapp-emoji"
WEBAPP_FAVICON_SIZES = (16, 32, 48, 180, 192, 512)
WEBAPP_LOGO_UPLOAD_CONTENT_TYPES = {
".gif": "image/gif",
".ico": "image/x-icon",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".svg": "image/svg+xml",
".webp": "image/webp",
}
def _theme_payload_for_version_compare(theme: Any) -> Dict[str, Any]:
if hasattr(theme, "model_dump"):
data = theme.model_dump(mode="json", exclude_none=True)
elif isinstance(theme, dict):
data = dict(theme)
else:
data = {}
data.pop("assets_version", None)
data.pop("default", None)
return data
def _bump_theme_asset_versions(
config: WebappThemesConfig,
previous: WebappThemesConfig,
) -> WebappThemesConfig:
previous_by_key = {theme.key: theme for theme in previous.themes}
default_changed = config.default_theme != previous.default_theme
data = config.model_dump(mode="json", exclude_none=True)
for theme in data.get("themes", []):
if not isinstance(theme, dict):
continue
if not str(theme.get("css_file") or "").strip():
continue
key = str(theme.get("key") or "")
previous_theme = previous_by_key.get(key)
previous_version = int(getattr(previous_theme, "assets_version", 0) or 0)
current_version = int(theme.get("assets_version") or 1)
theme_changed = previous_theme is None or _theme_payload_for_version_compare(
theme
) != _theme_payload_for_version_compare(previous_theme)
if theme_changed or (default_changed and key == config.default_theme):
theme["assets_version"] = max(previous_version + 1, current_version, 1)
elif previous_version > current_version:
theme["assets_version"] = previous_version
return WebappThemesConfig.model_validate(data)
def _detect_logo_extension(
body: bytes, content_type: str = "", filename: str = ""
) -> Optional[str]:
content_type = (content_type or "").split(";", 1)[0].strip().lower()
suffix = Path(filename or "").suffix.lower()
if content_type == "image/png" or body.startswith(b"\x89PNG\r\n\x1a\n"):
return ".png"
if content_type == "image/jpeg" or body.startswith(b"\xff\xd8\xff"):
return ".jpg"
if content_type == "image/gif" or body.startswith((b"GIF87a", b"GIF89a")):
return ".gif"
if content_type == "image/webp" or (
len(body) > 12 and body[:4] == b"RIFF" and body[8:12] == b"WEBP"
):
return ".webp"
if content_type in {"image/svg+xml", "image/svg"} or suffix == ".svg":
head = body[:512].lstrip().lower()
if head.startswith(b"<svg") or b"<svg" in head:
return ".svg"
if content_type == "image/x-icon" or suffix == ".ico":
if body.startswith(b"\x00\x00\x01\x00"):
return ".ico"
return suffix if suffix in WEBAPP_LOGO_UPLOAD_CONTENT_TYPES else None
def _write_uploaded_logo(body: bytes, content_type: str = "", filename: str = "") -> str:
if not body or len(body) > WEBAPP_LOGO_MAX_BYTES:
raise ValueError("logo must be a non-empty image up to 2 MiB")
ext = _detect_logo_extension(body, content_type, filename)
if ext not in WEBAPP_LOGO_UPLOAD_CONTENT_TYPES:
raise ValueError("unsupported image type")
digest = hashlib.sha256(body).hexdigest()[:16]
safe_name = f"logo-{digest}{ext}"
WEBAPP_UPLOADED_LOGO_DIR.mkdir(parents=True, exist_ok=True)
(WEBAPP_UPLOADED_LOGO_DIR / safe_name).write_bytes(body)
return f"{WEBAPP_UPLOADED_LOGO_PATH}/{safe_name}"
def _uploaded_logo_filename(url: str) -> Optional[str]:
parsed = urlsplit(str(url or ""))
path = parsed.path if parsed.scheme or parsed.netloc else str(url or "")
prefix = f"{WEBAPP_UPLOADED_LOGO_PATH}/"
if not path.startswith(prefix):
return None
filename = path.removeprefix(prefix)
if re.fullmatch(r"logo-[0-9a-f]{16}\.(?:gif|ico|jpe?g|png|svg|webp)", filename):
return filename
return None
def _favicon_digest(url: str) -> Optional[str]:
parsed = urlsplit(str(url or ""))
path = parsed.path if parsed.scheme or parsed.netloc else str(url or "")
match = re.fullmatch(
rf"{re.escape(WEBAPP_FAVICON_PATH)}/([0-9a-f]{{16}})/(?:[A-Za-z0-9_.-]+)",
path,
)
return match.group(1) if match else None
def _emoji_to_codepoints(value: str) -> str:
return "_".join(f"{ord(char):x}" for char in str(value or "").strip())
def prune_unused_appearance_assets(settings: Settings) -> None:
keep_logos = {
filename
for filename in [
_uploaded_logo_filename(getattr(settings, "WEBAPP_LOGO_URL", "")),
]
if filename
}
keep_favicons = {
digest
for digest in [
_favicon_digest(getattr(settings, "WEBAPP_FAVICON_URL", "")),
_favicon_digest(getattr(settings, "WEBAPP_LOGO_FAVICON_URL", "")),
]
if digest
}
keep_emoji_prefixes = set()
if (
getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False)
and str(getattr(settings, "WEBAPP_LOGO_EMOJI_FONT", "") or "").strip()
== "noto-color-animated"
):
codepoints = _emoji_to_codepoints(getattr(settings, "WEBAPP_LOGO_EMOJI", ""))
if codepoints:
keep_emoji_prefixes.add(f"{codepoints}.512.")
for path in WEBAPP_UPLOADED_LOGO_DIR.glob("logo-*"):
if path.is_file() and path.name not in keep_logos:
try:
path.unlink()
except OSError:
logger.warning("Failed to remove unused webapp logo %s", path, exc_info=True)
for path in WEBAPP_FAVICON_DIR.glob("*"):
if (
path.is_dir()
and re.fullmatch(r"[0-9a-f]{16}", path.name)
and path.name not in keep_favicons
):
try:
shutil.rmtree(path)
except OSError:
logger.warning("Failed to remove unused webapp favicon set %s", path, exc_info=True)
for path in WEBAPP_EMOJI_CACHE_DIR.glob("*.512.*"):
if path.is_file() and not any(
path.name.startswith(prefix) for prefix in keep_emoji_prefixes
):
try:
path.unlink()
except OSError:
logger.warning("Failed to remove unused webapp emoji asset %s", path, exc_info=True)
async def _persist_appearance_upload(
request: web.Request,
updates: Dict[str, Any],
actor_id: int,
) -> bool:
settings: Settings = request.app["settings"]
async_session_factory: sessionmaker = request.app["async_session_factory"]
result = await update_overrides(
settings,
async_session_factory,
updates=updates,
deletes=[],
actor_id=actor_id,
)
if not result.get("ok"):
logger.warning("Failed to persist uploaded appearance asset settings: %s", result)
return False
await refresh_webapp_runtime_after_settings_change(request, updates=updates, deletes=[])
return True
def _image_to_square_icon(source: Image.Image, size: int) -> Image.Image:
fitted = source.copy()
fitted.thumbnail((size, size), Image.Resampling.LANCZOS)
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
left = (size - fitted.width) // 2
top = (size - fitted.height) // 2
canvas.alpha_composite(fitted, (left, top))
return canvas
def _write_favicon_set(body: bytes, content_type: str = "", filename: str = "") -> Dict[str, Any]:
if not body or len(body) > WEBAPP_LOGO_MAX_BYTES:
raise ValueError("favicon source must be a non-empty image up to 2 MiB")
ext = _detect_logo_extension(body, content_type, filename)
digest = hashlib.sha256(body).hexdigest()[:16]
target_dir = WEBAPP_FAVICON_DIR / digest
target_dir.mkdir(parents=True, exist_ok=True)
if ext == ".svg":
safe_name = "favicon.svg"
(target_dir / safe_name).write_bytes(body)
return {
"favicon_url": f"{WEBAPP_FAVICON_PATH}/{digest}/{safe_name}",
"variants": {"svg": f"{WEBAPP_FAVICON_PATH}/{digest}/{safe_name}"},
}
try:
with Image.open(io.BytesIO(body)) as image:
image.seek(0)
source = ImageOps.exif_transpose(image).convert("RGBA")
except (OSError, UnidentifiedImageError, ValueError) as exc:
raise ValueError("favicon source must be a raster image") from exc
if source.width < 1 or source.height < 1 or source.width > 8192 or source.height > 8192:
raise ValueError("favicon source dimensions are not supported")
variants: Dict[str, str] = {}
png_icons: Dict[int, Image.Image] = {}
for size in WEBAPP_FAVICON_SIZES:
icon = _image_to_square_icon(source, size)
png_icons[size] = icon
filename = f"icon-{size}.png"
icon.save(target_dir / filename, format="PNG", optimize=True)
variants[f"{size}"] = f"{WEBAPP_FAVICON_PATH}/{digest}/{filename}"
png_icons[180].save(target_dir / "apple-touch-icon.png", format="PNG", optimize=True)
variants["apple_touch"] = f"{WEBAPP_FAVICON_PATH}/{digest}/apple-touch-icon.png"
png_icons[32].save(
target_dir / "favicon.ico",
format="ICO",
sizes=[(16, 16), (32, 32), (48, 48)],
)
variants["ico"] = f"{WEBAPP_FAVICON_PATH}/{digest}/favicon.ico"
return {
"favicon_url": variants["180"],
"variants": variants,
}
async def _read_uploaded_logo_file(request: web.Request) -> tuple[bytes, str, str]:
reader = await request.multipart()
async for part in reader:
if part.name != "file":
continue
body = bytearray()
while True:
chunk = await part.read_chunk(size=64 * 1024)
if not chunk:
break
body.extend(chunk)
if len(body) > WEBAPP_LOGO_MAX_BYTES:
raise ValueError("logo must be up to 2 MiB")
return bytes(body), part.headers.get("Content-Type", ""), part.filename or ""
raise ValueError("file field is required")
async def _hostname_resolves_to_public_address(hostname: str) -> bool:
if not hostname:
return False
try:
ip_obj = ipaddress.ip_address(hostname)
return not (
ip_obj.is_private
or ip_obj.is_loopback
or ip_obj.is_link_local
or ip_obj.is_unspecified
or ip_obj.is_reserved
)
except ValueError:
pass
loop = asyncio.get_running_loop()
try:
resolved = await loop.getaddrinfo(hostname, None, type=socket.SOCK_STREAM)
except Exception:
return False
found_public_ip = False
for entry in resolved:
sockaddr = entry[4]
candidate = sockaddr[0] if sockaddr else ""
try:
ip_obj = ipaddress.ip_address(candidate)
except ValueError:
continue
if (
ip_obj.is_private
or ip_obj.is_loopback
or ip_obj.is_link_local
or ip_obj.is_unspecified
or ip_obj.is_reserved
):
return False
found_public_ip = True
return found_public_ip
async def _fetch_logo_from_url(url: str) -> tuple[bytes, str, str]:
parsed = urlsplit(url)
if parsed.scheme != "https" or not parsed.hostname:
raise ValueError("only https image URLs are supported")
if not await _hostname_resolves_to_public_address(parsed.hostname):
raise ValueError("logo URL must resolve to a public address")
timeout = ClientTimeout(total=5)
async with ClientSession(timeout=timeout, headers={"User-Agent": "Mozilla/5.0"}) as session:
async with session.get(
url,
allow_redirects=False,
headers={"Accept": "image/avif,image/webp,image/svg+xml,image/png,image/*,*/*;q=0.8"},
) as response:
if response.status != 200:
raise ValueError(f"logo URL returned HTTP {response.status}")
content_type = (
(response.headers.get("Content-Type") or "").split(";", 1)[0].strip().lower()
)
if content_type and not content_type.startswith("image/"):
raise ValueError("logo URL returned non-image content")
body = bytearray()
async for chunk in response.content.iter_chunked(64 * 1024):
body.extend(chunk)
if len(body) > WEBAPP_LOGO_MAX_BYTES:
raise ValueError("logo must be up to 2 MiB")
return bytes(body), content_type, Path(parsed.path).name
async def admin_appearance_logo_upload_route(request: web.Request) -> web.Response:
actor_id = _require_admin_user_id(request)
content_type = (request.headers.get("Content-Type") or "").lower()
try:
if content_type.startswith("multipart/form-data"):
body, detected_content_type, filename = await _read_uploaded_logo_file(request)
else:
payload = await _read_json(request)
source_url = str(payload.get("url") or "").strip()
if not source_url:
return _error(400, "invalid_payload", "url or file is required")
body, detected_content_type, filename = await _fetch_logo_from_url(source_url)
logo_url = _write_uploaded_logo(body, detected_content_type, filename)
try:
favicon_payload = _write_favicon_set(body, detected_content_type, filename)
except ValueError:
favicon_payload = {}
except ValueError as exc:
return _error(400, "invalid_logo", str(exc))
except OSError as exc:
logger.exception("Failed to save uploaded webapp logo")
return _error(500, "write_failed", str(exc))
persisted = await _persist_appearance_upload(
request,
{
"WEBAPP_LOGO_URL": logo_url,
"WEBAPP_LOGO_USE_EMOJI": False,
**(
{"WEBAPP_LOGO_FAVICON_URL": favicon_payload["favicon_url"]}
if favicon_payload.get("favicon_url")
else {}
),
},
actor_id,
)
return _ok({"logo_url": logo_url, "persisted": persisted, **favicon_payload})
async def admin_appearance_favicon_upload_route(request: web.Request) -> web.Response:
actor_id = _require_admin_user_id(request)
content_type = (request.headers.get("Content-Type") or "").lower()
try:
if content_type.startswith("multipart/form-data"):
body, detected_content_type, filename = await _read_uploaded_logo_file(request)
else:
payload = await _read_json(request)
source_url = str(payload.get("url") or "").strip()
if not source_url:
return _error(400, "invalid_payload", "url or file is required")
body, detected_content_type, filename = await _fetch_logo_from_url(source_url)
favicon_payload = _write_favicon_set(body, detected_content_type, filename)
except ValueError as exc:
return _error(400, "invalid_favicon", str(exc))
except OSError as exc:
logger.exception("Failed to save uploaded webapp favicon")
return _error(500, "write_failed", str(exc))
persisted = await _persist_appearance_upload(
request,
{
"WEBAPP_FAVICON_URL": favicon_payload["favicon_url"],
"WEBAPP_FAVICON_USE_CUSTOM": True,
},
actor_id,
)
return _ok({"persisted": persisted, **favicon_payload})
async def admin_themes_get_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
primary = settings.WEBAPP_PRIMARY_COLOR or "#00fe7a"
catalog = resolved_webapp_themes_catalog(
primary_accent=primary,
env_default_theme=settings.WEBAPP_DEFAULT_THEME,
theme_dir=settings.WEBAPP_THEMES_DIR,
)
return _ok(
{
"exists": Path(settings.WEBAPP_THEMES_DIR).expanduser().exists(),
"themes_dir": str(Path(settings.WEBAPP_THEMES_DIR).expanduser()),
"catalog": _webapp_themes_catalog_payload(catalog),
}
)
async def admin_themes_save_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
settings: Settings = request.app["settings"]
previous_config = resolved_webapp_themes_catalog(
primary_accent=settings.WEBAPP_PRIMARY_COLOR or "#00fe7a",
env_default_theme=settings.WEBAPP_DEFAULT_THEME,
theme_dir=settings.WEBAPP_THEMES_DIR,
)
payload = await _read_json(request)
catalog = payload.get("catalog") if "catalog" in payload else payload
if not isinstance(catalog, dict):
return _error(400, "invalid_payload", "catalog must be an object")
try:
config = WebappThemesConfig.model_validate(catalog)
except (ValidationError, ValueError) as exc:
return _error(400, "invalid_webapp_themes_config", str(exc))
config, _changed = ensure_webapp_core_themes(config, settings.WEBAPP_PRIMARY_COLOR or "#00fe7a")
config = _bump_theme_asset_versions(config, previous_config)
try:
write_webapp_theme_dir(settings.WEBAPP_THEMES_DIR, config, delete_missing=True)
except OSError as exc:
logger.exception("Failed to write webapp themes to %s", settings.WEBAPP_THEMES_DIR)
return _error(500, "write_failed", str(exc))
await refresh_webapp_runtime_after_settings_change(request, updates={}, deletes=[])
return _ok(
{
"exists": True,
"themes_dir": str(Path(settings.WEBAPP_THEMES_DIR).expanduser()),
"catalog": _webapp_themes_catalog_payload(config),
}
)
@@ -0,0 +1,146 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
from bot.middlewares.i18n import JsonI18n, locale_language_options, resolve_locale_key
from bot.services.locale_override_service import (
LOCALE_OVERRIDES_PATH,
audience_for_locale_key,
group_id_for_locale_key,
locale_group_catalog,
load_locale_overrides,
update_locale_overrides,
)
def _locale_languages(
i18n: JsonI18n,
overrides: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
base_languages = set((i18n.base_locales_data or {}).keys())
override_languages = {str(entry.get("lang") or "") for entry in overrides or []}
override_languages.update((i18n.locale_overrides or {}).keys())
return locale_language_options(
base_languages | override_languages,
base_languages=base_languages,
)
def _locale_override_meta_map(overrides: List[Dict[str, Any]]) -> Dict[Tuple[str, str], Dict]:
result: Dict[Tuple[str, str], Dict] = {}
for entry in overrides:
lang = str(entry.get("lang") or "")
raw_key = str(entry.get("key") or "")
key = resolve_locale_key(raw_key)
if lang and key:
if raw_key != key and (lang, key) in result:
continue
result[(lang, key)] = entry
return result
def _admin_translations_payload(
i18n: JsonI18n,
overrides: List[Dict[str, Any]],
) -> Dict[str, Any]:
base_data = i18n.base_locales_data or i18n.locales_data or {}
effective_data = i18n.locales_data or {}
override_meta = _locale_override_meta_map(overrides)
language_items = _locale_languages(i18n, overrides)
languages = [item["code"] for item in language_items]
all_keys = sorted(
{key for messages in base_data.values() for key in messages.keys()}
| {key for _, key in override_meta.keys()}
)
groups_by_id = {
group["id"]: {
**group,
"items": [],
}
for group in locale_group_catalog()
}
for key in all_keys:
values: Dict[str, Dict[str, Any]] = {}
for lang in languages:
meta = override_meta.get((lang, key))
fallback_base = base_data.get(i18n.default_lang, {}).get(key, "")
values[lang] = {
"base": base_data.get(lang, {}).get(key, ""),
"fallback": fallback_base,
"effective": effective_data.get(lang, {}).get(key, ""),
"override": meta.get("value") if meta else "",
"overridden": bool(meta),
"updated_at": meta.get("updated_at") if meta else None,
"updated_by": meta.get("updated_by") if meta else None,
}
group_id = group_id_for_locale_key(key)
groups_by_id.setdefault(
group_id,
{"id": group_id, "title": group_id, "description": "", "items": []},
)
groups_by_id[group_id]["items"].append(
{
"key": key,
"audience": audience_for_locale_key(key),
"values": values,
}
)
groups = [group for group in groups_by_id.values() if group["items"]]
return {
"languages": language_items,
"groups": groups,
"path": str(LOCALE_OVERRIDES_PATH),
"override_count": len(overrides),
}
async def admin_translations_get_route(request: web.Request) -> web.Response:
_require_admin_user_id(request)
i18n: Optional[JsonI18n] = request.app.get("i18n")
if i18n is None:
return _error(503, "i18n_unavailable")
async_session_factory: sessionmaker = request.app["async_session_factory"]
await load_locale_overrides(i18n, async_session_factory)
async with async_session_factory() as session:
overrides = await locale_overrides_dal.get_overrides_with_meta(session)
return _ok(_admin_translations_payload(i18n, overrides))
async def admin_translations_patch_route(request: web.Request) -> web.Response:
actor_id = _require_admin_user_id(request)
i18n: Optional[JsonI18n] = request.app.get("i18n")
if i18n is None:
return _error(503, "i18n_unavailable")
async_session_factory: sessionmaker = request.app["async_session_factory"]
payload = await _read_json(request)
updates = payload.get("updates") or {}
deletes = payload.get("deletes") or []
if not isinstance(updates, dict):
return _error(400, "invalid_updates")
if not isinstance(deletes, list):
return _error(400, "invalid_deletes")
result = await update_locale_overrides(
i18n,
async_session_factory,
updates=updates,
deletes=deletes,
actor_id=actor_id,
)
if not result.get("ok"):
return web.json_response(
{"ok": False, "error": "validation_failed", "errors": result.get("errors", {})},
status=400,
)
return _ok(
{
"applied": result.get("applied", 0),
"reverted": result.get("reverted", 0),
"file_written": result.get("file_written", False),
}
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any
from bot.app.web.webapp.cache_helpers import (
invalidate_all_webapp_user_payloads,
reset_subscription_guides_cache,
reset_webapp_settings_cache,
)
WEBAPP_APPEARANCE_SETTING_KEYS = frozenset(
{
"WEBAPP_TITLE",
"WEBAPP_LOGO_URL",
"WEBAPP_LOGO_USE_EMOJI",
"WEBAPP_LOGO_EMOJI",
"WEBAPP_LOGO_EMOJI_FONT",
"WEBAPP_FAVICON_URL",
"WEBAPP_FAVICON_USE_CUSTOM",
"WEBAPP_LOGO_FAVICON_URL",
}
)
WEBAPP_DEVICE_PAYLOAD_SETTING_KEYS = frozenset(
{
"MY_DEVICES_SECTION_ENABLED",
"USER_HWID_DEVICE_LIMIT",
"USER_TRAFFIC_LIMIT_GB",
"USER_TRAFFIC_STRATEGY",
}
)
def changed_setting_keys(
updates: Mapping[str, Any] | None = None,
deletes: Sequence[Any] | None = None,
) -> set[str]:
keys = {str(key) for key in (updates or {}).keys()}
keys.update(str(key) for key in (deletes or []) if key is not None)
return keys
async def refresh_webapp_runtime_after_settings_change(
request: Any,
*,
updates: Mapping[str, Any] | None = None,
deletes: Sequence[Any] | None = None,
include_user_payloads: bool = True,
) -> None:
settings = request.app["settings"]
keys = changed_setting_keys(updates, deletes)
reset_webapp_settings_cache(request.app)
reset_subscription_guides_cache(request.app)
if include_user_payloads:
await invalidate_all_webapp_user_payloads(
settings,
include_devices=bool(keys & WEBAPP_DEVICE_PAYLOAD_SETTING_KEYS),
)
if keys & WEBAPP_APPEARANCE_SETTING_KEYS:
request.app["webapp_logo_cache"] = None
from bot.app.web.admin_api_impl.themes import prune_unused_appearance_assets
prune_unused_appearance_assets(settings)
@@ -0,0 +1,803 @@
"""Manifest of settings editable from the admin web app.
Each entry describes a single overridable attribute on the global
``Settings`` instance. The manifest is the only contract between the
admin UI and the backend: keys not listed here cannot be changed via
the API, even by an admin.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple
@dataclass(frozen=True)
class SettingField:
key: str
type: str # "string" | "int" | "float" | "bool" | "text" | "url" | "color" | "icon" | "json"
section: str
label: str
description: str = ""
placeholder: str = ""
optional: bool = True
secret: bool = False
min: Optional[float] = None
max: Optional[float] = None
choices: Optional[Tuple[Tuple[str, str], ...]] = None
subsection: Optional[str] = None # group label inside a section
i18n_label_key: Optional[str] = None
i18n_description_key: Optional[str] = None
i18n_subsection_key: Optional[str] = None
webhook_path: Optional[str] = None
webhook_requires_base_url: bool = False
webhook_provider_id: Optional[str] = None
webhook_hint_i18n_key: Optional[str] = None
webhook_hint: str = ""
SETTINGS_MANIFEST: List[SettingField] = [
# ─── General ────────────────────────────────────────────────────
SettingField(
"WEBAPP_TITLE",
"string",
"general",
"Web App title",
placeholder="My subscription",
),
SettingField(
"DEFAULT_LANGUAGE",
"string",
"general",
"Язык по умолчанию",
"Используется для приветственных сообщений и публичных страниц.",
),
SettingField(
"DEFAULT_CURRENCY_SYMBOL",
"string",
"general",
"Валюта",
"Например, RUB, USD, EUR.",
placeholder="RUB",
),
SettingField(
"SUPPORT_LINK", "url", "general", "Ссылка поддержки", "Куда вести пользователей за помощью."
),
SettingField("SERVER_STATUS_URL", "url", "general", "Ссылка на статус серверов"),
SettingField("TERMS_OF_SERVICE_URL", "url", "general", "Условия использования"),
SettingField("PRIVACY_POLICY_URL", "url", "general", "Политика конфиденциальности"),
SettingField("USER_AGREEMENT_URL", "url", "general", "Пользовательское соглашение"),
SettingField("DISABLE_WELCOME_MESSAGE", "bool", "general", "Скрыть приветствие /start"),
SettingField(
"START_COMMAND_DESCRIPTION", "string", "general", "Описание /start", placeholder=""
),
SettingField(
"REQUIRED_CHANNEL_ID",
"int",
"general",
"ID обязательного канала",
"Telegram ID канала, в котором нужно состоять.",
),
SettingField(
"REQUIRED_CHANNEL_LINK",
"string",
"general",
"Ссылка на канал",
"Имя пользователя или invite-link.",
),
SettingField(
"PANEL_API_URL",
"url",
"remnawave",
"URL API Remnawave",
"Например, https://panel.example.com/api.",
),
SettingField(
"PANEL_API_KEY",
"string",
"remnawave",
"API-ключ Remnawave",
"Секретный ключ API панели.",
secret=True,
),
SettingField(
"PANEL_WEBHOOK_SECRET",
"string",
"remnawave",
"Секрет вебхуков Remnawave",
"Используется для проверки входящих вебхуков панели.",
secret=True,
webhook_path="/webhook/panel",
webhook_requires_base_url=True,
webhook_provider_id="remnawave",
webhook_hint_i18n_key="admin_settings_panel_webhook_url_hint",
webhook_hint="Use this URL as WEBHOOK_URL in Remnawave Panel.",
),
SettingField(
"USER_SQUAD_UUIDS",
"string",
"remnawave",
"Internal Squads по умолчанию",
"UUID через запятую для legacy-режима без JSON-каталога тарифов.",
),
SettingField(
"USER_EXTERNAL_SQUAD_UUID",
"string",
"remnawave",
"External Squad по умолчанию",
"Необязательный UUID External Squad для новых пользователей.",
),
# ─── Web app appearance ────────────────────────────────────────
SettingField(
"SUBSCRIPTION_MINI_APP_URL",
"url",
"appearance",
"Публичный URL Mini App",
"Например, https://app.example.com/.",
),
SettingField(
"WEBAPP_PRIMARY_COLOR", "color", "appearance", "Основной цвет", placeholder="#00fe7a"
),
SettingField("WEBAPP_LOGO_USE_EMOJI", "bool", "appearance", "Использовать эмоджи-логотип"),
SettingField("WEBAPP_LOGO_URL", "url", "appearance", "URL логотипа"),
SettingField("WEBAPP_LOGO_EMOJI", "string", "appearance", "Эмоджи-логотип", placeholder="🫥"),
SettingField(
"WEBAPP_LOGO_EMOJI_FONT",
"string",
"appearance",
"Шрифт эмоджи-логотипа",
"Выберите шрифт для отображения эмодзи-логотипа",
choices=(
("system", "Системный (по умолчанию)"),
("noto-color", "Noto Color Emoji"),
("noto-color-animated", "Noto Color Emoji Animated"),
("noto-emoji", "Noto Emoji"),
("twemoji", "Twitter Emoji"),
("openmoji", "OpenMoji"),
("apple", "Apple Color Emoji (local)"),
("segoe", "Segoe UI Emoji (local)"),
("noto-local", "Noto Emoji (local)"),
),
),
SettingField(
"WEBAPP_FAVICON_USE_CUSTOM",
"bool",
"appearance",
"Использовать отдельную favicon",
),
SettingField("WEBAPP_FAVICON_URL", "url", "appearance", "URL отдельной favicon"),
SettingField("WEBAPP_LOGO_FAVICON_URL", "url", "appearance", "Favicon из логотипа"),
SettingField("WEBAPP_ENABLED", "bool", "appearance", "Web App включён"),
SettingField(
"SUBSCRIPTION_GUIDES_ENABLED",
"bool",
"subscription_guides",
"Embedded install guides",
"Open install instructions inside the Web App instead of an external connect page.",
),
SettingField(
"SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED",
"bool",
"subscription_guides",
"Open install guides from bot",
(
"Use the Telegram Mini App install screen for bot connect buttons and show "
"public install guide links."
),
),
SettingField(
"SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED",
"bool",
"subscription_guides",
"Use Remnawave Panel config",
(
"Fetch Subscription Page config from Remnawave Panel by the user's "
"subscription short UUID."
),
),
SettingField(
"SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED",
"bool",
"subscription_guides",
"Enable admin JSON override",
"Use the JSON field below instead of Remnawave Panel config. Disabled by default.",
),
SettingField(
"SUBSCRIPTION_PAGE_CONFIG_PATH",
"string",
"subscription_guides",
"Subscription Page config path",
"Fallback path to a Remnawave Subscription Page v1 JSON config file.",
placeholder="data/subpage-config/multiapp.json",
),
SettingField(
"SUBSCRIPTION_PAGE_CONFIG_JSON",
"json",
"subscription_guides",
"Subscription Page config JSON",
(
"Optional admin JSON override. It is applied only when the JSON override "
"switch is enabled."
),
placeholder='{\n "version": "1"\n}',
),
# ─── Subscription periods & pricing ────────────────────────────
SettingField("MONTH_1_ENABLED", "bool", "pricing", "Тариф 1 месяц"),
SettingField("MONTH_3_ENABLED", "bool", "pricing", "Тариф 3 месяца"),
SettingField("MONTH_6_ENABLED", "bool", "pricing", "Тариф 6 месяцев"),
SettingField("MONTH_12_ENABLED", "bool", "pricing", "Тариф 12 месяцев"),
SettingField("RUB_PRICE_1_MONTH", "int", "pricing", "Цена 1 мес. (RUB)"),
SettingField("RUB_PRICE_3_MONTHS", "int", "pricing", "Цена 3 мес. (RUB)"),
SettingField("RUB_PRICE_6_MONTHS", "int", "pricing", "Цена 6 мес. (RUB)"),
SettingField("RUB_PRICE_12_MONTHS", "int", "pricing", "Цена 12 мес. (RUB)"),
SettingField("STARS_PRICE_1_MONTH", "int", "pricing", "Цена 1 мес. (Stars)"),
SettingField("STARS_PRICE_3_MONTHS", "int", "pricing", "Цена 3 мес. (Stars)"),
SettingField("STARS_PRICE_6_MONTHS", "int", "pricing", "Цена 6 мес. (Stars)"),
SettingField("STARS_PRICE_12_MONTHS", "int", "pricing", "Цена 12 мес. (Stars)"),
SettingField(
"REFERRAL_BONUS_DAYS_INVITER_1_MONTH",
"int",
"pricing",
"Бонус приглашающему: 1 мес.",
min=0,
subsection="legacy_tariffs",
),
SettingField(
"REFERRAL_BONUS_DAYS_INVITER_3_MONTHS",
"int",
"pricing",
"Бонус приглашающему: 3 мес.",
min=0,
subsection="legacy_tariffs",
),
SettingField(
"REFERRAL_BONUS_DAYS_INVITER_6_MONTHS",
"int",
"pricing",
"Бонус приглашающему: 6 мес.",
min=0,
subsection="legacy_tariffs",
),
SettingField(
"REFERRAL_BONUS_DAYS_INVITER_12_MONTHS",
"int",
"pricing",
"Бонус приглашающему: 12 мес.",
min=0,
subsection="legacy_tariffs",
),
SettingField(
"REFERRAL_BONUS_DAYS_REFEREE_1_MONTH",
"int",
"pricing",
"Бонус приглашённому: 1 мес.",
min=0,
subsection="legacy_tariffs",
),
SettingField(
"REFERRAL_BONUS_DAYS_REFEREE_3_MONTHS",
"int",
"pricing",
"Бонус приглашённому: 3 мес.",
min=0,
subsection="legacy_tariffs",
),
SettingField(
"REFERRAL_BONUS_DAYS_REFEREE_6_MONTHS",
"int",
"pricing",
"Бонус приглашённому: 6 мес.",
min=0,
subsection="legacy_tariffs",
),
SettingField(
"REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS",
"int",
"pricing",
"Бонус приглашённому: 12 мес.",
min=0,
subsection="legacy_tariffs",
),
SettingField(
"TRAFFIC_PACKAGES", "string", "pricing", "Пакеты трафика", "Формат: 10:199,50:799 (ГБ:цена)"
),
SettingField("STARS_TRAFFIC_PACKAGES", "string", "pricing", "Пакеты трафика (Stars)"),
SettingField(
"SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED",
"bool",
"payments",
"Показывать описание подписки",
"Текст появится перед выбором срока покупки или продления.",
subsection="checkout",
),
SettingField(
"SUBSCRIPTION_PURCHASE_DESCRIPTION_RU",
"text",
"payments",
"Описание подписки (RU)",
"Русская версия текста на этапе оплаты.",
subsection="checkout",
),
SettingField(
"SUBSCRIPTION_PURCHASE_DESCRIPTION_EN",
"text",
"payments",
"Описание подписки (EN)",
"Английская версия текста на этапе оплаты.",
subsection="checkout",
),
# ─── Payment providers (toggles) ───────────────────────────────
# Common
SettingField("STARS_ENABLED", "bool", "payments", "Telegram Stars", subsection="common"),
SettingField(
"STARS_ADMIN_ONLY_ENABLED",
"bool",
"payments",
"Telegram Stars admin-only",
(
"Shows Telegram Stars only to users from ADMIN_IDS. "
"Payment callbacks remain active for admin test payments."
),
subsection="common",
i18n_label_key="admin_settings_provider_admin_only_label",
i18n_description_key="admin_settings_provider_admin_only_description",
),
SettingField(
"PAYMENT_METHODS_ORDER",
"string",
"payments",
"Порядок методов оплаты",
"Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay,heleket",
subsection="common",
),
# ─── Trial ─────────────────────────────────────────────────────
SettingField(
"TRIAL_ENABLED",
"bool",
"pricing",
"Триал включён",
optional=False,
subsection="trial",
),
SettingField(
"TRIAL_DURATION_DAYS",
"int",
"pricing",
"Длительность триала (дней)",
optional=False,
min=0,
subsection="trial",
),
SettingField(
"TRIAL_TRAFFIC_LIMIT_GB",
"float",
"pricing",
"Лимит трафика триала (ГБ)",
optional=False,
min=0,
subsection="trial",
),
SettingField(
"TRIAL_TRAFFIC_STRATEGY",
"string",
"pricing",
"Стратегия сброса трафика триала",
optional=False,
subsection="trial",
),
SettingField(
"TRIAL_SQUAD_UUIDS",
"string",
"pricing",
"Internal Squads для триала",
"UUID через запятую. Если пусто, используется USER_SQUAD_UUIDS.",
subsection="trial",
),
# ─── Referral program ──────────────────────────────────────────
SettingField(
"REFERRAL_ONE_BONUS_PER_REFEREE", "bool", "referral", "Один бонус на приглашённого"
),
SettingField(
"REFERRAL_WELCOME_BONUS_DAYS", "int", "referral", "Приветственный бонус (дней)", min=0
),
SettingField("LEGACY_REFS", "bool", "referral", "Поддержка старых ref-ссылок"),
# ─── Notifications ─────────────────────────────────────────────
SettingField(
"SUBSCRIPTION_NOTIFICATIONS_ENABLED",
"bool",
"notifications",
"Включены уведомления о подписке",
),
SettingField(
"SUBSCRIPTION_EMAIL_NOTIFICATIONS_ENABLED",
"bool",
"notifications",
"Дублировать уведомления о подписке на email",
"Письма отправляются только пользователям с привязанным email и рабочим SMTP.",
),
SettingField(
"SUBSCRIPTION_NOTIFY_ON_EXPIRE", "bool", "notifications", "Уведомлять об истечении"
),
SettingField(
"SUBSCRIPTION_NOTIFY_AFTER_EXPIRE", "bool", "notifications", "Уведомлять после истечения"
),
SettingField(
"SUBSCRIPTION_NOTIFY_DAYS_BEFORE",
"int",
"notifications",
"За сколько дней предупреждать",
min=0,
),
SettingField(
"SUBSCRIPTION_NOTIFY_HOURS_BEFORE",
"int",
"notifications",
"За сколько часов предупреждать",
min=0,
max=23,
),
SettingField("LOG_NEW_USERS", "bool", "notifications", "Логировать новых пользователей"),
SettingField("LOG_PAYMENTS", "bool", "notifications", "Логировать платежи"),
SettingField("LOG_SUPPORT", "bool", "notifications", "Логировать тикеты поддержки"),
SettingField(
"LOG_PROMO_ACTIVATIONS", "bool", "notifications", "Логировать активации промокодов"
),
SettingField("LOG_TRIAL_ACTIVATIONS", "bool", "notifications", "Логировать активации триала"),
SettingField(
"LOG_SUSPICIOUS_ACTIVITY", "bool", "notifications", "Логировать подозрительные действия"
),
SettingField(
"LOG_ADMIN_ACTIONS",
"bool",
"notifications",
"Логировать действия администраторов",
"Если выключено, события от пользователей из ADMIN_IDS не записываются в message logs.",
i18n_label_key="admin_settings_field_log_admin_actions_label",
i18n_description_key="admin_settings_field_log_admin_actions_description",
),
SettingField(
"LOG_LEVEL",
"string",
"notifications",
"Глобальный уровень логов",
"DEBUG / INFO / WARNING / ERROR",
),
SettingField("LOG_CHAT_ID", "int", "notifications", "ID чата для логов"),
SettingField("LOG_THREAD_ID", "int", "notifications", "ID треда (для супергрупп)"),
SettingField(
"LOG_SUPPORT_THREAD_ID",
"int",
"notifications",
"ID треда поддержки",
"Тред лог-чата для уведомлений о тикетах поддержки.",
),
SettingField(
"BACKUP_ENABLED",
"bool",
"backups",
"Бэкапы включены",
"Worker будет периодически собирать ZIP-архив и отправлять его в Telegram.",
),
SettingField(
"BACKUP_CHAT_ID",
"int",
"backups",
"ID чата для бэкапов",
"Куда отправлять ZIP-архивы. Если пусто, используется LOG_CHAT_ID.",
),
SettingField(
"BACKUP_THREAD_ID",
"int",
"backups",
"ID треда для бэкапов",
"Необязательный topic/thread ID. Если пусто, используется LOG_THREAD_ID.",
),
SettingField(
"BACKUP_INTERVAL_SECONDS",
"int",
"backups",
"Период бэкапов (сек.)",
"По умолчанию 3600: запуск на границе часа (12:00, 13:00 и т.д.).",
optional=False,
min=60,
),
SettingField(
"BACKUP_LOCAL_RETENTION",
"int",
"backups",
"Сколько архивов хранить",
"Сколько последних ZIP-архивов оставлять в data/backups на сервере.",
optional=False,
min=1,
),
SettingField(
"BACKUP_COMPOSE_ENABLED",
"bool",
"backups",
"Добавлять compose-папку",
(
"Добавляет snapshot /app/compose-source. Если папка не смонтирована, "
"бэкап БД все равно будет создан."
),
),
SettingField(
"SUPPORT_TICKETS_ENABLED",
"bool",
"support",
"Тикеты поддержки включены",
"Показывает раздел поддержки в ЛК и включает создание тикетов.",
),
SettingField(
"SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED",
"bool",
"support",
"Email-уведомления админам",
(
"Если выключено, новые тикеты и ответы пользователей останутся "
"только в Telegram и лог-чате."
),
),
SettingField(
"SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS",
"int",
"support",
"Пауза Telegram-уведомлений",
(
"Минимум секунд между повторными Telegram/log уведомлениями "
"по одному непрочитанному тикету."
),
min=0,
),
SettingField(
"SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS",
"int",
"support",
"Пауза email-уведомлений",
"Минимум секунд между повторными email-уведомлениями по одному непрочитанному тикету.",
min=0,
),
SettingField(
"SUPPORT_TICKET_MAX_BODY_LENGTH",
"int",
"support",
"Макс. длина сообщения",
"Максимальное количество символов в сообщении тикета.",
min=1,
),
SettingField(
"SUPPORT_TICKET_MAX_SUBJECT_LENGTH",
"int",
"support",
"Макс. длина темы",
"Максимальное количество символов в теме тикета.",
min=1,
),
SettingField(
"SUPPORT_TICKET_RATE_LIMIT_PER_HOUR",
"int",
"support",
"Лимит тикетов в час",
"Сколько новых тикетов пользователь может создать за час. 0 — без лимита.",
min=0,
),
# ─── Devices ───────────────────────────────────────────────────
SettingField("MY_DEVICES_SECTION_ENABLED", "bool", "devices", "Раздел «Мои устройства»"),
SettingField(
"USER_HWID_DEVICE_LIMIT", "int", "devices", "Лимит устройств по умолчанию (0 = ∞)", min=0
),
SettingField("USER_TRAFFIC_LIMIT_GB", "float", "devices", "Лимит трафика пользователя (ГБ)"),
SettingField("USER_TRAFFIC_STRATEGY", "string", "devices", "Стратегия сброса трафика"),
]
def _provider_field_to_setting_field(spec: Any, manifest_field: Any) -> SettingField:
return SettingField(
key=manifest_field.key,
type=manifest_field.type,
section="payments",
label=manifest_field.label,
description=manifest_field.description,
placeholder=manifest_field.placeholder,
optional=manifest_field.optional,
secret=manifest_field.secret,
min=manifest_field.min,
max=manifest_field.max,
choices=tuple(manifest_field.choices) if manifest_field.choices else None,
subsection=manifest_field.subsection,
i18n_label_key=getattr(manifest_field, "i18n_label_key", None),
i18n_description_key=getattr(manifest_field, "i18n_description_key", None),
i18n_subsection_key=getattr(manifest_field, "i18n_subsection_key", None),
)
def aggregated_manifest() -> List[SettingField]:
"""SETTINGS_MANIFEST + per-provider fragments declared in provider SPECs."""
from bot.payment_providers import iter_provider_manifest_fields # local to avoid cycle
fields: List[SettingField] = list(SETTINGS_MANIFEST)
for spec, manifest_field in iter_provider_manifest_fields():
fields.append(_provider_field_to_setting_field(spec, manifest_field))
return fields
def get_field_by_key(key: str) -> Optional[SettingField]:
for field in aggregated_manifest():
if field.key == key:
return field
return None
def manifest_keys() -> List[str]:
return [f.key for f in aggregated_manifest()]
def coerce_value(field: SettingField, raw: Any) -> Any:
"""Coerce a value coming from JSON to the type declared by the field."""
if field.type == "json":
if raw is None:
return ""
text = raw if isinstance(raw, str) else str(raw)
text = text.strip()
if not text:
return ""
from config.subscription_guides_config import validate_subscription_guides_config_text
validate_subscription_guides_config_text(text)
return text
if raw is None or (isinstance(raw, str) and raw.strip() == ""):
if not field.optional:
raise ValueError(f"{field.key}: value required")
return None
if field.type == "bool":
if isinstance(raw, bool):
return raw
if isinstance(raw, (int, float)):
return bool(raw)
if isinstance(raw, str):
return raw.strip().lower() in {"1", "true", "yes", "on"}
return bool(raw)
if field.type == "int":
try:
value = int(str(raw).strip())
except (TypeError, ValueError) as exc:
raise ValueError(f"{field.key}: integer expected") from exc
if field.min is not None and value < field.min:
raise ValueError(f"{field.key}: must be >= {field.min:g}")
if field.max is not None and value > field.max:
raise ValueError(f"{field.key}: must be <= {field.max:g}")
return value
if field.type == "float":
try:
value = float(str(raw).strip())
except (TypeError, ValueError) as exc:
raise ValueError(f"{field.key}: number expected") from exc
if field.min is not None and value < field.min:
raise ValueError(f"{field.key}: must be >= {field.min:g}")
if field.max is not None and value > field.max:
raise ValueError(f"{field.key}: must be <= {field.max:g}")
return value
if isinstance(raw, str):
return raw.strip()
return str(raw)
def _i18n_slug(value: str) -> str:
slug = re.sub(r"[^a-z0-9]+", "_", value.strip().lower()).strip("_")
return slug or "default"
def manifest_payload() -> List[dict]:
"""Serialize the manifest for the admin UI.
For provider presentation fields we resolve the SPEC-declared default
(e.g. the button text the bot would use if the admin leaves the override
blank) and expose it as ``default``; ``placeholder`` falls back to the
same value so existing UIs that only read ``placeholder`` also show the
hint inside the empty input.
"""
from bot.payment_providers import (
find_manifest_owner,
manifest_field_default,
provider_admin_only_pairs,
provider_webhook_metadata,
)
sections_order = {
"general": 1,
"appearance": 2,
"remnawave": 3,
"pricing": 11,
"payments": 4,
"trial": 5,
"referral": 6,
"notifications": 7,
"support": 8,
"backups": 9,
"devices": 10,
"subscription_guides": 10,
}
exclusive_map = {
key: opposite
for public_key, admin_key in provider_admin_only_pairs()
for key, opposite in ((public_key, admin_key), (admin_key, public_key))
}
items: List[dict] = []
for field in aggregated_manifest():
auto_label_i18n_key = f"admin_settings_field_{field.key.lower()}_label"
auto_description_i18n_key = f"admin_settings_field_{field.key.lower()}_description"
auto_subsection_i18n_key = (
f"admin_settings_subsection_{_i18n_slug(field.subsection)}"
if field.subsection
else None
)
default_value: Optional[str] = None
webhook_metadata: Optional[dict] = None
owner = find_manifest_owner(field.key)
if owner is not None:
spec, manifest_field = owner
default_value = manifest_field_default(spec, manifest_field)
webhook_metadata = provider_webhook_metadata(spec)
placeholder = field.placeholder
if not placeholder and default_value:
placeholder = default_value
item = {
"key": field.key,
"type": field.type,
"section": field.section,
"section_order": sections_order.get(field.section, 99),
"subsection": field.subsection,
"label": field.label,
"description": field.description,
"i18n_label_key": field.i18n_label_key or auto_label_i18n_key,
"i18n_description_key": field.i18n_description_key
or (auto_description_i18n_key if field.description else None),
"i18n_subsection_key": field.i18n_subsection_key or auto_subsection_i18n_key,
"i18n_placeholder_key": (
f"admin_settings_field_{field.key.lower()}_placeholder" if placeholder else None
),
"placeholder": placeholder,
"optional": field.optional,
"secret": field.secret,
}
if field.min is not None:
item["min"] = field.min
if field.max is not None:
item["max"] = field.max
if field.key in exclusive_map:
item["mutually_exclusive_key"] = exclusive_map[field.key]
if default_value is not None:
item["default"] = default_value
if webhook_metadata:
item.update(webhook_metadata)
if field.webhook_path:
item["webhook_path"] = field.webhook_path
item["webhook_requires_base_url"] = field.webhook_requires_base_url
if field.webhook_provider_id:
item["provider_id"] = field.webhook_provider_id
if field.webhook_hint_i18n_key:
item["webhook_hint_i18n_key"] = field.webhook_hint_i18n_key
if field.webhook_hint:
item["webhook_hint"] = field.webhook_hint
if field.choices:
item["choices"] = [
{
"value": v,
"label": lbl,
"i18n_label_key": (
f"admin_settings_field_{field.key.lower()}_choice_{_i18n_slug(str(v))}"
),
}
for v, lbl in field.choices
]
items.append(item)
return items
+29
View File
@@ -0,0 +1,29 @@
from __future__ import annotations
from typing import Optional
from aiohttp import web
from bot.app.web.webapp_auth import verify_webapp_session_token
from config.settings import Settings
WEBAPP_SESSION_COOKIE_NAME = "rw_webapp_session"
def extract_authenticated_user_id(request: web.Request) -> Optional[int]:
settings: Settings = request.app["settings"]
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
user_id = verify_webapp_session_token(
settings,
auth_header.removeprefix("Bearer ").strip(),
)
if user_id:
return user_id
session_cookie = request.cookies.get(WEBAPP_SESSION_COOKIE_NAME)
if session_cookie:
return verify_webapp_session_token(settings, session_cookie)
return None
@@ -0,0 +1,54 @@
"""Compatibility facade for the subscription Mini App backend."""
# ruff: noqa: I001
from bot.app.web.webapp import (
_runtime as _runtime,
account as _account,
application as _application,
assets as _assets,
auth as _auth,
billing as _billing,
common as _common,
devices as _devices,
guides as _guides,
payloads as _payloads,
routes as _routes,
serializers as _serializers,
support as _support,
telegram_notifications as _telegram_notifications,
)
_MODULES = (
_runtime,
_payloads,
_common,
_assets,
_auth,
_account,
_telegram_notifications,
_serializers,
_billing,
_devices,
_guides,
_support,
_routes,
_application,
)
_NAMESPACE = {}
for _module in _MODULES:
_NAMESPACE.update(
{
_name: _value
for _name, _value in vars(_module).items()
if not _name.startswith("__") and _name != "annotations"
}
)
for _module in _MODULES:
vars(_module).update(_NAMESPACE)
globals().update(_NAMESPACE)
__all__ = sorted(_name for _name in _NAMESPACE if not _name.startswith("__"))
Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@@ -0,0 +1,219 @@
<!doctype html>
<html lang="__LANG__">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link id="app-favicon" rel="icon" href="data:," sizes="any">
<title>__PAGE_TITLE__</title>
<style nonce="__NONCE__">
:root {
color-scheme: dark light;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
background: #0b1017;
color: #f7fafc;
}
body {
min-height: 100dvh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
box-sizing: border-box;
}
main {
width: min(100%, 420px);
display: grid;
gap: 14px;
text-align: center;
}
h1 {
margin: 0;
font-size: 24px;
line-height: 1.2;
}
p {
margin: 0;
color: #aeb8c5;
font-size: 15px;
line-height: 1.55;
}
.actions {
display: grid;
gap: 10px;
margin-top: 4px;
}
.button {
display: inline-flex;
min-height: 46px;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 8px;
background: #14b86f;
color: #03120b;
padding: 0 18px;
box-sizing: border-box;
font: inherit;
font-weight: 800;
text-decoration: none;
cursor: pointer;
}
.button.secondary {
border-color: #2d3847;
background: transparent;
color: #f7fafc;
}
.button[aria-disabled="true"] {
pointer-events: none;
background: #344052;
color: #aeb8c5;
}
[hidden] {
display: none !important;
}
</style>
</head>
<body>
<main>
<h1 id="title"></h1>
<p id="status"></p>
<div class="actions">
<a id="open-link" class="button" href="#" rel="noreferrer"></a>
<button id="close-button" class="button secondary" type="button" hidden></button>
</div>
</main>
<script nonce="__NONCE__">
(() => {
const messages = __MESSAGES_JSON__;
const titleEl = document.getElementById("title");
const statusEl = document.getElementById("status");
const openLink = document.getElementById("open-link");
const closeButton = document.getElementById("close-button");
const params = new URLSearchParams(window.location.hash.replace(/^#/, ""));
const target = String(params.get("url") || "").trim();
const isUnsafe =
!target ||
hasControlChars(target) ||
/^(?:javascript|data|vbscript|https?):/i.test(target);
let attempted = false;
let pageLeft = false;
let state = "opening";
function hasControlChars(value) {
return Array.from(String(value || "")).some((char) => {
const code = char.charCodeAt(0);
return code <= 31 || code === 127;
});
}
function text(key, fallback) {
const value = messages && messages[key];
return typeof value === "string" && value ? value : fallback;
}
function tryCloseWindow() {
try {
window.close();
} catch (_error) {
void _error;
}
}
function render(nextState) {
state = nextState;
if (nextState === "unavailable") {
titleEl.textContent = text("unavailableTitle", "App link unavailable");
statusEl.textContent = text("unavailableHint", "Return to Telegram and try again.");
openLink.textContent = text("button", "Open app");
openLink.setAttribute("aria-disabled", "true");
openLink.removeAttribute("href");
closeButton.hidden = true;
return;
}
if (nextState === "done") {
titleEl.textContent = text("doneTitle", "Settings added");
statusEl.textContent = text("doneHint", "You can close this window.");
openLink.textContent = text("retryButton", "Open again");
openLink.removeAttribute("aria-disabled");
openLink.href = target;
closeButton.textContent = text("closeButton", "Close window");
closeButton.hidden = false;
return;
}
titleEl.textContent = text("title", "Opening app");
statusEl.textContent =
nextState === "manual"
? text("manualHint", "If the app did not open automatically, tap the button below.")
: text("hint", "Opening the app on this device...");
openLink.textContent = text("button", "Open app");
openLink.removeAttribute("aria-disabled");
openLink.href = target;
closeButton.hidden = true;
}
function markDone() {
if (state === "done" || isUnsafe) return;
render("done");
window.setTimeout(tryCloseWindow, 120);
}
function notePageLeft() {
if (!attempted) return;
pageLeft = true;
window.setTimeout(markDone, 900);
}
function openTarget() {
if (isUnsafe) return;
attempted = true;
pageLeft = false;
render("opening");
window.location.href = target;
window.setTimeout(() => {
if (state === "opening" && !pageLeft) render("manual");
}, 1600);
}
if (isUnsafe) {
render("unavailable");
return;
}
openLink.addEventListener("click", (event) => {
event.preventDefault();
openTarget();
});
closeButton.addEventListener("click", () => {
tryCloseWindow();
render("done");
});
window.addEventListener("pagehide", notePageLeft);
window.addEventListener("blur", notePageLeft);
document.addEventListener("visibilitychange", () => {
if (!attempted) return;
if (document.hidden) {
pageLeft = true;
} else if (pageLeft) {
markDone();
}
});
render("opening");
window.setTimeout(openTarget, 80);
})();
</script>
</body>
</html>
@@ -0,0 +1,73 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#03070b" />
<link id="app-favicon" rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png" />
<link
id="app-apple-touch-icon"
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="apple-touch-icon-precomposed"
sizes="180x180"
href="/apple-touch-icon-precomposed.png"
/>
<title>/minishop</title>
<link rel="stylesheet" href="/subscription_webapp.css" />
<style>
.app-boot-fallback {
min-height: 100dvh;
display: grid;
place-items: center;
padding: 24px;
background: #03070b;
}
.app-boot-fallback__spinner {
width: 28px;
height: 28px;
border: 2px solid rgba(242, 247, 244, 0.18);
border-top-color: #00fe7a;
border-radius: 999px;
animation: appBootSpin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.app-boot-fallback__spinner {
animation: none;
}
}
@keyframes appBootSpin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<main id="app">
<div class="app-boot-fallback" role="status" aria-label="Загрузка">
<div class="app-boot-fallback__spinner" aria-hidden="true"></div>
</div>
</main>
<!-- WEBAPP_I18N_SCRIPT -->
<!-- WEBAPP_CONFIG_SCRIPT -->
<!-- WEBAPP_JS_SCRIPT -->
<!-- WEBAPP_DEV_MOCK_START -->
<script src="/subscription_webapp.js" defer></script>
<!-- WEBAPP_DEV_MOCK_END -->
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
{
"key": "ascii",
"names": {
"ru": "ASCII",
"en": "ASCII"
},
"enabled": true,
"default": false,
"use_primary_accent": false,
"use_in_admin": true,
"css_file": "style.css",
"assets_version": 4,
"tokens": {
"color_scheme": "dark",
"style_preset": "ascii"
}
}
@@ -0,0 +1,27 @@
{
"key": "dark",
"names": {
"ru": "Темная",
"en": "Dark"
},
"enabled": true,
"default": true,
"use_primary_accent": true,
"use_in_admin": true,
"assets_version": 1,
"tokens": {
"color_scheme": "dark",
"bg": "#03070b",
"panel": "#111820",
"panel_2": "#0b1118",
"panel_3": "#17212b",
"border": "rgba(255, 255, 255, 0.12)",
"border_strong": "rgba(255, 255, 255, 0.2)",
"text": "#f2f7f4",
"muted": "#a9b4b0",
"dim": "#68736f",
"danger": "#ff6b6b",
"blue": "#2d9cff",
"radius": "8px"
}
}
+206
View File
@@ -0,0 +1,206 @@
.theme-key-light {
color-scheme: light;
--accent: #047857;
--bg: #f7f8fb;
--panel: #ffffff;
--panel-2: #f1f5f9;
--panel-3: #e8edf3;
--border: rgba(15, 23, 42, 0.11);
--border-strong: rgba(15, 23, 42, 0.2);
--text: #0f172a;
--muted: #475569;
--dim: #64748b;
--danger: #dc2626;
--danger-text: #b91c1c;
--danger-soft: color-mix(in srgb, var(--danger) 9%, var(--panel));
--danger-border: color-mix(in srgb, var(--danger) 34%, var(--border));
--success: #16a34a;
--success-text: #166534;
--success-soft: color-mix(in srgb, var(--success) 10%, var(--panel));
--success-border: color-mix(in srgb, var(--success) 34%, var(--border));
--warning: #d97706;
--warning-text: #92400e;
--warning-soft: color-mix(in srgb, var(--warning) 11%, var(--panel));
--warning-border: color-mix(in srgb, var(--warning) 34%, var(--border));
--info: #2563eb;
--info-text: #1d4ed8;
--info-soft: color-mix(in srgb, var(--info) 9%, var(--panel));
--info-border: color-mix(in srgb, var(--info) 30%, var(--border));
--blue: #2563eb;
--radius: 8px;
--accent-contrast: #ffffff;
--surface-sheen: rgba(15, 23, 42, 0.035);
--surface-sheen-soft: rgba(15, 23, 42, 0.012);
--surface-hover: rgba(15, 23, 42, 0.045);
--surface-muted: rgba(15, 23, 42, 0.035);
--surface-subtle: rgba(15, 23, 42, 0.025);
--surface-subtle-border: rgba(15, 23, 42, 0.1);
--overlay-scrim: rgba(15, 23, 42, 0.34);
--nav-bg: rgba(255, 255, 255, 0.88);
--rail-bg: rgba(255, 255, 255, 0.72);
--shadow-soft: 0 6px 18px rgba(15, 23, 42, 0.06);
--shadow-strong: 0 18px 44px rgba(15, 23, 42, 0.12);
--shadow-popover: 0 14px 28px rgba(15, 23, 42, 0.12);
--inset-highlight: rgba(255, 255, 255, 0.75);
--admin-bg: #f7f8fb;
--admin-surface: #ffffff;
--admin-surface-2: #f1f5f9;
--admin-elev: #e8edf3;
--admin-border: rgba(15, 23, 42, 0.1);
--admin-border-strong: rgba(15, 23, 42, 0.18);
--admin-text: #0f172a;
--admin-muted: #64748b;
--admin-dim: #64748b;
--admin-chart-stroke: #065f46;
--admin-chart-fill: rgba(6, 95, 70, 0.22);
}
.theme-key-light .ui-spinner,
.theme-key-light .brand-mark-spinner {
color: inherit;
}
.theme-key-light .telegram-button-spinner {
border-color: rgba(255, 255, 255, 0.35);
border-top-color: #ffffff;
}
.theme-key-light .btn-primary,
.theme-key-light .admin-btn.admin-btn-primary,
.theme-key-light .admin-extend-control .admin-btn.admin-btn-primary {
background: color-mix(in srgb, var(--accent) 50%, #000000);
border-color: color-mix(in srgb, var(--accent) 42%, #000000);
color: #ffffff;
}
.theme-key-light .btn-primary:hover:not(:disabled),
.theme-key-light .admin-btn.admin-btn-primary:hover:not(:disabled),
.theme-key-light .admin-extend-control .admin-btn.admin-btn-primary:hover:not(:disabled) {
background: color-mix(in srgb, var(--accent) 52%, #000000);
}
.theme-key-light.app-shell {
background: var(--bg) !important;
}
.theme-key-light .phone-screen {
background: var(--bg);
}
/* Flatten Settings rows: no gradient sheen, no inset highlight that reads as a 3D bevel */
.theme-key-light .settings-row {
background: var(--panel);
box-shadow: none;
}
.theme-key-light .settings-row-linked {
background: var(--success-soft);
}
/* Avatar/profile card: bigger lift, but rows below have an opaque background and
stack above, so the shadow stays visually under them instead of bleeding through. */
.theme-key-light .settings-profile {
box-shadow:
0 10px 24px rgba(15, 23, 42, 0.10),
inset 0 1px 0 var(--inset-highlight);
}
.theme-key-light .settings-links-block {
position: relative;
z-index: 1;
}
/* New user-facing activation surfaces */
.theme-key-light .trial-offer-card,
.theme-key-light .trial-activation-card,
.theme-key-light .activation-success-dialog {
border-color: color-mix(in srgb, var(--accent) 24%, var(--border));
background: #ffffff;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
}
.theme-key-light .trial-card-head > svg,
.theme-key-light .dialog-title-icon {
color: color-mix(in srgb, var(--accent) 54%, #000000);
}
.theme-key-light .trial-card-facts span,
.theme-key-light .trial-activation-facts div {
border-color: rgba(15, 23, 42, 0.12);
background: rgba(15, 23, 42, 0.025);
}
/* Slightly stronger axis/grid contrast for the revenue chart on a light surface */
.theme-key-light .admin-revenue-svg-frame {
background: #ffffff;
}
/* Bonus section: drop accent color from body strongs; only the bonus-system heading
and explicitly-accent card headings stay tinted and they use the same darkened
accent technique as .btn-primary on light, so they remain readable on white. */
.theme-key-light .bonus-card strong {
color: var(--text);
}
.theme-key-light .bonus-card-head strong,
.theme-key-light .card-heading-accent {
color: color-mix(in srgb, var(--accent) 50%, #000000);
}
.theme-key-light .bonus-card-head > svg {
color: color-mix(in srgb, var(--accent) 50%, #000000);
}
/* Install guide theme surfaces */
.theme-key-light .install-platform-trigger,
.theme-key-light .install-app-button,
.theme-key-light .install-step,
.theme-key-light .install-subscription-card,
.theme-key-light .install-qr-wrap {
background: #ffffff;
border-color: rgba(15, 23, 42, 0.12);
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.055);
}
.theme-key-light .install-app-button.active {
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
background: color-mix(in srgb, var(--accent) 8%, #ffffff);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
}
.theme-key-light .install-platform-trigger:focus-visible,
.theme-key-light .install-platform-trigger[data-state="open"],
.theme-key-light .install-app-button:focus-visible {
border-color: color-mix(in srgb, var(--accent) 48%, var(--border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
body:has(.theme-key-light) .install-platform-content {
background: #ffffff;
border-color: rgba(15, 23, 42, 0.14);
box-shadow: 0 14px 28px rgba(15, 23, 42, 0.12);
}
body:has(.theme-key-light) .install-platform-item[data-highlighted],
body:has(.theme-key-light) .install-platform-item[data-selected] {
background: color-mix(in srgb, var(--accent) 9%, #ffffff);
}
.theme-key-light .install-step-icon,
.theme-key-light .install-subscription-header-icon {
background: color-mix(in srgb, var(--accent) 8%, #ffffff);
color: color-mix(in srgb, var(--accent) 55%, #000000);
}
.theme-key-light .install-qr-divider {
color: rgba(15, 23, 42, 0.24);
}
.theme-key-light .install-feature-star.attention-dot {
background: #f59e0b;
}
.theme-key-light .install-loading .ui-spinner {
color: color-mix(in srgb, var(--accent) 55%, #000000);
}
@@ -0,0 +1,16 @@
{
"key": "light",
"names": {
"ru": "Светлая",
"en": "Light"
},
"enabled": true,
"default": false,
"use_primary_accent": true,
"use_in_admin": true,
"css_file": "style.css",
"assets_version": 3,
"tokens": {
"color_scheme": "light"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Some files were not shown because too many files have changed in this diff Show More