diff --git a/pxy_bots/renderer.py b/pxy_bots/renderer.py index 83f010c..7f3dd3d 100644 --- a/pxy_bots/renderer.py +++ b/pxy_bots/renderer.py @@ -14,9 +14,9 @@ def _build_keyboard(buttons: Optional[List[dict]]) -> Optional[InlineKeyboardMar 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":"..."} + - open_url: {"label":"...", "kind":"open_url", "url":"https://..."} + - callback_api: {"label":"...", "kind":"callback_api", "action":"rerun", + "params": {...}, "state_token":"..."} """ if not buttons: return None @@ -45,7 +45,7 @@ def _build_keyboard(buttons: Optional[List[dict]]) -> Optional[InlineKeyboardMar payload = json.dumps(data, separators=(",", ":"), ensure_ascii=False) except Exception: payload = '{"e":"bad"}' - # Telegram doc: 1–64 bytes recommended + # Telegram recommends <=64 bytes for callback_data if len(payload.encode("utf-8")) > 64: logger.warning("renderer: callback_data too long (%sB); trimming", len(payload.encode("utf-8"))) @@ -103,9 +103,34 @@ 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) - sent.append(msg) + try: + msg = await bot.send_photo(chat_id=chat_id, + photo=file_id or media_url, + caption=caption, + reply_markup=kb) + sent.append(msg) + except TelegramError as te: + # Typical: "BadRequest: Wrong type of the web page content" + logger.exception("renderer.photo_error send_photo err=%s url=%s", te, media_url) + # Fallback 1: try as document (Telegram is more permissive) + try: + msg = await bot.send_document(chat_id=chat_id, + document=file_id or media_url, + caption=caption, + reply_markup=kb) + sent.append(msg) + except TelegramError as te2: + logger.exception("renderer.photo_fallback_doc_error err=%s url=%s", te2, media_url) + # Fallback 2: plain text with link + fallback_text = (caption + "\n" if caption else "") + (media_url or "") + if fallback_text.strip(): + try: + msg = await bot.send_message(chat_id=chat_id, + text=fallback_text, + reply_markup=kb) + sent.append(msg) + except TelegramError as te3: + logger.exception("renderer.photo_fallback_text_error err=%s url=%s", te3, media_url) elif mtype == "document": file_id = m.get("file_id") diff --git a/pxy_bots/views.py b/pxy_bots/views.py index 823334c..e98f508 100644 --- a/pxy_bots/views.py +++ b/pxy_bots/views.py @@ -8,6 +8,7 @@ import openai from telegram import Update, Bot from django.http import JsonResponse, HttpResponse from django.views.decorators.csrf import csrf_exempt +from django.core.cache import cache from asgiref.sync import sync_to_async from .models import TelegramBot @@ -18,15 +19,11 @@ from .handlers import ( next_route, complete_stop, missed_stop, city_eco_score, available_jobs, accept_job, next_pickup, complete_pickup, private_eco_score ) +from .renderer import render_spec logger = logging.getLogger(__name__) openai.api_key = os.getenv("OPENAI_API_KEY") -# at top with other imports -from .renderer import render_spec -# top imports -from django.core.cache import cache - # --------------------------- # Canonical req.v1 builder @@ -152,6 +149,7 @@ def build_req_v1(update: Dict[str, Any], bot_name: str) -> Dict[str, Any]: } return env + # --------------------------- # Existing helper flows # --------------------------- @@ -231,6 +229,7 @@ async def transcribe_with_whisper(update: Update, bot: Bot) -> Optional[str]: ) return transcript_str.strip() if transcript_str else None + # --------------------------- # Webhook # --------------------------- @@ -259,28 +258,30 @@ async def telegram_webhook(request, bot_name: str): payload = json.loads(request.body.decode("utf-8") or "{}") except json.JSONDecodeError: return JsonResponse({"ok": False, "error": "invalid_json"}, status=400) - - # ----- Idempotency / retry guard (drops duplicates for ~90s) ----- + + # ----- Idempotency / retry guard (drops duplicates for ~90s) ----- upd_id = payload.get("update_id") - # Fallback if no update_id: use message_id + user_id - fallback_msg = (payload.get("message") or {}).get("message_id") - fallback_user = ((payload.get("message") or {}).get("from") or {}).get("id") + cbq = payload.get("callback_query") or {} + cbq_id = cbq.get("id") + msg = payload.get("message") or {} + fallback_msg_id = msg.get("message_id") + fallback_user = (msg.get("from") or {}).get("id") dedupe_key = None if upd_id is not None: dedupe_key = f"tg:update:{upd_id}" - elif fallback_msg and fallback_user: - dedupe_key = f"tg:msg:{fallback_msg}:{fallback_user}" + elif cbq_id: + dedupe_key = f"tg:cbq:{cbq_id}" + elif fallback_msg_id and fallback_user: + dedupe_key = f"tg:msg:{fallback_msg_id}:{fallback_user}" if dedupe_key: - # cache.add returns True if the key did not exist (first time), False otherwise if not cache.add(dedupe_key, "1", timeout=90): logger.info("tg.idempotent.skip key=%s", dedupe_key) return JsonResponse({"status": "duplicate_skipped"}) # ----------------------------------------------------------------- - - # Build canonical req.v1 (LOG ONLY for now) + # Build canonical req.v1 (log only for now) try: canon = build_req_v1(payload, bot_name) logger.info("tg.canonical env=%s", json.dumps(canon, ensure_ascii=False)) @@ -290,8 +291,7 @@ 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 + # --- TEMP demo: send a text + photo + buttons ---------------------- if update.message and (update.message.text or "").strip() == "/_render_demo": bot = Bot(token=bot_instance.token) spec = { @@ -300,7 +300,8 @@ async def telegram_webhook(request, bot_name: str): {"type": "text", "text": "Demo: render_spec text ✅"}, { "type": "photo", - "media_url": "https://upload.wikimedia.org/wikipedia/commons/5/5f/Alameda_Central_CDMX.jpg", + # Use a known-good image URL (or host your own under /static/) + "media_url": "https://picsum.photos/seed/polisplexity/800/480.jpg", "caption": "Demo: render_spec photo ✅" } ], @@ -312,8 +313,7 @@ async def telegram_webhook(request, bot_name: str): } 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