"""Copy user messages without losing Telegram entities, including custom emoji.""" import asyncio from functools import wraps from aiogram.types import Message, MessageEntity COPY_TIMEOUT = 15.0 def bounded_copy(func): @wraps(func) async def wrapped(*args, **kwargs): async with asyncio.timeout(COPY_TIMEOUT): return await func(*args, **kwargs) return wrapped def utf16_length(text: str) -> int: """Telegram entity offsets count UTF-16 code units, not Python characters.""" return len(text.encode("utf-16-le")) // 2 @bounded_copy async def copy_preserving_entities(message: Message, recipient_id: int, sender_name: str | None = None): if sender_name is None: # Native copy keeps the original body/caption and its entities unchanged. return await message.copy_to(recipient_id) prefix = "📨 " header = f"{prefix}{sender_name}:\n\n" header_entities = [MessageEntity(type="bold", offset=utf16_length(prefix), length=utf16_length(sender_name + ":"))] offset = utf16_length(header) has_caption = any((message.photo, message.video, message.document, message.animation, message.audio, message.voice)) body = message.text if message.text is not None else (message.caption or "") original = message.entities if message.text is not None else message.caption_entities entities = header_entities + [entity.model_copy(update={"offset": entity.offset + offset}) for entity in original or ()] limit = 4096 if message.text is not None else 1024 if (message.text is not None or has_caption) and offset + utf16_length(body) <= limit: if message.text is not None: return await message.bot.send_message(recipient_id, header + body, entities=entities, parse_mode=None) return await message.copy_to(recipient_id, caption=header + body, caption_entities=entities, parse_mode=None) # Do not trim a long body/caption: send the header separately and copy the original. await message.bot.send_message(recipient_id, header, entities=header_entities, parse_mode=None) return await message.copy_to(recipient_id)