Lumen / lumen_rich.py
SilverElixir
Throttle streaming catch-up edits and pace chunk sends
c019423
Raw History Blame Contribute Delete
7.58 kB
"""
lumen_rich.py — отправка сообщений: гости, Rich Messages, чанкинг.
"""
from __future__ import annotations
import contextlib
import hashlib
import logging
from typing import Any
from aiogram.enums import ParseMode
from aiogram.types import (
InlineQueryResultArticle,
InputRichMessage,
InputTextMessageContent,
Message,
ReplyParameters,
)
from lumen_formatting import (
_md_to_html,
_md_to_rich_html,
_split_text_chunks,
_strip_markdown,
_truncate_html_to_fit,
)
log = logging.getLogger("bot")
def is_guest_message(message: Message | dict) -> bool:
if isinstance(message, dict):
return bool(message.get("guest_query_id"))
return bool(getattr(message, "guest_query_id", None))
async def _answer_guest_text(message: Message, text: str) -> None:
import bot
qid = getattr(message, "guest_query_id", None)
if not qid:
return
payload = _truncate_html_to_fit(text, bot.TG_MAX_LEN)
res_id = hashlib.sha1(payload.encode("utf-8", errors="ignore")).hexdigest()[:32]
res_art = InlineQueryResultArticle(
id=res_id, title=bot._t(message.chat.id, "inline_answer_title"),
input_message_content=InputTextMessageContent(message_text=payload, parse_mode="HTML"),
)
try:
await bot.telegram_api_call("answerGuestQuery", {
"guest_query_id": str(qid),
"result": res_art.model_dump(exclude_none=True),
}, request_timeout=10)
except Exception as exc:
log.warning("[guest] Failed answering guest: %s", exc)
def _is_real_telegram_message(res: Any) -> bool:
"""Только реальный Message (int message_id): фейки тестов тихо уходят на legacy."""
return isinstance(res, Message)
async def _try_send_rich(message: Message, rich_html: str, *, is_first_chunk: bool, **kwargs: Any) -> Any:
"""Рич-отправка; при неуспехе — None и legacy. Трединг только первого чанка, иначе каждый кусок пиликает отдельно."""
import bot
if not bot.RICH_MESSAGES_ENABLED:
return None
sender = getattr(bot.bot, "send_rich_message", None)
if sender is None or message is None:
return None
kwargs.pop("parse_mode", None)
reply_parameters = kwargs.pop("reply_parameters", None)
if reply_parameters is None and is_first_chunk and isinstance(getattr(message, "message_id", None), int):
reply_parameters = ReplyParameters(message_id=message.message_id)
call_timeout = kwargs.pop("call_timeout", bot.TELEGRAM_REQUEST_TIMEOUT)
res = await bot._tg_call(
sender, chat_id=message.chat.id, rich_message=InputRichMessage(html=rich_html),
reply_parameters=reply_parameters, call_timeout=call_timeout, **kwargs,
)
return res if bot._is_real_telegram_message(res) else None
async def _try_edit_rich(msg: Message | None, rich_html: str, **kwargs: Any) -> bool:
"""Рич-правка; True только при успехе, иначе legacy. Фейки без int id отсекаются."""
import bot
if not bot.RICH_MESSAGES_ENABLED or msg is None:
return False
editor = getattr(bot.bot, "edit_message_text", None)
chat = getattr(msg, "chat", None)
chat_id = getattr(chat, "id", None)
message_id = getattr(msg, "message_id", None)
if editor is None or not isinstance(chat_id, int) or not isinstance(message_id, int):
return False
kwargs.pop("parse_mode", None)
call_timeout = kwargs.pop("call_timeout", 15.0)
res = await bot._tg_call(
editor, text=None, rich_message=InputRichMessage(html=rich_html),
chat_id=chat_id, message_id=message_id, call_timeout=call_timeout, **kwargs,
)
return bot._is_real_telegram_message(res)
async def _send_text(message: Message, text: str, parse_html: bool = True, **kwargs: Any) -> None:
import bot
if bot.is_guest_message(message):
await bot._answer_guest_text(message, text)
return
chunks = _split_text_chunks(text, bot.TG_MAX_LEN)
for i, chunk in enumerate(chunks):
chunk_kwargs = kwargs if i == len(chunks) - 1 else {k: v for k, v in kwargs.items() if k != "reply_markup"}
if i > 0:
# Пауза между кусками — против флуд-контроля за серию подряд.
await bot._typing_sleep(bot.STREAM_TYPING_TICK_SEC)
if parse_html:
# Сначала рич (таблицы/заголовки/математика), при любом неуспехе —
# обычный HTML-путь ниже. Reply threading — только у первого чанка.
rich_res = await bot._try_send_rich(message, _md_to_rich_html(chunk), is_first_chunk=(i == 0), **chunk_kwargs)
if rich_res is not None:
continue
if i == 0:
res = await bot._tg_call(
message.reply, _md_to_html(chunk) if parse_html else chunk,
call_timeout=bot.TELEGRAM_REQUEST_TIMEOUT,
parse_mode=ParseMode.HTML if parse_html else None,
**chunk_kwargs,
)
if res is None and parse_html:
res = await bot._tg_call(
message.reply, _strip_markdown(chunk),
call_timeout=bot.TELEGRAM_REQUEST_TIMEOUT,
parse_mode=None,
**chunk_kwargs,
)
else:
res = await bot._tg_call(
bot.bot.send_message,
chat_id=message.chat.id, text=_md_to_html(chunk) if parse_html else chunk,
call_timeout=bot.TELEGRAM_REQUEST_TIMEOUT,
parse_mode=ParseMode.HTML if parse_html else None,
**chunk_kwargs,
)
if res is None and parse_html:
await bot._tg_call(
bot.bot.send_message,
chat_id=message.chat.id, text=_strip_markdown(chunk),
call_timeout=bot.TELEGRAM_REQUEST_TIMEOUT,
parse_mode=None,
**chunk_kwargs,
)
async def _safe_reply(message: Message, text: str, parse_html: bool = True, **kwargs: Any) -> None:
import bot
await bot._send_text(message, text, parse_html=parse_html, **kwargs)
async def _delete_message_quietly(msg: Message | None) -> None:
if msg is None:
return
with contextlib.suppress(Exception):
await msg.delete()
async def _edit_message_quietly(msg: Message | None, text: str, **kwargs: Any) -> bool:
import bot
if msg is None:
return False
try:
# Порядок: рич → HTML → голый текст; таблицы видны только через рич.
if await bot._try_edit_rich(msg, _md_to_rich_html(text), **kwargs):
return True
kwargs.setdefault("parse_mode", ParseMode.HTML)
res = await bot._tg_call(msg.edit_text, _md_to_html(text), **kwargs)
if res is not None:
return True
kwargs["parse_mode"] = None
# Последний рубеж — голый текст БЕЗ markdown-синтаксиса (см.
# _strip_markdown): сырые `**` в чате хуже потери жирности.
res = await bot._tg_call(msg.edit_text, _strip_markdown(text), **kwargs)
return res is not None
except Exception:
return False