Files
remnawave-bedolaga-telegram…/app/handlers/admin/privacy_policy.py
T
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

525 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import html
from datetime import datetime
import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import User
from app.localization.texts import get_texts
from app.services.privacy_policy_service import PrivacyPolicyService
from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
from app.utils.validators import get_html_help_text, validate_html_tags
logger = structlog.get_logger(__name__)
def _format_timestamp(value: datetime | None) -> str:
if not value:
return ''
try:
return value.strftime('%d.%m.%Y %H:%M')
except Exception:
return ''
async def _build_overview(
db_user: User,
db: AsyncSession,
):
texts = get_texts(db_user.language)
policy = await PrivacyPolicyService.get_policy(
db,
db_user.language,
fallback=False,
)
normalized_language = PrivacyPolicyService.normalize_language(db_user.language)
has_content = bool(policy and policy.content and policy.content.strip())
description = texts.t(
'ADMIN_PRIVACY_POLICY_DESCRIPTION',
'Политика конфиденциальности отображается в разделе «Инфо».',
)
status_text = texts.t(
'ADMIN_PRIVACY_POLICY_STATUS_DISABLED',
'⚠️ Показ политики выключен или текст отсутствует.',
)
if policy and policy.is_enabled and has_content:
status_text = texts.t(
'ADMIN_PRIVACY_POLICY_STATUS_ENABLED',
'✅ Политика активна и показывается пользователям.',
)
elif policy and policy.is_enabled:
status_text = texts.t(
'ADMIN_PRIVACY_POLICY_STATUS_ENABLED_EMPTY',
'⚠️ Политика включена, но текст пуст — пользователи её не увидят.',
)
updated_at = _format_timestamp(getattr(policy, 'updated_at', None))
updated_block = ''
if updated_at:
updated_block = texts.t(
'ADMIN_PRIVACY_POLICY_UPDATED_AT',
'Последнее обновление: {timestamp}',
).format(timestamp=updated_at)
preview_block = texts.t(
'ADMIN_PRIVACY_POLICY_PREVIEW_EMPTY',
'Текст ещё не задан.',
)
if has_content:
preview_title = texts.t(
'ADMIN_PRIVACY_POLICY_PREVIEW_TITLE',
'<b>Превью текста:</b>',
)
preview_raw = policy.content.strip()
preview_trimmed = preview_raw[:400]
if len(preview_raw) > 400:
preview_trimmed += '...'
preview_block = f'{preview_title}\n<code>{html.escape(preview_trimmed)}</code>'
language_block = texts.t(
'ADMIN_PRIVACY_POLICY_LANGUAGE',
'Язык: <code>{lang}</code>',
).format(lang=normalized_language)
header = texts.t(
'ADMIN_PRIVACY_POLICY_HEADER',
'🛡️ <b>Политика конфиденциальности</b>',
)
actions_prompt = texts.t(
'ADMIN_PRIVACY_POLICY_ACTION_PROMPT',
'Выберите действие:',
)
message_parts = [
header,
description,
language_block,
status_text,
]
if updated_block:
message_parts.append(updated_block)
message_parts.append(preview_block)
message_parts.append(actions_prompt)
overview_text = '\n\n'.join(part for part in message_parts if part)
buttons: list[list[types.InlineKeyboardButton]] = []
buttons.append(
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_EDIT_BUTTON',
'✏️ Изменить текст',
),
callback_data='admin_privacy_policy_edit',
)
]
)
if has_content:
buttons.append(
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_VIEW_BUTTON',
'👀 Просмотреть текущий текст',
),
callback_data='admin_privacy_policy_view',
)
]
)
toggle_text = texts.t(
'ADMIN_PRIVACY_POLICY_ENABLE_BUTTON',
'✅ Включить показ',
)
if policy and policy.is_enabled:
toggle_text = texts.t(
'ADMIN_PRIVACY_POLICY_DISABLE_BUTTON',
'🚫 Отключить показ',
)
buttons.append(
[
types.InlineKeyboardButton(
text=toggle_text,
callback_data='admin_privacy_policy_toggle',
)
]
)
buttons.append(
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_HTML_HELP',
'️ HTML помощь',
),
callback_data='admin_privacy_policy_help',
)
]
)
buttons.append(
[
types.InlineKeyboardButton(
text=texts.BACK,
callback_data='admin_submenu_settings',
)
]
)
return overview_text, types.InlineKeyboardMarkup(inline_keyboard=buttons), policy
@admin_required
@error_handler
async def show_privacy_policy_management(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
):
overview_text, markup, _ = await _build_overview(db_user, db)
await callback.message.edit_text(
overview_text,
reply_markup=markup,
)
await callback.answer()
@admin_required
@error_handler
async def toggle_privacy_policy(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
):
texts = get_texts(db_user.language)
updated_policy = await PrivacyPolicyService.toggle_enabled(db, db_user.language)
logger.info(
'Админ %s переключил показ политики конфиденциальности: %s',
db_user.telegram_id,
'enabled' if updated_policy.is_enabled else 'disabled',
)
status_message = (
texts.t('ADMIN_PRIVACY_POLICY_ENABLED', '✅ Политика включена')
if updated_policy.is_enabled
else texts.t('ADMIN_PRIVACY_POLICY_DISABLED', '🚫 Политика отключена')
)
overview_text, markup, _ = await _build_overview(db_user, db)
await callback.message.edit_text(
overview_text,
reply_markup=markup,
)
await callback.answer(status_message)
@admin_required
@error_handler
async def start_edit_privacy_policy(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext,
db: AsyncSession,
):
texts = get_texts(db_user.language)
policy = await PrivacyPolicyService.get_policy(
db,
db_user.language,
fallback=False,
)
current_preview = ''
if policy and policy.content:
preview = policy.content.strip()[:400]
if len(policy.content.strip()) > 400:
preview += '...'
current_preview = (
texts.t(
'ADMIN_PRIVACY_POLICY_CURRENT_PREVIEW',
'Текущий текст (превью):',
)
+ f'\n<code>{html.escape(preview)}</code>\n\n'
)
prompt = texts.t(
'ADMIN_PRIVACY_POLICY_EDIT_PROMPT',
'Отправьте новый текст политики конфиденциальности. Допускается HTML-разметка.',
)
hint = texts.t(
'ADMIN_PRIVACY_POLICY_EDIT_HINT',
'Используйте /html_help для справки по тегам.',
)
message_text = (
f'📝 <b>{texts.t("ADMIN_PRIVACY_POLICY_EDIT_TITLE", "Редактирование политики")}</b>\n\n'
f'{current_preview}{prompt}\n\n{hint}'
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_HTML_HELP',
'️ HTML помощь',
),
callback_data='admin_privacy_policy_help',
)
],
[
types.InlineKeyboardButton(
text=texts.t('ADMIN_PRIVACY_POLICY_CANCEL', '❌ Отмена'),
callback_data='admin_privacy_policy_cancel',
)
],
]
)
await callback.message.edit_text(message_text, reply_markup=keyboard)
await state.set_state(AdminStates.editing_privacy_policy)
await callback.answer()
@admin_required
@error_handler
async def cancel_edit_privacy_policy(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext,
db: AsyncSession,
):
await state.clear()
overview_text, markup, _ = await _build_overview(db_user, db)
await callback.message.edit_text(
overview_text,
reply_markup=markup,
)
await callback.answer()
@admin_required
@error_handler
async def process_privacy_policy_edit(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession,
):
texts = get_texts(db_user.language)
new_text = message.text or ''
if len(new_text) > 4000:
await message.answer(
texts.t(
'ADMIN_PRIVACY_POLICY_TOO_LONG',
'❌ Текст политики слишком длинный. Максимум 4000 символов.',
)
)
return
is_valid, error_message = validate_html_tags(new_text)
if not is_valid:
await message.answer(
texts.t(
'ADMIN_PRIVACY_POLICY_HTML_ERROR',
'❌ Ошибка в HTML: {error}',
).format(error=error_message)
)
return
await PrivacyPolicyService.save_policy(db, db_user.language, new_text)
logger.info(
'Админ обновил текст политики конфиденциальности (символов)',
telegram_id=db_user.telegram_id,
new_text_count=len(new_text),
)
await state.clear()
success_text = texts.t(
'ADMIN_PRIVACY_POLICY_SAVED',
'✅ Политика конфиденциальности обновлена.',
)
reply_markup = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_BACK_BUTTON',
'⬅️ К настройкам политики',
),
callback_data='admin_privacy_policy',
)
]
]
)
await message.answer(success_text, reply_markup=reply_markup)
@admin_required
@error_handler
async def view_privacy_policy(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
):
texts = get_texts(db_user.language)
policy = await PrivacyPolicyService.get_policy(
db,
db_user.language,
fallback=False,
)
if not policy or not policy.content or not policy.content.strip():
await callback.answer(
texts.t(
'ADMIN_PRIVACY_POLICY_PREVIEW_EMPTY_ALERT',
'Текст политики пока не задан.',
),
show_alert=True,
)
return
content = policy.content.strip()
truncated = False
max_length = 3800
if len(content) > max_length:
content = content[: max_length - 3] + '...'
truncated = True
header = texts.t(
'ADMIN_PRIVACY_POLICY_VIEW_TITLE',
'👀 <b>Текущий текст политики</b>',
)
note = ''
if truncated:
note = texts.t(
'ADMIN_PRIVACY_POLICY_VIEW_TRUNCATED',
'\n\n⚠️ Текст сокращён для отображения. Полную версию увидят пользователи в меню.',
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_BACK_BUTTON',
'⬅️ К настройкам политики',
),
callback_data='admin_privacy_policy',
)
],
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_EDIT_BUTTON',
'✏️ Изменить текст',
),
callback_data='admin_privacy_policy_edit',
)
],
]
)
await callback.message.edit_text(
f'{header}\n\n{content}{note}',
reply_markup=keyboard,
)
await callback.answer()
@admin_required
@error_handler
async def show_privacy_policy_html_help(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext,
db: AsyncSession,
):
texts = get_texts(db_user.language)
help_text = get_html_help_text()
current_state = await state.get_state()
buttons: list[list[types.InlineKeyboardButton]] = []
if current_state == AdminStates.editing_privacy_policy.state:
buttons.append(
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_RETURN_TO_EDIT',
'⬅️ Назад к редактированию',
),
callback_data='admin_privacy_policy_edit',
)
]
)
buttons.append(
[
types.InlineKeyboardButton(
text=texts.t(
'ADMIN_PRIVACY_POLICY_BACK_BUTTON',
'⬅️ К настройкам политики',
),
callback_data='admin_privacy_policy',
)
]
)
await callback.message.edit_text(
help_text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=buttons),
)
await callback.answer()
def register_handlers(dp: Dispatcher) -> None:
dp.callback_query.register(
show_privacy_policy_management,
F.data == 'admin_privacy_policy',
)
dp.callback_query.register(
toggle_privacy_policy,
F.data == 'admin_privacy_policy_toggle',
)
dp.callback_query.register(
start_edit_privacy_policy,
F.data == 'admin_privacy_policy_edit',
)
dp.callback_query.register(
cancel_edit_privacy_policy,
F.data == 'admin_privacy_policy_cancel',
)
dp.callback_query.register(
view_privacy_policy,
F.data == 'admin_privacy_policy_view',
)
dp.callback_query.register(
show_privacy_policy_html_help,
F.data == 'admin_privacy_policy_help',
)
dp.message.register(
process_privacy_policy_edit,
AdminStates.editing_privacy_policy,
)