import re
import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.rules import clear_all_rules, create_or_update_rules, get_current_rules_content
from app.database.models import User
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
def _safe_preview(html_text: str, limit: int = 500) -> str:
"""Создаёт превью текста, безопасно обрезая HTML-теги."""
plain = re.sub(r'<[^>]+>', '', html_text)
if len(plain) <= limit:
return plain
return plain[:limit] + '...'
logger = structlog.get_logger(__name__)
@admin_required
@error_handler
async def show_rules_management(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
text = """
📋 Управление правилами сервиса
Текущие правила показываются пользователям при регистрации и в главном меню.
Выберите действие:
"""
keyboard = [
[types.InlineKeyboardButton(text='📝 Редактировать правила', callback_data='admin_edit_rules')],
[types.InlineKeyboardButton(text='👀 Просмотр правил', callback_data='admin_view_rules')],
[types.InlineKeyboardButton(text='🗑️ Очистить правила', callback_data='admin_clear_rules')],
[types.InlineKeyboardButton(text='ℹ️ Помощь по HTML', callback_data='admin_rules_help')],
[types.InlineKeyboardButton(text='⬅️ Назад', callback_data='admin_submenu_settings')],
]
await callback.message.edit_text(text, reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard))
await callback.answer()
@admin_required
@error_handler
async def view_current_rules(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
try:
current_rules = await get_current_rules_content(db, db_user.language)
is_valid, error_msg = validate_html_tags(current_rules)
warning = ''
if not is_valid:
warning = f'\n\n⚠️ Внимание: В правилах найдена ошибка HTML: {error_msg}'
await callback.message.edit_text(
f'📋 Текущие правила сервиса\n\n{current_rules}{warning}',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='✏️ Редактировать', callback_data='admin_edit_rules')],
[types.InlineKeyboardButton(text='🗑️ Очистить', callback_data='admin_clear_rules')],
[types.InlineKeyboardButton(text='⬅️ Назад', callback_data='admin_rules')],
]
),
)
await callback.answer()
except Exception as e:
logger.error('Ошибка при показе правил', error=e)
await callback.message.edit_text(
'❌ Ошибка при загрузке правил. Возможно, в тексте есть некорректные HTML теги.',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='🗑️ Очистить правила', callback_data='admin_clear_rules')],
[types.InlineKeyboardButton(text='⬅️ Назад', callback_data='admin_rules')],
]
),
)
await callback.answer()
@admin_required
@error_handler
async def start_edit_rules(callback: types.CallbackQuery, db_user: User, state: FSMContext, db: AsyncSession):
try:
current_rules = await get_current_rules_content(db, db_user.language)
preview = _safe_preview(current_rules, 500)
text = (
'✏️ Редактирование правил\n\n'
f'Текущие правила:\n{preview}\n\n'
'Отправьте новый текст правил сервиса.\n\n'
'Поддерживается HTML разметка. Все теги будут проверены перед сохранением.\n\n'
'💡 Совет: Нажмите /html_help для просмотра поддерживаемых тегов'
)
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='ℹ️ HTML помощь', callback_data='admin_rules_help')],
[types.InlineKeyboardButton(text='❌ Отмена', callback_data='admin_rules')],
]
),
)
await state.set_state(AdminStates.editing_rules_page)
await callback.answer()
except Exception as e:
logger.error('Ошибка при начале редактирования правил', error=e)
await callback.answer('❌ Ошибка при загрузке правил для редактирования', show_alert=True)
@admin_required
@error_handler
async def process_rules_edit(message: types.Message, db_user: User, state: FSMContext, db: AsyncSession):
new_rules = message.text
if len(new_rules) > 4000:
await message.answer('❌ Текст правил слишком длинный (максимум 4000 символов)')
return
is_valid, error_msg = validate_html_tags(new_rules)
if not is_valid:
await message.answer(
f'❌ Ошибка в HTML разметке:\n{error_msg}\n\n'
f'Пожалуйста, исправьте ошибки и отправьте текст заново.\n\n'
f'💡 Используйте /html_help для просмотра правильного синтаксиса',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='ℹ️ HTML помощь', callback_data='admin_rules_help')],
[types.InlineKeyboardButton(text='❌ Отмена', callback_data='admin_rules')],
]
),
)
return
try:
preview_text = f'📋 Предварительный просмотр новых правил:\n\n{new_rules}\n\n'
preview_text += '⚠️ Внимание! Новые правила будут показываться всем пользователям.\n\n'
preview_text += 'Сохранить изменения?'
if len(preview_text) > 4000:
preview_text = (
'📋 Предварительный просмотр новых правил:\n\n'
f'{_safe_preview(new_rules, 500)}\n\n'
f'⚠️ Внимание! Новые правила будут показываться всем пользователям.\n\n'
f'Текст правил: {len(new_rules)} символов\n'
f'Сохранить изменения?'
)
await message.answer(
preview_text,
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(text='✅ Сохранить', callback_data='admin_save_rules'),
types.InlineKeyboardButton(text='❌ Отмена', callback_data='admin_rules'),
]
]
),
)
await state.update_data(new_rules=new_rules)
except Exception as e:
logger.error('Ошибка при показе превью правил', error=e)
await message.answer(
'⚠️ Подтверждение сохранения правил\n\n'
f'Новые правила готовы к сохранению ({len(new_rules)} символов).\n'
f'HTML теги проверены и корректны.\n\n'
f'Сохранить изменения?',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(text='✅ Сохранить', callback_data='admin_save_rules'),
types.InlineKeyboardButton(text='❌ Отмена', callback_data='admin_rules'),
]
]
),
)
await state.update_data(new_rules=new_rules)
@admin_required
@error_handler
async def save_rules(callback: types.CallbackQuery, db_user: User, state: FSMContext, db: AsyncSession):
data = await state.get_data()
new_rules = data.get('new_rules')
if not new_rules:
await callback.answer('❌ Ошибка: текст правил не найден', show_alert=True)
return
is_valid, error_msg = validate_html_tags(new_rules)
if not is_valid:
await callback.message.edit_text(
f'❌ Ошибка при сохранении:\n{error_msg}\n\nПравила не были сохранены из-за ошибок в HTML разметке.',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='🔄 Попробовать снова', callback_data='admin_edit_rules')],
[types.InlineKeyboardButton(text='📋 К правилам', callback_data='admin_rules')],
]
),
)
await state.clear()
await callback.answer()
return
try:
await create_or_update_rules(db=db, content=new_rules, language=db_user.language)
from app.localization.texts import clear_rules_cache
clear_rules_cache()
from app.localization.texts import refresh_rules_cache
await refresh_rules_cache(db_user.language)
await callback.message.edit_text(
'✅ Правила сервиса успешно обновлены!\n\n'
'✓ Новые правила сохранены в базе данных\n'
'✓ HTML теги проверены и корректны\n'
'✓ Кеш правил очищен и обновлен\n'
'✓ Правила будут показываться пользователям\n\n'
f'📊 Размер текста: {len(new_rules)} символов',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='👀 Просмотреть', callback_data='admin_view_rules')],
[types.InlineKeyboardButton(text='📋 К правилам', callback_data='admin_rules')],
]
),
)
await state.clear()
logger.info('Правила сервиса обновлены администратором', telegram_id=db_user.telegram_id)
await callback.answer()
except Exception as e:
logger.error('Ошибка сохранения правил', error=e)
await callback.message.edit_text(
'❌ Ошибка при сохранении правил\n\nПроизошла ошибка при записи в базу данных. Попробуйте еще раз.',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='🔄 Попробовать снова', callback_data='admin_save_rules')],
[types.InlineKeyboardButton(text='📋 К правилам', callback_data='admin_rules')],
]
),
)
await callback.answer()
@admin_required
@error_handler
async def clear_rules_confirmation(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
await callback.message.edit_text(
'🗑️ Очистка правил сервиса\n\n'
'⚠️ ВНИМАНИЕ! Вы собираетесь полностью удалить все правила сервиса.\n\n'
'После очистки пользователи будут видеть стандартные правила по умолчанию.\n\n'
'Это действие нельзя отменить. Продолжить?',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(text='✅ Да, очистить', callback_data='admin_confirm_clear_rules'),
types.InlineKeyboardButton(text='❌ Отмена', callback_data='admin_rules'),
]
]
),
)
await callback.answer()
@admin_required
@error_handler
async def confirm_clear_rules(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
try:
await clear_all_rules(db, db_user.language)
from app.localization.texts import clear_rules_cache
clear_rules_cache()
await callback.message.edit_text(
'✅ Правила успешно очищены!\n\n'
'✓ Все пользовательские правила удалены\n'
'✓ Теперь используются стандартные правила\n'
'✓ Кеш правил очищен\n\n'
'Пользователи будут видеть правила по умолчанию.',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='📝 Создать новые', callback_data='admin_edit_rules')],
[types.InlineKeyboardButton(text='👀 Посмотреть текущие', callback_data='admin_view_rules')],
[types.InlineKeyboardButton(text='📋 К правилам', callback_data='admin_rules')],
]
),
)
logger.info('Правила очищены администратором', telegram_id=db_user.telegram_id)
await callback.answer()
except Exception as e:
logger.error('Ошибка при очистке правил', error=e)
await callback.answer('❌ Ошибка при очистке правил', show_alert=True)
@admin_required
@error_handler
async def show_html_help(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
help_text = get_html_help_text()
await callback.message.edit_text(
f'ℹ️ Справка по HTML форматированию\n\n{help_text}',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='📝 Редактировать правила', callback_data='admin_edit_rules')],
[types.InlineKeyboardButton(text='⬅️ Назад', callback_data='admin_rules')],
]
),
)
await callback.answer()
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_rules_management, F.data == 'admin_rules')
dp.callback_query.register(view_current_rules, F.data == 'admin_view_rules')
dp.callback_query.register(start_edit_rules, F.data == 'admin_edit_rules')
dp.callback_query.register(save_rules, F.data == 'admin_save_rules')
dp.callback_query.register(clear_rules_confirmation, F.data == 'admin_clear_rules')
dp.callback_query.register(confirm_clear_rules, F.data == 'admin_confirm_clear_rules')
dp.callback_query.register(show_html_help, F.data == 'admin_rules_help')
dp.message.register(process_rules_edit, AdminStates.editing_rules_page)