Получение 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