Получение html-разметки бота
This commit is contained in:
227
bot/handlers/commands/users/emoji.py
Normal file
227
bot/handlers/commands/users/emoji.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Обработчик команды /emoji для извлечения ID премиум эмодзи
|
||||||
|
"""
|
||||||
|
from aiogram import Router
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.types import Message
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
from bot.filters.admin import IsAdmin
|
||||||
|
from configs import settings, COMMANDS
|
||||||
|
from middleware.loggers import logger
|
||||||
|
|
||||||
|
__all__ = ("router",)
|
||||||
|
|
||||||
|
router: Router = Router(name="emoji_extractor_router")
|
||||||
|
|
||||||
|
MAX_MSG_LEN = 3800 # Безопасный лимит (4096 - запас)
|
||||||
|
SEPARATOR = "\n" + "─" * 30 + "\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||||
|
|
||||||
|
def _utf16_slice(text: str, offset: int, length: int) -> str:
|
||||||
|
"""
|
||||||
|
Корректно извлекает подстроку с учётом UTF-16 смещений Telegram API.
|
||||||
|
Telegram передаёт offset/length в UTF-16 code units, а не в Unicode codepoints.
|
||||||
|
"""
|
||||||
|
encoded = text.encode("utf-16-le")
|
||||||
|
return encoded[offset * 2 : (offset + length) * 2].decode("utf-16-le")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_custom_emojis(message: Message) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Извлекает все кастомные эмодзи из сообщения (текст + подпись).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список словарей: {"char": str, "id": str, "offset": int}
|
||||||
|
"""
|
||||||
|
text = message.text or message.caption
|
||||||
|
entities = message.entities or message.caption_entities
|
||||||
|
|
||||||
|
if not text or not entities:
|
||||||
|
return []
|
||||||
|
|
||||||
|
custom_emojis = []
|
||||||
|
for entity in entities:
|
||||||
|
if entity.type == "custom_emoji":
|
||||||
|
emoji_char = _utf16_slice(text, entity.offset, entity.length)
|
||||||
|
custom_emojis.append({
|
||||||
|
"char": emoji_char,
|
||||||
|
"id": entity.custom_emoji_id,
|
||||||
|
"offset": entity.offset,
|
||||||
|
})
|
||||||
|
|
||||||
|
return custom_emojis
|
||||||
|
|
||||||
|
|
||||||
|
def format_emoji_html(emoji_char: str, emoji_id: str) -> str:
|
||||||
|
return f'<tg-emoji emoji-id="{emoji_id}">{emoji_char}</tg-emoji>'
|
||||||
|
|
||||||
|
|
||||||
|
def escape_html(text: str) -> str:
|
||||||
|
return (
|
||||||
|
text.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_emoji_block(idx: int, emoji_data: dict, is_last: bool) -> str:
|
||||||
|
"""Формирует текстовый блок для одного эмодзи."""
|
||||||
|
emoji_char = emoji_data["char"]
|
||||||
|
emoji_id = emoji_data["id"]
|
||||||
|
html_code = format_emoji_html(emoji_char, emoji_id)
|
||||||
|
html_escaped = escape_html(html_code)
|
||||||
|
|
||||||
|
block = (
|
||||||
|
f"<b>{idx}.</b> Эмодзи: {emoji_char}\n"
|
||||||
|
f"📋 <b>ID:</b> <code>{emoji_id}</code>\n\n"
|
||||||
|
f"📝 <b>HTML-код:</b>\n"
|
||||||
|
f"<code>{html_escaped}</code>\n\n"
|
||||||
|
f"🎨 <b>Превью:</b> {html_code}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_last:
|
||||||
|
block += SEPARATOR
|
||||||
|
|
||||||
|
return block
|
||||||
|
|
||||||
|
|
||||||
|
def build_pages(custom_emojis: list[dict]) -> list[str]:
|
||||||
|
"""
|
||||||
|
Разбивает список эмодзи на страницы, каждая не длиннее MAX_MSG_LEN.
|
||||||
|
Возвращает список готовых HTML-строк для отправки.
|
||||||
|
"""
|
||||||
|
total = len(custom_emojis)
|
||||||
|
pages: list[str] = []
|
||||||
|
current_page = ""
|
||||||
|
|
||||||
|
for idx, emoji_data in enumerate(custom_emojis, 1):
|
||||||
|
is_last = (idx == total)
|
||||||
|
block = _build_emoji_block(idx, emoji_data, is_last)
|
||||||
|
|
||||||
|
if current_page and len(current_page) + len(block) > MAX_MSG_LEN:
|
||||||
|
pages.append(current_page)
|
||||||
|
current_page = block
|
||||||
|
else:
|
||||||
|
current_page += block
|
||||||
|
|
||||||
|
if current_page:
|
||||||
|
pages.append(current_page)
|
||||||
|
|
||||||
|
return pages
|
||||||
|
|
||||||
|
|
||||||
|
# ================= КОМАНДА /EMOJI =================
|
||||||
|
|
||||||
|
@router.message(
|
||||||
|
Command(*COMMANDS.get("emoji", ["emoji"]), prefix=settings.PREFIX, ignore_case=True),
|
||||||
|
IsAdmin()
|
||||||
|
)
|
||||||
|
async def emoji_extractor_cmd(message: Message) -> None:
|
||||||
|
if not message.reply_to_message:
|
||||||
|
await message.answer(
|
||||||
|
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
|
||||||
|
"📝 Как использовать:\n"
|
||||||
|
"1. Ответьте на сообщение с премиум эмодзи\n"
|
||||||
|
"2. Напишите <code>/emoji</code>\n\n"
|
||||||
|
"💡 <i>Бот извлечёт все кастомные эмодзи и покажет HTML-код</i>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
replied_message = message.reply_to_message
|
||||||
|
custom_emojis = extract_custom_emojis(replied_message)
|
||||||
|
|
||||||
|
if not custom_emojis:
|
||||||
|
await message.answer(
|
||||||
|
"⚠️ <b>Кастомные эмодзи не найдены</b>\n\n"
|
||||||
|
"В этом сообщении нет премиум эмодзи.\n\n"
|
||||||
|
"💡 <i>Попробуйте ответить на сообщение с анимированными эмодзи</i>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
total = len(custom_emojis)
|
||||||
|
pages = build_pages(custom_emojis)
|
||||||
|
total_pages = len(pages)
|
||||||
|
|
||||||
|
ikb = InlineKeyboardBuilder()
|
||||||
|
ikb.button(text="✖️ Закрыть", callback_data="emoji_close")
|
||||||
|
|
||||||
|
try:
|
||||||
|
for page_num, page_content in enumerate(pages, 1):
|
||||||
|
# Заголовок только на первой странице
|
||||||
|
if page_num == 1:
|
||||||
|
header = f"✨ <b>НАЙДЕНО ЭМОДЗИ: {total}</b>\n\n"
|
||||||
|
else:
|
||||||
|
header = f"✨ <b>ПРОДОЛЖЕНИЕ ({page_num}/{total_pages})</b>\n\n"
|
||||||
|
|
||||||
|
# Подвал только на последней странице
|
||||||
|
footer = (
|
||||||
|
"\n\n💡 <i>Скопируйте HTML-код и используйте в своих сообщениях</i>"
|
||||||
|
if page_num == total_pages
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кнопка закрытия только на последней странице
|
||||||
|
markup = ikb.as_markup() if page_num == total_pages else None
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
text=header + page_content + footer,
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=markup,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Извлечено {total} кастомных эмодзи ({total_pages} стр.) "
|
||||||
|
f"админом {message.from_user.id}",
|
||||||
|
log_type="EMOJI_EXTRACT",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки эмодзи: {e}", log_type="ERROR")
|
||||||
|
await message.answer(
|
||||||
|
"❌ <b>Ошибка извлечения эмодзи</b>\n\n"
|
||||||
|
"Попробуйте позже или обратитесь к разработчику.",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ================= ОБРАБОТЧИК КНОПКИ ЗАКРЫТИЯ =================
|
||||||
|
|
||||||
|
@router.callback_query(lambda c: c.data == "emoji_close", IsAdmin())
|
||||||
|
async def emoji_close_callback(callback) -> None:
|
||||||
|
try:
|
||||||
|
await callback.message.delete()
|
||||||
|
await callback.answer("✅ Закрыто")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка удаления сообщения с эмодзи: {e}", log_type="ERROR")
|
||||||
|
await callback.answer("❌ Не удалось удалить", show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ================= ДОПОЛНИТЕЛЬНАЯ КОМАНДА /EMOJIHELP =================
|
||||||
|
|
||||||
|
@router.message(
|
||||||
|
Command(*COMMANDS.get("emojihelp", ["emojihelp"]), prefix=settings.PREFIX, ignore_case=True),
|
||||||
|
IsAdmin()
|
||||||
|
)
|
||||||
|
async def emoji_help_cmd(message: Message) -> None:
|
||||||
|
text = (
|
||||||
|
"🎨 <b>РАБОТА С КАСТОМНЫМИ ЭМОДЗИ</b>\n\n"
|
||||||
|
"📝 <b>Команда /emoji</b>\n"
|
||||||
|
"Извлекает ID премиум эмодзи из сообщения\n\n"
|
||||||
|
"🔧 <b>Как использовать:</b>\n"
|
||||||
|
"1️⃣ Ответьте на сообщение с эмодзи\n"
|
||||||
|
"2️⃣ Напишите <code>/emoji</code>\n"
|
||||||
|
"3️⃣ Скопируйте HTML-код\n\n"
|
||||||
|
"💻 <b>Формат HTML-кода:</b>\n"
|
||||||
|
'<code><tg-emoji emoji-id="ID">fallback</tg-emoji></code>\n\n'
|
||||||
|
"⚠️ <b>Важно:</b>\n"
|
||||||
|
'├─ Используйте <code>parse_mode="HTML"</code>\n'
|
||||||
|
"├─ Пользователи без Premium видят fallback\n"
|
||||||
|
"└─ Работает только с кастомными эмодзи\n\n"
|
||||||
|
"💡 <i>Попробуйте отправить эмодзи и ответить командой /emoji</i>"
|
||||||
|
)
|
||||||
|
await message.answer(text, parse_mode="HTML")
|
||||||
Reference in New Issue
Block a user