Files
PrimoGuardBot-/bot/handlers/commands/users/emoji.py
2026-02-20 03:12:47 +07:00

228 lines
8.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Обработчик команды /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("&", "&amp;")
.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(
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>&lt;tg-emoji emoji-id="ID"&gt;fallback&lt;/tg-emoji&gt;</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")