diff --git a/bot/handlers/commands/users/emoji.py b/bot/handlers/commands/users/emoji.py
new file mode 100644
index 0000000..4744966
--- /dev/null
+++ b/bot/handlers/commands/users/emoji.py
@@ -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'{emoji_char}'
+
+
+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"{idx}. Эмодзи: {emoji_char}\n"
+ f"📋 ID: {emoji_id}\n\n"
+ f"📝 HTML-код:\n"
+ f"{html_escaped}\n\n"
+ f"🎨 Превью: {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(
+ "❌ Используйте команду в ответ на сообщение\n\n"
+ "📝 Как использовать:\n"
+ "1. Ответьте на сообщение с премиум эмодзи\n"
+ "2. Напишите /emoji\n\n"
+ "💡 Бот извлечёт все кастомные эмодзи и покажет 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(
+ "⚠️ Кастомные эмодзи не найдены\n\n"
+ "В этом сообщении нет премиум эмодзи.\n\n"
+ "💡 Попробуйте ответить на сообщение с анимированными эмодзи",
+ 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"✨ НАЙДЕНО ЭМОДЗИ: {total}\n\n"
+ else:
+ header = f"✨ ПРОДОЛЖЕНИЕ ({page_num}/{total_pages})\n\n"
+
+ # Подвал только на последней странице
+ footer = (
+ "\n\n💡 Скопируйте HTML-код и используйте в своих сообщениях"
+ 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(
+ "❌ Ошибка извлечения эмодзи\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 = (
+ "🎨 РАБОТА С КАСТОМНЫМИ ЭМОДЗИ\n\n"
+ "📝 Команда /emoji\n"
+ "Извлекает ID премиум эмодзи из сообщения\n\n"
+ "🔧 Как использовать:\n"
+ "1️⃣ Ответьте на сообщение с эмодзи\n"
+ "2️⃣ Напишите /emoji\n"
+ "3️⃣ Скопируйте HTML-код\n\n"
+ "💻 Формат HTML-кода:\n"
+ '<tg-emoji emoji-id="ID">fallback</tg-emoji>\n\n'
+ "⚠️ Важно:\n"
+ '├─ Используйте parse_mode="HTML"\n'
+ "├─ Пользователи без Premium видят fallback\n"
+ "└─ Работает только с кастомными эмодзи\n\n"
+ "💡 Попробуйте отправить эмодзи и ответить командой /emoji"
+ )
+ await message.answer(text, parse_mode="HTML")