This commit is contained in:
2026-02-20 03:12:47 +07:00
parent 5d350d0885
commit 5aca4e8438
23 changed files with 2291 additions and 1330 deletions

View File

@@ -14,23 +14,28 @@ __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]:
"""
Извлекает все кастомные эмодзи из сообщения.
Args:
message: Сообщение для анализа
Извлекает все кастомные эмодзи из сообщения (текст + подпись).
Returns:
Список словарей с информацией об эмодзи
Список словарей: {"char": str, "id": str, "offset": int}
"""
if not message.entities and not message.caption_entities:
return []
# Определяем текст и entities
text = message.text or message.caption
entities = message.entities or message.caption_entities
@@ -38,44 +43,76 @@ def extract_custom_emojis(message: Message) -> list[dict]:
return []
custom_emojis = []
for entity in entities:
if entity.type == "custom_emoji":
# Извлекаем символ эмодзи
emoji_char = text[entity.offset:entity.offset + entity.length]
emoji_char = _utf16_slice(text, entity.offset, entity.length)
custom_emojis.append({
"char": emoji_char,
"id": entity.custom_emoji_id,
"offset": entity.offset
"offset": entity.offset,
})
return custom_emojis
def format_emoji_html(emoji_char: str, emoji_id: str) -> str:
"""
Форматирует эмодзи в HTML-тег.
Args:
emoji_char: Символ эмодзи (fallback)
emoji_id: ID кастомного эмодзи
Returns:
HTML-строка
"""
return f'<tg-emoji emoji-id="{emoji_id}">{emoji_char}</tg-emoji>'
def escape_html(text: str) -> str:
"""Экранирует HTML символы"""
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
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(
@@ -83,14 +120,6 @@ def escape_html(text: str) -> str:
IsAdmin()
)
async def emoji_extractor_cmd(message: Message) -> None:
"""
Извлекает кастомные эмодзи из сообщения.
Доступно только администраторам.
Использование: /emoji (в ответ на сообщение)
"""
# Проверяем, что команда в ответ на сообщение
if not message.reply_to_message:
await message.answer(
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
@@ -98,66 +127,57 @@ async def emoji_extractor_cmd(message: Message) -> None:
"1. Ответьте на сообщение с премиум эмодзи\n"
"2. Напишите <code>/emoji</code>\n\n"
"💡 <i>Бот извлечёт все кастомные эмодзи и покажет HTML-код</i>",
parse_mode="HTML"
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"
parse_mode="HTML",
)
return
# === ФОРМИРУЕМ ОТВЕТ ===
total = len(custom_emojis)
pages = build_pages(custom_emojis)
total_pages = len(pages)
output = f"✨ <b>НАЙДЕНО ЭМОДЗИ: {len(custom_emojis)}</b>\n\n"
for idx, emoji_data in enumerate(custom_emojis, 1):
emoji_char = emoji_data["char"]
emoji_id = emoji_data["id"]
output += f"<b>{idx}.</b> Эмодзи: {emoji_char}\n"
output += f"📋 <b>ID:</b> <code>{emoji_id}</code>\n\n"
# HTML-код (экранированный для отображения)
html_code = format_emoji_html(emoji_char, emoji_id)
html_escaped = escape_html(html_code)
output += f"📝 <b>HTML-код:</b>\n"
output += f"<code>{html_escaped}</code>\n\n"
# Пример использования
output += f"🎨 <b>Превью:</b> {html_code}\n"
if idx < len(custom_emojis):
output += "\n" + "" * 30 + "\n\n"
output += "💡 <i>Скопируйте HTML-код и используйте в своих сообщениях</i>"
# Создаём клавиатуру
ikb = InlineKeyboardBuilder()
ikb.button(text="✖️ Закрыть", callback_data="emoji_close")
# Отправляем
try:
await message.answer(
text=output,
parse_mode="HTML",
reply_markup=ikb.as_markup()
)
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"Извлечено {len(custom_emojis)} кастомных эмодзи админом {message.from_user.id}",
log_type="EMOJI_EXTRACT"
f"Извлечено {total} кастомных эмодзи ({total_pages} стр.) "
f"админом {message.from_user.id}",
log_type="EMOJI_EXTRACT",
)
except Exception as e:
@@ -165,7 +185,7 @@ async def emoji_extractor_cmd(message: Message) -> None:
await message.answer(
"❌ <b>Ошибка извлечения эмодзи</b>\n\n"
"Попробуйте позже или обратитесь к разработчику.",
parse_mode="HTML"
parse_mode="HTML",
)
@@ -173,7 +193,6 @@ async def emoji_extractor_cmd(message: Message) -> None:
@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("✅ Закрыто")
@@ -189,9 +208,6 @@ async def emoji_close_callback(callback) -> None:
IsAdmin()
)
async def emoji_help_cmd(message: Message) -> None:
"""
Справка по работе с кастомными эмодзи.
"""
text = (
"🎨 <b>РАБОТА С КАСТОМНЫМИ ЭМОДЗИ</b>\n\n"
"📝 <b>Команда /emoji</b>\n"
@@ -201,15 +217,11 @@ async def emoji_help_cmd(message: Message) -> None:
"2⃣ Напишите <code>/emoji</code>\n"
"3⃣ Скопируйте HTML-код\n\n"
"💻 <b>Формат HTML-кода:</b>\n"
"<code>&lt;tg-emoji emoji-id=\"ID\"&gt;fallback&lt;/tg-emoji&gt;</code>\n\n"
"📌 <b>Пример использования в коде:</b>\n"
"<code>text = 'Привет &lt;tg-emoji emoji-id=\"5368324170671202286\"&gt;👍&lt;/tg-emoji&gt;'\n"
"await message.answer(text, parse_mode=\"HTML\")</code>\n\n"
'<code>&lt;tg-emoji emoji-id="ID"&gt;fallback&lt;/tg-emoji&gt;</code>\n\n'
"⚠️ <b>Важно:</b>\n"
"├─ Используйте <code>parse_mode=\"HTML\"</code>\n"
'├─ Используйте <code>parse_mode="HTML"</code>\n'
"├─ Пользователи без Premium видят fallback\n"
"└─ Работает только с кастомными эмодзи\n\n"
"💡 <i>Попробуйте отправить эмодзи и ответить командой /emoji</i>"
)
await message.answer(text, parse_mode="HTML")