This commit is contained in:
parent
f656f04a5f
commit
ab7b9e0240
102
pxy_bots/router.py
Normal file
102
pxy_bots/router.py
Normal file
@ -0,0 +1,102 @@
|
||||
# pxy_bots/router.py
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- allowlist of outbound hosts (adjust as needed) ---
|
||||
ALLOWED_FORWARD_HOSTS = {"127.0.0.1", "localhost", "app.polisplexity.tech"}
|
||||
|
||||
# --- minimal route map (in-memory) ---
|
||||
# Per-bot mapping. Keys inside are command names WITHOUT leading slash.
|
||||
# Special keys:
|
||||
# "_default" → used when no command name detected (plain text)
|
||||
# "_callback" → used for callback_query events
|
||||
ROUTE_MAP: Dict[str, Dict[str, str]] = {
|
||||
# Example: use the local echo endpoint to validate the full loop
|
||||
"PolisplexityBot": {
|
||||
"_default": "http://127.0.0.1:8000/api/bots/echo_render",
|
||||
"_callback": "http://127.0.0.1:8000/api/bots/echo_render",
|
||||
"report_trash": "http://127.0.0.1:8000/api/bots/echo_render",
|
||||
# add more commands here…
|
||||
},
|
||||
# Wildcard bot (applies to any) — optional:
|
||||
"*": {
|
||||
"_default": "http://127.0.0.1:8000/api/bots/echo_render",
|
||||
"_callback": "http://127.0.0.1:8000/api/bots/echo_render",
|
||||
},
|
||||
}
|
||||
|
||||
# Try to use requests; fallback to urllib
|
||||
try:
|
||||
import requests # type: ignore
|
||||
_HAS_REQUESTS = True
|
||||
except Exception:
|
||||
import urllib.request # type: ignore
|
||||
_HAS_REQUESTS = False
|
||||
|
||||
|
||||
def _allowed(url: str) -> Tuple[bool, Optional[str]]:
|
||||
try:
|
||||
p = urlparse(url)
|
||||
host = (p.hostname or "").lower()
|
||||
if p.scheme not in {"http", "https"}:
|
||||
return False, "bad_scheme"
|
||||
if host not in ALLOWED_FORWARD_HOSTS:
|
||||
return False, f"host_not_allowed:{host}"
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, f"invalid_url:{e}"
|
||||
|
||||
|
||||
def pick_url(bot_name: str, canon: Dict) -> Optional[str]:
|
||||
"""Decide target URL from bot + command/trigger."""
|
||||
bot_routes = ROUTE_MAP.get(bot_name) or ROUTE_MAP.get("*") or {}
|
||||
trigger = ((canon.get("command") or {}).get("trigger")) or "message"
|
||||
cmd = ((canon.get("command") or {}).get("name")) or ""
|
||||
|
||||
if trigger == "callback":
|
||||
return bot_routes.get("_callback") or bot_routes.get("_default")
|
||||
|
||||
if cmd:
|
||||
return bot_routes.get(cmd) or bot_routes.get("_default")
|
||||
|
||||
return bot_routes.get("_default")
|
||||
|
||||
|
||||
def post_json(url: str, payload: Dict, timeout: float = 4.0) -> Tuple[int, Dict]:
|
||||
"""Blocking POST JSON; never raises; returns (status, body_json_or_wrapper)."""
|
||||
ok, why = _allowed(url)
|
||||
if not ok:
|
||||
logger.warning("router.reject url=%s reason=%s", url, why)
|
||||
return 400, {"ok": False, "error": f"forward_rejected:{why}", "url": url}
|
||||
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
if _HAS_REQUESTS:
|
||||
try:
|
||||
r = requests.post(url, data=data, headers=headers, timeout=timeout)
|
||||
try:
|
||||
body = r.json()
|
||||
except Exception:
|
||||
body = {"text": r.text[:2000]}
|
||||
return r.status_code, body
|
||||
except Exception as e:
|
||||
logger.exception("router.requests_failed url=%s", url)
|
||||
return 502, {"ok": False, "error": f"requests_failed:{e.__class__.__name__}"}
|
||||
else:
|
||||
try:
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp: # nosec
|
||||
raw = resp.read(65536)
|
||||
try:
|
||||
body = json.loads(raw.decode("utf-8"))
|
||||
except Exception:
|
||||
body = {"text": raw.decode("utf-8", errors="replace")[:2000]}
|
||||
return getattr(resp, "status", 200), body
|
||||
except Exception as e:
|
||||
logger.exception("router.urllib_failed url=%s", url)
|
||||
return 502, {"ok": False, "error": f"urllib_failed:{e.__class__.__name__}"}
|
@ -1,6 +1,7 @@
|
||||
from django.urls import path
|
||||
from .views import telegram_webhook
|
||||
from .views import telegram_webhook, echo_render
|
||||
|
||||
urlpatterns = [
|
||||
path('webhook/<str:bot_name>/', telegram_webhook, name='telegram_webhook'),
|
||||
path("bots/echo_render", echo_render, name="pxy_bots_echo_render"),
|
||||
]
|
||||
|
@ -21,6 +21,11 @@ from .handlers import (
|
||||
)
|
||||
from .renderer import render_spec
|
||||
|
||||
from .renderer import render_spec
|
||||
from .router import pick_url, post_json # <-- add this
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
openai.api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
@ -288,6 +293,36 @@ async def telegram_webhook(request, bot_name: str):
|
||||
except Exception as e:
|
||||
logger.exception("tg.canonical.failed: %s", e)
|
||||
|
||||
# Try routing via in-memory map. If a URL exists, post req.v1 and render the response.
|
||||
try:
|
||||
route_url = pick_url(bot_name, canon)
|
||||
except Exception:
|
||||
route_url = None
|
||||
|
||||
if route_url:
|
||||
# do blocking HTTP off the event loop
|
||||
status, body = await sync_to_async(post_json)(route_url, canon)
|
||||
logger.info("tg.routed url=%s status=%s", route_url, status)
|
||||
|
||||
# Accept either a full render_spec or any dict with a "messages" array
|
||||
spec = None
|
||||
if isinstance(body, dict) and ("messages" in body or body.get("schema_version") == "render.v1"):
|
||||
spec = body
|
||||
elif isinstance(body, dict) and "text" in body:
|
||||
# lenient: wrap plain text into a render_spec
|
||||
spec = {"schema_version": "render.v1", "messages": [{"type": "text", "text": str(body["text"])}]}
|
||||
|
||||
if spec:
|
||||
chat_id = (canon.get("chat") or {}).get("id")
|
||||
if chat_id:
|
||||
bot = Bot(token=bot_instance.token)
|
||||
await render_spec(bot=bot, chat_id=chat_id, spec=spec)
|
||||
return JsonResponse({"status": "ok", "routed": True, "status_code": status})
|
||||
|
||||
# If no spec, fall through to legacy handlers, but return routing status for visibility
|
||||
logger.warning("tg.routed.no_spec url=%s body_keys=%s", route_url, list(body.keys()) if isinstance(body, dict) else type(body))
|
||||
|
||||
|
||||
# Convert to telegram.Update
|
||||
update = Update.de_json(payload, Bot(token=bot_instance.token))
|
||||
|
||||
@ -354,3 +389,34 @@ async def telegram_webhook(request, bot_name: str):
|
||||
except Exception as e:
|
||||
logger.exception("Error in webhook: %s", e)
|
||||
return JsonResponse({"error": f"Unexpected error: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
# pxy_bots/api/views.py
|
||||
import json
|
||||
from django.http import JsonResponse
|
||||
|
||||
def echo_render(request):
|
||||
"""
|
||||
Accepts req.v1 and returns a simple render_spec so you can validate the router.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except Exception:
|
||||
data = {}
|
||||
|
||||
text = (((data.get("input") or {}).get("text")) or "Hola 👋")
|
||||
who = (((data.get("user") or {}).get("id")) or "user")
|
||||
cmd = (((data.get("command") or {}).get("name")) or "none")
|
||||
|
||||
spec = {
|
||||
"schema_version": "render.v1",
|
||||
"messages": [
|
||||
{"type": "text", "text": f"echo: user={who} cmd={cmd}"},
|
||||
{"type": "text", "text": f"you said: {text}"},
|
||||
],
|
||||
"buttons": [
|
||||
{"label": "Abrir Dashboard", "kind": "open_url", "url": "https://app.polisplexity.tech/"},
|
||||
{"label": "Re-ejecutar 10’", "kind": "callback_api", "action": "rerun", "params": {"minutes": 10}},
|
||||
],
|
||||
}
|
||||
return JsonResponse(spec)
|
||||
|
Loading…
x
Reference in New Issue
Block a user