This commit is contained in:
parent
0eb2b393f2
commit
4dde3f74ee
131
pxy_bots/renderer.py
Normal file
131
pxy_bots/renderer.py
Normal file
@ -0,0 +1,131 @@
|
||||
# pxy_bots/renderer.py
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Bot, Message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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 button")
|
||||
continue
|
||||
btn = InlineKeyboardButton(text=label, url=url)
|
||||
elif kind == "callback_api":
|
||||
# Keep callback_data small. Prefer server-provided state_token when available.
|
||||
data = {}
|
||||
if b.get("state_token"):
|
||||
data = {"t": b["state_token"]} # compact key to minimize size
|
||||
else:
|
||||
# fallback: tiny payload (risk hitting 64B limit if it grows)
|
||||
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 limit: 1–64 bytes; try to keep it tiny
|
||||
if len(payload.encode("utf-8")) > 64:
|
||||
logger.warning("renderer: callback_data too long (%s bytes); 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] = []
|
||||
|
||||
for i, m in enumerate(msgs):
|
||||
mtype = (m.get("type") or "text").lower()
|
||||
|
||||
# Per-message buttons override top-level
|
||||
kb = _build_keyboard(m.get("buttons") or (top_buttons if i == len(msgs) - 1 else None))
|
||||
|
||||
if mtype == "text":
|
||||
text = m.get("text") or ""
|
||||
msg = await bot.send_message(chat_id=chat_id, text=text, reply_markup=kb)
|
||||
sent.append(msg)
|
||||
|
||||
elif mtype == "photo":
|
||||
# Prefer file_id if provided, else media_url (Telegram can fetch by URL)
|
||||
file_id = m.get("file_id")
|
||||
media_url = m.get("media_url")
|
||||
caption = m.get("caption") or None
|
||||
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)
|
||||
sent.append(msg)
|
||||
|
||||
elif mtype == "document":
|
||||
file_id = m.get("file_id")
|
||||
media_url = m.get("media_url")
|
||||
caption = m.get("caption") or None
|
||||
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)
|
||||
sent.append(msg)
|
||||
|
||||
elif mtype == "video":
|
||||
file_id = m.get("file_id")
|
||||
media_url = m.get("media_url")
|
||||
caption = m.get("caption") or None
|
||||
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)
|
||||
sent.append(msg)
|
||||
|
||||
else:
|
||||
logger.warning("renderer: unsupported message type=%s; skipping", mtype)
|
||||
continue
|
||||
|
||||
return sent
|
@ -22,6 +22,10 @@ from .handlers import (
|
||||
logger = logging.getLogger(__name__)
|
||||
openai.api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
# at top with other imports
|
||||
from .renderer import render_spec
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Canonical req.v1 builder
|
||||
# ---------------------------
|
||||
@ -264,6 +268,31 @@ async def telegram_webhook(request, bot_name: str):
|
||||
# Convert to telegram.Update
|
||||
update = Update.de_json(payload, Bot(token=bot_instance.token))
|
||||
|
||||
# --- TEMP: demo renderer (safe to delete later) ----------------------
|
||||
# If user sends "/_render_demo", send a text + photo + buttons
|
||||
if update.message and (update.message.text or "").strip() == "/_render_demo":
|
||||
bot = Bot(token=bot_instance.token)
|
||||
spec = {
|
||||
"schema_version": "render.v1",
|
||||
"messages": [
|
||||
{"type": "text", "text": "Demo: render_spec text ✅"},
|
||||
{
|
||||
"type": "photo",
|
||||
"media_url": "https://upload.wikimedia.org/wikipedia/commons/5/5f/Alameda_Central_CDMX.jpg",
|
||||
"caption": "Demo: render_spec photo ✅"
|
||||
}
|
||||
],
|
||||
"buttons": [
|
||||
{"label": "Abrir Dashboard", "kind": "open_url", "url": "https://app.polisplexity.tech/"},
|
||||
{"label": "Re-ejecutar 10’", "kind": "callback_api", "action": "rerun", "params": {"minutes": 10}}
|
||||
],
|
||||
"telemetry": {"run_id": "demo-run-001"}
|
||||
}
|
||||
await render_spec(bot=bot, chat_id=update.effective_chat.id, spec=spec)
|
||||
return JsonResponse({"status": "ok", "render_demo": True})
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
if not update.message:
|
||||
# No message (e.g., callback handled elsewhere in legacy); ack anyway
|
||||
return JsonResponse({"status": "no message"})
|
||||
|
Loading…
x
Reference in New Issue
Block a user