Files
remnawave-bedolaga-telegram…/app/utils/button_styles_cache.py
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
  via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
  to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
  so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
  270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
  for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
  style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
  and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
  to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
  switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
  inline.py, and admin/payments.py
2026-02-16 09:18:12 +03:00

122 lines
4.8 KiB
Python

"""Lightweight in-process cache for per-section cabinet button styles.
Avoids circular imports between ``cabinet.routes`` and ``app.utils.miniapp_buttons``
by keeping the cache and its helpers in a dedicated module.
"""
import json
import structlog
from app.database.database import AsyncSessionLocal
logger = structlog.get_logger(__name__)
# ---- Defaults per section ------------------------------------------------
DEFAULT_BUTTON_STYLES: dict[str, dict] = {
'home': {'style': 'primary', 'icon_custom_emoji_id': '', 'enabled': True, 'labels': {}},
'subscription': {'style': 'success', 'icon_custom_emoji_id': '', 'enabled': True, 'labels': {}},
'balance': {'style': 'primary', 'icon_custom_emoji_id': '', 'enabled': True, 'labels': {}},
'referral': {'style': 'success', 'icon_custom_emoji_id': '', 'enabled': True, 'labels': {}},
'support': {'style': 'primary', 'icon_custom_emoji_id': '', 'enabled': True, 'labels': {}},
'info': {'style': 'primary', 'icon_custom_emoji_id': '', 'enabled': True, 'labels': {}},
'admin': {'style': 'danger', 'icon_custom_emoji_id': '', 'enabled': True, 'labels': {}},
}
BOT_LOCALES = ('ru', 'en', 'ua', 'zh', 'fa')
SECTIONS = list(DEFAULT_BUTTON_STYLES.keys())
# Map callback_data values to their logical section name.
CALLBACK_TO_SECTION: dict[str, str] = {
'menu_profile_unavailable': 'home',
'back_to_menu': 'home',
'menu_subscription': 'subscription',
'subscription': 'subscription',
'subscription_extend': 'subscription',
'subscription_upgrade': 'subscription',
'subscription_connect': 'subscription',
'subscription_resume_checkout': 'subscription',
'return_to_saved_cart': 'subscription',
'menu_buy': 'subscription',
'buy_traffic': 'subscription',
'menu_balance': 'balance',
'balance_topup': 'balance',
'menu_referrals': 'referral',
'menu_referral': 'referral',
'menu_support': 'support',
'menu_info': 'info',
'admin_panel': 'admin',
}
# DB key used for storage.
BUTTON_STYLES_KEY = 'CABINET_BUTTON_STYLES'
# Valid Telegram Bot API style values.
VALID_STYLES = frozenset({'primary', 'success', 'danger'})
# All style values accepted by the admin API ('default' = no color, Telegram default).
ALLOWED_STYLE_VALUES = VALID_STYLES | {'default'}
# ---- Module-level cache ---------------------------------------------------
_cached_styles: dict[str, dict] | None = None
def _deep_copy_styles(source: dict[str, dict]) -> dict[str, dict]:
"""Return a deep copy of styles dict (copies nested ``labels`` dicts)."""
return {section: {**cfg, 'labels': dict(cfg.get('labels', {}))} for section, cfg in source.items()}
def get_cached_button_styles() -> dict[str, dict]:
"""Return the current merged config (DB overrides + defaults).
If the cache has not been loaded yet, returns defaults.
"""
if _cached_styles is not None:
return _deep_copy_styles(_cached_styles)
return _deep_copy_styles(DEFAULT_BUTTON_STYLES)
async def load_button_styles_cache() -> dict[str, dict]:
"""Load button styles from DB and refresh the module cache.
Called at bot startup and after admin updates via the cabinet API.
"""
global _cached_styles
merged = _deep_copy_styles(DEFAULT_BUTTON_STYLES)
try:
from sqlalchemy import select
from app.database.models import SystemSetting
async with AsyncSessionLocal() as session:
result = await session.execute(select(SystemSetting).where(SystemSetting.key == BUTTON_STYLES_KEY))
setting = result.scalar_one_or_none()
if setting and setting.value:
db_data: dict = json.loads(setting.value)
for section, overrides in db_data.items():
if section in merged and isinstance(overrides, dict):
if overrides.get('style') in ALLOWED_STYLE_VALUES:
merged[section]['style'] = overrides['style']
if isinstance(overrides.get('icon_custom_emoji_id'), str):
merged[section]['icon_custom_emoji_id'] = overrides['icon_custom_emoji_id']
if isinstance(overrides.get('enabled'), bool):
merged[section]['enabled'] = overrides['enabled']
if isinstance(overrides.get('labels'), dict):
merged[section]['labels'] = {
k: v
for k, v in overrides['labels'].items()
if isinstance(k, str) and isinstance(v, str) and k in BOT_LOCALES
}
except Exception:
logger.exception('Failed to load button styles from DB, using defaults')
_cached_styles = merged
logger.info('Button styles cache loaded', list=list(merged.keys()))
return merged