From 4394fa1b7bbac89ecc657ba2f59082d63345512d Mon Sep 17 00:00:00 2001 From: Ekaropolus Date: Tue, 16 Sep 2025 20:05:27 -0600 Subject: [PATCH] Render need handler --- pxy_bots/renderer.py | 84 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/pxy_bots/renderer.py b/pxy_bots/renderer.py index d6622ef..83f010c 100644 --- a/pxy_bots/renderer.py +++ b/pxy_bots/renderer.py @@ -8,9 +8,80 @@ from telegram.error import TelegramError logger = logging.getLogger(__name__) -# ... _build_keyboard stays the same ... + +def _build_keyboard(buttons: Optional[List[dict]]) -> Optional[InlineKeyboardMarkup]: + """ + Build an InlineKeyboardMarkup from a list of button specs. + + Supported kinds: + - open_url: {"label":"...", "kind":"open_url", "url":"https://..."} + - callback_api:{"label":"...", "kind":"callback_api", "action":"rerun", + "params": {...}, "state_token":"..."} + """ + if not buttons: + return None + + rows: List[List[InlineKeyboardButton]] = [] + current_row: List[InlineKeyboardButton] = [] + + for b in buttons: + kind = (b.get("kind") or "").lower() + label = b.get("label") or "…" + + if kind == "open_url": + url = b.get("url") + if not url: + logger.warning("renderer: open_url without url; skipping") + continue + btn = InlineKeyboardButton(text=label, url=url) + + elif kind == "callback_api": + # Keep callback_data tiny; prefer server-issued state_token. + if b.get("state_token"): + data = {"t": b["state_token"]} # compact key + else: + data = {"a": b.get("action"), "p": b.get("params") or {}} + try: + payload = json.dumps(data, separators=(",", ":"), ensure_ascii=False) + except Exception: + payload = '{"e":"bad"}' + # Telegram doc: 1–64 bytes recommended + if len(payload.encode("utf-8")) > 64: + logger.warning("renderer: callback_data too long (%sB); trimming", + len(payload.encode("utf-8"))) + payload = payload.encode("utf-8")[:64].decode("utf-8", errors="ignore") + btn = InlineKeyboardButton(text=label, callback_data=payload) + + else: + logger.warning("renderer: unknown button kind=%s; skipping", kind) + continue + + current_row.append(btn) + if len(current_row) >= 3: + rows.append(current_row) + current_row = [] + + if current_row: + rows.append(current_row) + + return InlineKeyboardMarkup(rows) if rows else None + async def render_spec(*, bot: Bot, chat_id: int, spec: Dict) -> List[Message]: + """ + Send messages according to a render_spec: + { + "messages": [ + {"type":"text","text":"..."}, + {"type":"photo","media_url":"https://...","caption":"..."} + ], + "buttons":[ ... ], # optional top-level buttons (attached to the LAST message) + "telemetry": {"run_id":"...", "cache_ttl_s":600}, + "schema_version": "render.v1" + } + + Returns the list of telegram.Message objects sent. + """ msgs = spec.get("messages") or [] top_buttons = spec.get("buttons") or None sent: List[Message] = [] @@ -32,7 +103,8 @@ async def render_spec(*, bot: Bot, chat_id: int, spec: Dict) -> List[Message]: if not (file_id or media_url): logger.warning("renderer: photo without file_id/media_url; skipping") continue - msg = await bot.send_photo(chat_id=chat_id, photo=file_id or media_url, caption=caption, reply_markup=kb) + msg = await bot.send_photo(chat_id=chat_id, photo=file_id or media_url, + caption=caption, reply_markup=kb) sent.append(msg) elif mtype == "document": @@ -42,7 +114,8 @@ async def render_spec(*, bot: Bot, chat_id: int, spec: Dict) -> List[Message]: if not (file_id or media_url): logger.warning("renderer: document without file_id/media_url; skipping") continue - msg = await bot.send_document(chat_id=chat_id, document=file_id or media_url, caption=caption, reply_markup=kb) + msg = await bot.send_document(chat_id=chat_id, document=file_id or media_url, + caption=caption, reply_markup=kb) sent.append(msg) elif mtype == "video": @@ -52,7 +125,8 @@ async def render_spec(*, bot: Bot, chat_id: int, spec: Dict) -> List[Message]: if not (file_id or media_url): logger.warning("renderer: video without file_id/media_url; skipping") continue - msg = await bot.send_video(chat_id=chat_id, video=file_id or media_url, caption=caption, reply_markup=kb) + msg = await bot.send_video(chat_id=chat_id, video=file_id or media_url, + caption=caption, reply_markup=kb) sent.append(msg) else: @@ -60,9 +134,7 @@ async def render_spec(*, bot: Bot, chat_id: int, spec: Dict) -> List[Message]: except TelegramError as te: logger.exception("renderer.telegram_error type=%s err=%s", mtype, te) - # continue to next message except Exception as e: logger.exception("renderer.unexpected type=%s err=%s", mtype, e) - # continue to next message return sent