228 lines
8.4 KiB
Python
228 lines
8.4 KiB
Python
"""
|
||
Обработчик команды /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")
|