- Rate-limit on brute-force: 5 failed attempts per 5 min blocks user
- Daily stacking limit: max 5 promo activations per 24h (in-memory + DB)
- Format validation: only alphanumeric/hyphen/underscore, 3-50 chars
Sliding window limiter: max 3 /start calls per 60 seconds per user.
Runs before the general 0.5s throttle. Shows cooldown timer on block.
Lazy cleanup of start_buckets when size exceeds 500 entries.
The 'Back' button on tariff extend confirmation sends
tariff_extend:{id} without a period segment, which crashed
select_tariff_extend_period with IndexError on parts[2].
Now redirects to show_tariff_extend when period is missing.
Remnawave already sends user.not_connected webhooks, making the
monitoring service's 1h/24h trial inactivity checks redundant.
The monitoring checks caused false positives because they relied on
traffic_used_gb which may not be synced in real-time.
Removed:
- _check_trial_inactivity_notifications from monitoring cycle
- _send_trial_inactive_notification method
- trial_inactive_1h / trial_inactive_24h notification settings
- Admin UI toggles and preview buttons for these notifications
Changed callback_data from 'subscription' (no handler) to 'menu_subscription'
(registered handler) in _get_subscription_keyboard and _get_traffic_keyboard.
In cabinet mode the button opens a WebApp URL so the bug was invisible,
but in default MAIN_MENU_MODE the callback went unhandled.
- Keyboard now shows "Возобновить" for disabled/expired daily tariffs
instead of useless "Приостановить"
- resume_daily_subscription handles EXPIRED→ACTIVE (not only DISABLED)
- Pause handler detects inactive status and calls resume directly
- subscription_extend redirects daily tariffs to subscription info
(daily tariffs have no period_prices, so extend page was empty)
- Add enabled flag to hide/show each button section in main menu
- Add per-locale custom labels (ru, en, ua, zh, fa) for button text
- Deep-copy nested labels dict in cache to prevent reference leaks
- Validate label entries from DB (type + locale key checks)
- Use selective merge in PATCH handler instead of blind .update()
Allow admins to set buttons to Telegram's default style with no color
override. Refactors style resolution from or-chain to explicit if/elif/else
so that 'default' does not fall through to global config or hardcoded defaults.
Add cabinet admin API for configuring button colors (primary/success/danger)
and custom emoji IDs per menu section (home, subscription, balance, referral,
support, info, admin). Styles are stored as JSON in system_settings and cached
in-process for fast resolution.
Style resolution chain: explicit param > per-section DB > global config > defaults.
- Rename mode from 'text' to 'cabinet' (text/text_only/minimal kept as aliases)
- Add build_cabinet_url() for joining MINIAPP_CUSTOM_URL with section paths
- Cabinet main menu now has section-specific buttons: subscription, balance,
referral, support, info — each opens the corresponding cabinet page
- Add CALLBACK_TO_CABINET_PATH mapping for automatic deep-linking from
callback_data to cabinet routes (/subscription, /balance, /referral, etc.)
- Unmapped callback_data gracefully falls back to regular Telegram callbacks
- Add startup validation warning when cabinet mode is active without MINIAPP_CUSTOM_URL
- Update admin broadcast buttons with section-specific routing
- Backward compatible: is_text_main_menu_mode() kept as alias for is_cabinet_mode()
- tickets.py: remove ENABLE_LOGO_MODE branches that used edit_message_caption
on text messages (prompt is always text, not photo with caption)
- webhook_service: add db.rollback() before retrying DB ops in _handle_user_deleted
when subscription was cascade-deleted, catch PendingRollbackError alongside StaleDataError
- Full CRUD + broadcast/unpin/activate/deactivate endpoints
- Admin auth required on all endpoints (get_current_admin_user)
- Broadcast cooldown (60s) on all mass operation endpoints
- Cached Bot singleton to prevent aiohttp session leaks
- Guard against deleting active pinned messages (409 Conflict)
- Route ordering: /active/* before /{message_id}/* to prevent path conflicts
- Pydantic schemas with proper validation (file_id max_length=255)
- Add retry loop with backoff to _unpin_message_for_user (max 3 attempts)
- Add TelegramRetryAfter handling in _send_and_pin_message (unpin + send phases)
- Fix missing failed_count increment when all broadcast retries exhaust (for/else)
- Remove dead code in unpin_active_pinned_message (unreachable TelegramRetryAfter catch)
- Harden sanitize_html: allowlist URI schemes (http/https/tg/mailto/tel), whitelist
tag attributes, strip all attrs from tags without explicit whitelist, full HTML
entity decoding via html.unescape
Catch TelegramBadRequest with "query is too old" before generic Exception handler
to prevent it from being logged as error and triggering error reports.
- YooKassa: SELECT FOR UPDATE on payment row to prevent concurrent double-processing
- subtract_user_balance: row locking to prevent concurrent balance race conditions
- subtract_user_balance: transaction creation before commit for atomicity
- subscription renewal: compensating refund if extend_subscription fails after charge
- StaleDataError: use savepoint instead of full rollback to protect parent transaction
Remove all modem purchase/management code:
- Delete modem handler, service, and tests
- Remove modem button from keyboards and admin panel
- Remove modem pricing from calculations
- Remove modem REST API endpoint and schemas
- Remove modem decorator, config settings, and notification formatting
- Keep DB column and migration for backwards compatibility
When squads are deleted from the RemnaWave panel and servers are synced,
the bot cleaned subscription connected_squads but left stale UUIDs in
tariff.allowed_squads. This caused errors when users tried to purchase
or extend subscriptions with tariffs referencing deleted squads.
Now sync_with_remnawave also removes stale UUIDs from all tariffs.
When a user is deleted from the panel, the subscription may already be
cascade-deleted by the time the webhook handler tries to decrement
server counters. This caused StaleDataError followed by
PendingRollbackError when accessing subscription.id in the error handler.
- Save subscription.id before DB operations to avoid lazy load after rollback
- Catch StaleDataError explicitly and rollback the session
- Re-fetch subscription/user after potential rollback in _handle_user_deleted
- Skip subscription cleanup if it was already cascade-deleted
Unverified email users could not change their email (e.g. to fix a typo)
because the endpoint required email_verified=True. Now unverified emails
are replaced directly without code verification, and a new verification
email is sent to the updated address.
reset_user_subscription and reset_trial endpoints did not clean up
subscription_servers rows before deleting the subscription, causing
ForeignKeyViolationError on subscription_servers.subscription_id_fkey.
Also fixed the same missing cleanup in user_service.hard_delete_user.
- Cabinet API: use get_traffic_topup_packages() instead of
get_traffic_packages() in classic mode endpoints (lines 622, 727, 2410)
to prevent infinite free traffic exploit via initial-purchase packages
- WATA service: add retry logic for 429 rate limit responses with
Retry-After parsing from header and response body, up to 2 retries,
downgrade 429 from error to warning log level
- Wrap unprotected add/remove_user_to/from_servers calls in try/except
in miniapp.py and cabinet subscription.py to prevent 500 errors
- Fix is_tariff_change to include classic-to-tariff transitions
(subscription.tariff_id=None → new tariff_id) so purchased traffic
is properly reset when switching modes
extend_subscription was unconditionally resetting purchased_traffic_gb
and deleting TrafficPurchase records whenever traffic_limit_gb was passed,
even when extending the same tariff (not changing). Now only resets
on actual tariff change (is_tariff_change=True), preserving purchased
traffic on same-tariff extensions.
add_user_to_servers and remove_user_from_servers were calling
db.commit() internally, breaking transaction atomicity for all
callers that perform additional operations afterward. Changed to
db.flush() so the caller controls the commit boundary.
- backup: add DATE column parsing in restore, use is_file() in delete_backup
- updates: add missing callback.answer() in show_updates_menu early return
- webhook: add server counter decrement and SubscriptionServer cleanup on user deletion, use single commit
Previously only status was set to expired and remnawave_uuid cleared.
Now also clears subscription_url, subscription_crypto_link,
remnawave_short_uuid, and connected_squads so the bot correctly
shows no active subscription after panel deletion.