mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-14 16:10:27 +00:00
442 lines
18 KiB
Python
442 lines
18 KiB
Python
import logging
|
||
import re
|
||
from aiogram import Dispatcher, types, F
|
||
from aiogram.fsm.context import FSMContext
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.database.models import User
|
||
from app.states import AdminStates
|
||
from app.keyboards.admin import get_welcome_text_keyboard, get_admin_main_keyboard
|
||
from app.utils.decorators import admin_required, error_handler
|
||
from app.database.crud.welcome_text import (
|
||
get_active_welcome_text,
|
||
set_welcome_text,
|
||
get_current_welcome_text_or_default,
|
||
get_available_placeholders,
|
||
get_current_welcome_text_settings,
|
||
toggle_welcome_text_status
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def validate_html_tags(text: str) -> tuple[bool, str]:
|
||
"""
|
||
Проверяет HTML-теги в тексте на соответствие требованиям Telegram API.
|
||
|
||
Args:
|
||
text: Текст для проверки
|
||
|
||
Returns:
|
||
Кортеж из (валидно ли, сообщение об ошибке или None)
|
||
"""
|
||
# Поддерживаемые теги в parse_mode="HTML" для Telegram API
|
||
allowed_tags = {
|
||
'b', 'strong', # жирный
|
||
'i', 'em', # курсив
|
||
'u', 'ins', # подчеркнуто
|
||
's', 'strike', 'del', # зачеркнуто
|
||
'code', # моноширинный для коротких фрагментов
|
||
'pre', # моноширинный блок кода
|
||
'a' # ссылки
|
||
}
|
||
|
||
# Убираем плейсхолдеры из строки перед проверкой тегов
|
||
# Плейсхолдеры имеют формат {ключ}, и не являются тегами
|
||
placeholder_pattern = r'\{[^{}]+\}'
|
||
clean_text = re.sub(placeholder_pattern, '', text)
|
||
|
||
# Находим все открывающие и закрывающие теги
|
||
tag_pattern = r'<(/?)([a-zA-Z]+)(\s[^>]*)?>'
|
||
tags_with_pos = [(m.group(1), m.group(2), m.group(3), m.start(), m.end()) for m in re.finditer(tag_pattern, clean_text)]
|
||
|
||
for closing, tag, attrs, start_pos, end_pos in tags_with_pos:
|
||
tag_lower = tag.lower()
|
||
|
||
# Проверяем, является ли тег поддерживаемым
|
||
if tag_lower not in allowed_tags:
|
||
return False, f"Неподдерживаемый HTML-тег: <{tag}>. Используйте только теги: {', '.join(sorted(allowed_tags))}"
|
||
|
||
# Проверяем атрибуты для тега <a>
|
||
if tag_lower == 'a':
|
||
if closing:
|
||
continue # Для закрывающего тега не нужно проверять атрибуты
|
||
if not attrs:
|
||
return False, "Тег <a> должен содержать атрибут href, например: <a href='URL'>ссылка</a>"
|
||
|
||
# Проверяем, что есть атрибут href
|
||
if 'href=' not in attrs.lower():
|
||
return False, "Тег <a> должен содержать атрибут href, например: <a href='URL'>ссылка</a>"
|
||
|
||
# Проверяем формат URL
|
||
href_match = re.search(r'href\s*=\s*[\'"]([^\'"]+)[\'"]', attrs, re.IGNORECASE)
|
||
if href_match:
|
||
url = href_match.group(1)
|
||
# Проверяем, что URL начинается с поддерживаемой схемы
|
||
if not re.match(r'^https?://|^tg://', url, re.IGNORECASE):
|
||
return False, f"URL в теге <a> должен начинаться с http://, https:// или tg://. Найдено: {url}"
|
||
else:
|
||
return False, "Не удалось извлечь URL из атрибута href тега <a>"
|
||
|
||
# Проверяем парность тегов с использованием стека
|
||
stack = []
|
||
for closing, tag, attrs, start_pos, end_pos in tags_with_pos:
|
||
tag_lower = tag.lower()
|
||
|
||
if tag_lower not in allowed_tags:
|
||
continue
|
||
|
||
if closing:
|
||
# Это закрывающий тег
|
||
if not stack:
|
||
return False, f"Лишний закрывающий тег: </{tag}>"
|
||
|
||
last_opening_tag = stack.pop()
|
||
if last_opening_tag.lower() != tag_lower:
|
||
return False, f"Тег </{tag}> не соответствует открывающему тегу <{last_opening_tag}>"
|
||
else:
|
||
# Это открывающий тег
|
||
stack.append(tag)
|
||
|
||
# Если остались незакрытые теги
|
||
if stack:
|
||
unclosed_tags = ", ".join([f"<{tag}>" for tag in stack])
|
||
return False, f"Незакрытые теги: {unclosed_tags}"
|
||
|
||
return True, None
|
||
|
||
def get_telegram_formatting_info() -> str:
|
||
return """
|
||
📝 <b>Поддерживаемые теги форматирования:</b>
|
||
|
||
• <code><b>жирный текст</b></code> → <b>жирный текст</b>
|
||
• <code><i>курсив</i></code> → <i>курсив</i>
|
||
• <code><u>подчеркнутый</u></code> → <u>подчеркнутый</u>
|
||
• <code><s>зачеркнутый</s></code> → <s>зачеркнутый</s>
|
||
• <code><code>моноширинный</code></code> → <code>моноширинный</code>
|
||
• <code><pre>блок кода</pre></code> → многострочный код
|
||
• <code><a href="URL">ссылка</a></code> → ссылка
|
||
|
||
⚠️ <b>ВНИМАНИЕ:</b> Используйте ТОЛЬКО указанные выше теги!
|
||
Любые другие HTML-теги не поддерживаются и будут отображаться как обычный текст.
|
||
|
||
❌ <b>НЕ используйте:</b> <div>, <span>, <p>, <br>, <h1>-<h6>, <img> и другие HTML-теги.
|
||
"""
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_welcome_text_panel(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
status_emoji = "🟢" if welcome_settings['is_enabled'] else "🔴"
|
||
status_text = "включено" if welcome_settings['is_enabled'] else "отключено"
|
||
|
||
await callback.message.edit_text(
|
||
f"👋 Управление приветственным текстом\n\n"
|
||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||
f"Здесь вы можете управлять текстом, который показывается новым пользователям после регистрации.\n\n"
|
||
f"💡 Доступные плейсхолдеры для автозамены:",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
|
||
parse_mode="HTML"
|
||
)
|
||
await callback.answer()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def toggle_welcome_text(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
new_status = await toggle_welcome_text_status(db, db_user.id)
|
||
|
||
status_emoji = "🟢" if new_status else "🔴"
|
||
status_text = "включено" if new_status else "отключено"
|
||
action_text = "включены" if new_status else "отключены"
|
||
|
||
await callback.message.edit_text(
|
||
f"👋 Управление приветственным текстом\n\n"
|
||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||
f"✅ Приветственные сообщения {action_text}!\n\n"
|
||
f"Здесь вы можете управлять текстом, который показывается новым пользователям после регистрации.\n\n"
|
||
f"💡 Доступные плейсхолдеры для автозамены:",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, new_status),
|
||
parse_mode="HTML"
|
||
)
|
||
await callback.answer()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_current_welcome_text(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
current_text = welcome_settings['text']
|
||
is_enabled = welcome_settings['is_enabled']
|
||
|
||
if not welcome_settings['id']:
|
||
status = "📝 Используется стандартный текст:"
|
||
else:
|
||
status = "📝 Текущий приветственный текст:"
|
||
|
||
status_emoji = "🟢" if is_enabled else "🔴"
|
||
status_text = "включено" if is_enabled else "отключено"
|
||
|
||
placeholders = get_available_placeholders()
|
||
placeholders_text = "\n".join([f"• <code>{key}</code> - {desc}" for key, desc in placeholders.items()])
|
||
|
||
await callback.message.edit_text(
|
||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||
f"{status}\n\n"
|
||
f"<code>{current_text}</code>\n\n"
|
||
f"💡 Доступные плейсхолдеры:\n{placeholders_text}",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, is_enabled),
|
||
parse_mode="HTML"
|
||
)
|
||
await callback.answer()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_placeholders_help(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
placeholders = get_available_placeholders()
|
||
placeholders_text = "\n".join([f"• <code>{key}</code>\n {desc}" for key, desc in placeholders.items()])
|
||
|
||
help_text = (
|
||
"💡 Доступные плейсхолдеры для автозамены:\n\n"
|
||
f"{placeholders_text}\n\n"
|
||
"📌 Примеры использования:\n"
|
||
"• <code>Привет, {user_name}! Добро пожаловать!</code>\n"
|
||
"• <code>Здравствуйте, {first_name}! Рады видеть вас!</code>\n"
|
||
"• <code>Привет, {username}! Спасибо за регистрацию!</code>\n\n"
|
||
"При отсутствии данных пользователя используется слово 'друг'."
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
help_text,
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
|
||
parse_mode="HTML"
|
||
)
|
||
await callback.answer()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_formatting_help(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
formatting_info = get_telegram_formatting_info()
|
||
|
||
await callback.message.edit_text(
|
||
formatting_info,
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
|
||
parse_mode="HTML"
|
||
)
|
||
await callback.answer()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_edit_welcome_text(
|
||
callback: types.CallbackQuery,
|
||
state: FSMContext,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
current_text = welcome_settings['text']
|
||
|
||
placeholders = get_available_placeholders()
|
||
placeholders_text = "\n".join([f"• <code>{key}</code> - {desc}" for key, desc in placeholders.items()])
|
||
|
||
await callback.message.edit_text(
|
||
f"📝 Редактирование приветственного текста\n\n"
|
||
f"Текущий текст:\n"
|
||
f"<code>{current_text}</code>\n\n"
|
||
f"💡 Доступные плейсхолдеры:\n{placeholders_text}\n\n"
|
||
f"Отправьте новый текст:",
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await state.set_state(AdminStates.editing_welcome_text)
|
||
await callback.answer()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_welcome_text_edit(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
new_text = message.text.strip()
|
||
|
||
if len(new_text) < 10:
|
||
await message.answer("❌ Текст слишком короткий! Минимум 10 символов.")
|
||
return
|
||
|
||
if len(new_text) > 4000:
|
||
await message.answer("❌ Текст слишком длинный! Максимум 4000 символов.")
|
||
return
|
||
|
||
# Проверяем HTML-теги на валидность
|
||
is_valid, error_msg = validate_html_tags(new_text)
|
||
if not is_valid:
|
||
await message.answer(f"❌ Ошибка в HTML-разметке:\n\n{error_msg}")
|
||
return
|
||
|
||
success = await set_welcome_text(db, new_text, db_user.id)
|
||
|
||
if success:
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
status_emoji = "🟢" if welcome_settings['is_enabled'] else "🔴"
|
||
status_text = "включено" if welcome_settings['is_enabled'] else "отключено"
|
||
|
||
placeholders = get_available_placeholders()
|
||
placeholders_text = "\n".join([f"• <code>{key}</code>" for key in placeholders.keys()])
|
||
|
||
await message.answer(
|
||
f"✅ Приветственный текст успешно обновлен!\n\n"
|
||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||
f"Новый текст:\n"
|
||
f"<code>{new_text}</code>\n\n"
|
||
f"💡 Будут заменяться плейсхолдеры: {placeholders_text}",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
|
||
parse_mode="HTML"
|
||
)
|
||
else:
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
await message.answer(
|
||
"❌ Ошибка при сохранении текста. Попробуйте еще раз.",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled'])
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def reset_welcome_text(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
default_text = await get_current_welcome_text_or_default()
|
||
success = await set_welcome_text(db, default_text, db_user.id)
|
||
|
||
if success:
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
status_emoji = "🟢" if welcome_settings['is_enabled'] else "🔴"
|
||
status_text = "включено" if welcome_settings['is_enabled'] else "отключено"
|
||
|
||
await callback.message.edit_text(
|
||
f"✅ Приветственный текст сброшен на стандартный!\n\n"
|
||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||
f"Стандартный текст:\n"
|
||
f"<code>{default_text}</code>\n\n"
|
||
f"💡 Плейсхолдер <code>{{user_name}}</code> будет заменяться на имя пользователя",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
|
||
parse_mode="HTML"
|
||
)
|
||
else:
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
await callback.message.edit_text(
|
||
"❌ Ошибка при сбросе текста. Попробуйте еще раз.",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled'])
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_preview_welcome_text(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
from app.database.crud.welcome_text import get_welcome_text_for_user
|
||
|
||
class TestUser:
|
||
def __init__(self):
|
||
self.first_name = "Иван"
|
||
self.username = "test_user"
|
||
|
||
test_user = TestUser()
|
||
preview_text = await get_welcome_text_for_user(db, test_user)
|
||
|
||
welcome_settings = await get_current_welcome_text_settings(db)
|
||
|
||
if preview_text:
|
||
await callback.message.edit_text(
|
||
f"👁️ Предварительный просмотр\n\n"
|
||
f"Как будет выглядеть текст для пользователя 'Иван' (@test_user):\n\n"
|
||
f"<code>{preview_text}</code>",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
|
||
parse_mode="HTML"
|
||
)
|
||
else:
|
||
await callback.message.edit_text(
|
||
f"👁️ Предварительный просмотр\n\n"
|
||
f"🔴 Приветственные сообщения отключены.\n"
|
||
f"Новые пользователи не будут получать приветственный текст после регистрации.",
|
||
reply_markup=get_welcome_text_keyboard(db_user.language, welcome_settings['is_enabled']),
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
def register_welcome_text_handlers(dp: Dispatcher):
|
||
dp.callback_query.register(
|
||
show_welcome_text_panel,
|
||
F.data == "welcome_text_panel"
|
||
)
|
||
|
||
dp.callback_query.register(
|
||
toggle_welcome_text,
|
||
F.data == "toggle_welcome_text"
|
||
)
|
||
|
||
dp.callback_query.register(
|
||
show_current_welcome_text,
|
||
F.data == "show_welcome_text"
|
||
)
|
||
|
||
dp.callback_query.register(
|
||
show_placeholders_help,
|
||
F.data == "show_placeholders_help"
|
||
)
|
||
|
||
dp.callback_query.register(
|
||
show_formatting_help,
|
||
F.data == "show_formatting_help"
|
||
)
|
||
|
||
dp.callback_query.register(
|
||
show_preview_welcome_text,
|
||
F.data == "preview_welcome_text"
|
||
)
|
||
|
||
dp.callback_query.register(
|
||
start_edit_welcome_text,
|
||
F.data == "edit_welcome_text"
|
||
)
|
||
|
||
dp.callback_query.register(
|
||
reset_welcome_text,
|
||
F.data == "reset_welcome_text"
|
||
)
|
||
|
||
dp.message.register(
|
||
process_welcome_text_edit,
|
||
AdminStates.editing_welcome_text
|
||
)
|