diff --git a/telegram/bot.py b/telegram/bot.py index 3d7deda..97252f8 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -35,7 +35,7 @@ from pathlib import Path # Add pipeline lib to path for shared modules sys.path.insert(0, "/opt/teleo-eval/pipeline") -from telegram import Update +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( Application, CommandHandler, @@ -198,11 +198,17 @@ async def _reply_checkout_qr_photo(msg, proxy_body: dict | None, *, do_quote: bo card = extract_checkout_qr_photo_message(proxy_body) if not card: return False + reply_markup = None + if card.get("button_url"): + reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(card.get("button_text") or "Pay with x402", url=card["button_url"])]] + ) try: await msg.reply_photo( photo=card["photo_url"], caption=card["caption_html"], parse_mode=card.get("parse_mode") or "HTML", + reply_markup=reply_markup, do_quote=do_quote, ) return True diff --git a/telegram/http_chat_proxy.py b/telegram/http_chat_proxy.py index 8d16904..7ff9025 100644 --- a/telegram/http_chat_proxy.py +++ b/telegram/http_chat_proxy.py @@ -193,6 +193,28 @@ def extract_checkout_qr_url(payload: dict[str, Any] | None) -> str | None: return url +def extract_checkout_url(payload: dict[str, Any] | None) -> str | None: + """Return a Telegram-safe Leo checkout URL for an inline payment button.""" + if not isinstance(payload, dict): + return None + checkout = payload.get("checkout") + if not isinstance(checkout, dict): + return None + telegram = checkout.get("telegram") + raw_url = None + if isinstance(telegram, dict): + raw_url = telegram.get("buttonUrl") or telegram.get("button_url") or telegram.get("checkoutUrl") + raw_url = raw_url or checkout.get("checkoutUrl") or checkout.get("checkout_url") + if not isinstance(raw_url, str): + return None + url = raw_url.strip() + if len(url) > 2000: + return None + if not re.match(r"^https://leo\.livingip\.xyz/agents/leo/research/checkout\?", url): + return None + return url + + def extract_checkout_qr_photo_message(payload: dict[str, Any] | None) -> dict[str, str] | None: """Return a compact Telegram photo+caption payload for a Leo paid-research quote.""" qr_url = extract_checkout_qr_url(payload) @@ -205,10 +227,14 @@ def extract_checkout_qr_photo_message(payload: dict[str, Any] | None) -> dict[st telegram = checkout.get("telegram") caption_html = None + button_text = "Pay with x402" if isinstance(telegram, dict): raw_caption = telegram.get("captionHtml") or telegram.get("caption_html") if isinstance(raw_caption, str) and 1 <= len(raw_caption) <= 1024: caption_html = raw_caption.strip() + raw_button_text = telegram.get("buttonText") or telegram.get("button_text") + if isinstance(raw_button_text, str) and 1 <= len(raw_button_text.strip()) <= 40: + button_text = raw_button_text.strip() if not caption_html: price = str(checkout.get("priceUsd") or checkout.get("price_usd") or "0.07").strip() @@ -237,6 +263,8 @@ def extract_checkout_qr_photo_message(payload: dict[str, Any] | None) -> dict[st "photo_url": qr_url, "caption_html": caption_html, "parse_mode": "HTML", + "button_text": button_text, + "button_url": extract_checkout_url(payload) or "", } diff --git a/tests/test_telegram_leo_x402_bridge.py b/tests/test_telegram_leo_x402_bridge.py index e6fab2f..8f6b489 100644 --- a/tests/test_telegram_leo_x402_bridge.py +++ b/tests/test_telegram_leo_x402_bridge.py @@ -16,6 +16,7 @@ from http_chat_proxy import ( # noqa: E402 classify_smart_research_auto_resume_response, extract_auto_smart_research_followup_goal, extract_auto_smart_research_goal, + extract_checkout_url, extract_checkout_qr_photo_message, extract_checkout_qr_url, extract_paid_work_order_id, @@ -308,8 +309,10 @@ def test_extract_checkout_qr_url(payload, expected): def test_extract_checkout_qr_photo_message_prefers_structured_telegram_card(): + checkout_url = "https://leo.livingip.xyz/agents/leo/research/checkout?q=test" payload = { "checkout": { + "checkoutUrl": checkout_url, "checkoutQrUrl": ( "https://leo.livingip.xyz/api/agents/leo/research/checkout-qr?" "url=https%3A%2F%2Fleo.livingip.xyz%2Fagents%2Fleo%2Fresearch%2Fcheckout%3Fq%3Dtest" @@ -319,10 +322,11 @@ def test_extract_checkout_qr_photo_message_prefers_structured_telegram_card(): "captionHtml": ( "Paid Leo research\n" "0.07 USDC on Solana mainnet.\n" - '' - "Pay with x402\n" + "Pay with the button below or scan the QR.\n" "Recipient: 8EgACpZ16XWEt7YjJPsh1ZheVRZUGmmwQ8nJdmA1o5w4" - ) + ), + "buttonText": "Pay with x402", + "buttonUrl": checkout_url, }, } } @@ -333,7 +337,10 @@ def test_extract_checkout_qr_photo_message_prefers_structured_telegram_card(): "photo_url": payload["checkout"]["checkoutQrUrl"], "caption_html": payload["checkout"]["telegram"]["captionHtml"], "parse_mode": "HTML", + "button_text": "Pay with x402", + "button_url": checkout_url, } + assert checkout_url not in card["caption_html"] assert "npx agentcash" not in card["caption_html"] assert "Human checkout:" not in card["caption_html"] assert "QR checkout:" not in card["caption_html"] @@ -354,6 +361,11 @@ def test_extract_checkout_qr_photo_message_builds_address_fallback_caption(): assert card["photo_url"] == payload["checkout"]["checkoutQrUrl"] assert "0.07 USDC on Solana mainnet" in card["caption_html"] assert "8EgACpZ16XWEt7YjJPsh1ZheVRZUGmmwQ8nJdmA1o5w4" in card["caption_html"] + assert card["button_url"] == "" + + +def test_extract_checkout_url_rejects_non_leo_checkout_url(): + assert extract_checkout_url({"checkout": {"checkoutUrl": "https://example.com/pay"}}) is None def test_extract_checkout_qr_photo_message_rejects_non_leo_qr_url():