feat(gateway): register multi-platform gateway routes

This commit is contained in:
Anish Sarkar 2026-05-29 10:20:43 +05:30
parent 51bf2a8361
commit 185759de1f
3 changed files with 72 additions and 25 deletions

View file

@ -19,6 +19,8 @@ from .editor_routes import router as editor_router
from .export_routes import router as export_router from .export_routes import router as export_router
from .folders_routes import router as folders_router from .folders_routes import router as folders_router
from .gateway_webhook_routes import router as gateway_router from .gateway_webhook_routes import router as gateway_router
from .gateway_whatsapp_baileys_routes import router as gateway_whatsapp_baileys_router
from .gateway_whatsapp_webhook_routes import router as gateway_whatsapp_webhook_router
from .google_calendar_add_connector_route import ( from .google_calendar_add_connector_route import (
router as google_calendar_add_connector_router, router as google_calendar_add_connector_router,
) )
@ -70,6 +72,8 @@ router.include_router(export_router)
router.include_router(documents_router) router.include_router(documents_router)
router.include_router(folders_router) router.include_router(folders_router)
router.include_router(gateway_router) router.include_router(gateway_router)
router.include_router(gateway_whatsapp_webhook_router)
router.include_router(gateway_whatsapp_baileys_router)
router.include_router(notes_router) router.include_router(notes_router)
router.include_router(new_chat_router) # Chat with assistant-ui persistence router.include_router(new_chat_router) # Chat with assistant-ui persistence
router.include_router(agent_revert_router) # POST /threads/{id}/revert/{action_id} router.include_router(agent_revert_router) # POST /threads/{id}/revert/{action_id}

View file

@ -7,6 +7,7 @@ import logging
import uuid import uuid
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from urllib.parse import quote
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel from pydantic import BaseModel
@ -16,14 +17,17 @@ from starlette.responses import Response
from app.config import config from app.config import config
from app.db import ( from app.db import (
ExternalChatBindingState,
ExternalChatBinding,
ExternalChatPlatform,
ExternalChatAccount, ExternalChatAccount,
ExternalChatBinding,
ExternalChatBindingState,
ExternalChatPlatform,
User, User,
get_async_session, get_async_session,
) )
from app.gateway.accounts import get_or_create_system_telegram_account from app.gateway.accounts import (
get_or_create_system_telegram_account,
get_or_create_system_whatsapp_account,
)
from app.gateway.bindings import resume_binding, revoke_binding from app.gateway.bindings import resume_binding, revoke_binding
from app.gateway.inbox import persist_inbound_event, telegram_event_dedupe_key from app.gateway.inbox import persist_inbound_event, telegram_event_dedupe_key
from app.gateway.pairing import generate_pairing_code, pairing_expires_at from app.gateway.pairing import generate_pairing_code, pairing_expires_at
@ -131,11 +135,39 @@ async def start_binding(
user: User = Depends(current_active_user), user: User = Depends(current_active_user),
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
) -> StartBindingResponse: ) -> StartBindingResponse:
if body.platform != ExternalChatPlatform.TELEGRAM:
raise HTTPException(status_code=400, detail="Only Telegram is supported in v1")
account = await get_or_create_system_telegram_account(session)
code = generate_pairing_code() code = generate_pairing_code()
if body.platform == ExternalChatPlatform.TELEGRAM:
account = await get_or_create_system_telegram_account(session)
username = account.bot_username or config.TELEGRAM_SHARED_BOT_USERNAME
if not username:
raise HTTPException(
status_code=500,
detail="Telegram bot username is not configured",
)
deep_link = f"https://t.me/{username}?start={code}"
elif body.platform == ExternalChatPlatform.WHATSAPP:
if config.GATEWAY_WHATSAPP_INTAKE_MODE != "cloud":
raise HTTPException(
status_code=400,
detail="WhatsApp /start pairing requires GATEWAY_WHATSAPP_INTAKE_MODE=cloud",
)
account = await get_or_create_system_whatsapp_account(session)
phone = config.WHATSAPP_SHARED_DISPLAY_PHONE_NUMBER
if not phone:
raise HTTPException(
status_code=500,
detail="WHATSAPP_SHARED_DISPLAY_PHONE_NUMBER is not configured",
)
normalized_phone = "".join(ch for ch in phone if ch.isdigit())
if not normalized_phone:
raise HTTPException(
status_code=500,
detail="WHATSAPP_SHARED_DISPLAY_PHONE_NUMBER must contain digits",
)
deep_link = f"https://wa.me/{normalized_phone}?text={quote(f'/start {code}')}"
else:
raise HTTPException(status_code=400, detail="Unsupported platform")
expires_at = pairing_expires_at() expires_at = pairing_expires_at()
binding = ExternalChatBinding( binding = ExternalChatBinding(
account_id=account.id, account_id=account.id,
@ -149,13 +181,10 @@ async def start_binding(
await session.commit() await session.commit()
await session.refresh(binding) await session.refresh(binding)
username = account.bot_username or config.TELEGRAM_SHARED_BOT_USERNAME
if not username:
raise HTTPException(status_code=500, detail="Telegram bot username is not configured")
return StartBindingResponse( return StartBindingResponse(
binding_id=binding.id, binding_id=binding.id,
code=code, code=code,
deep_link=f"https://t.me/{username}?start={code}", deep_link=deep_link,
expires_at=expires_at, expires_at=expires_at,
) )
@ -166,21 +195,21 @@ async def list_bindings(
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
result = await session.execute( result = await session.execute(
select(ExternalChatBinding).where( select(ExternalChatBinding, ExternalChatAccount)
ExternalChatBinding.user_id == user.id .join(ExternalChatAccount, ExternalChatBinding.account_id == ExternalChatAccount.id)
) .where(ExternalChatBinding.user_id == user.id)
) )
return [ return [
{ {
"id": binding.id, "id": binding.id,
"platform": "telegram", "platform": account.platform.value,
"state": binding.state.value, "state": binding.state.value,
"search_space_id": binding.search_space_id, "search_space_id": binding.search_space_id,
"external_display_name": binding.external_display_name, "external_display_name": binding.external_display_name,
"external_username": binding.external_username, "external_username": binding.external_username,
"suspended_reason": binding.suspended_reason, "suspended_reason": binding.suspended_reason,
} }
for binding in result.scalars() for binding, account in result.all()
] ]

View file

@ -9,14 +9,14 @@ from sqlalchemy import select, update
from app.celery_app import celery_app from app.celery_app import celery_app
from app.db import ( from app.db import (
ExternalChatAccount,
ExternalChatEventStatus, ExternalChatEventStatus,
ExternalChatHealthStatus, ExternalChatHealthStatus,
ExternalChatInboundEvent, ExternalChatInboundEvent,
ExternalChatPlatform, ExternalChatPlatform,
ExternalChatAccount,
) )
from app.gateway.accounts import account_token
from app.gateway.inbox import persist_inbound_event, telegram_event_dedupe_key from app.gateway.inbox import persist_inbound_event, telegram_event_dedupe_key
from app.gateway.registry import resolve_platform_bundle
from app.gateway.telegram.adapter import TelegramAdapter from app.gateway.telegram.adapter import TelegramAdapter
from app.observability.metrics import ( from app.observability.metrics import (
record_gateway_health_check_failure, record_gateway_health_check_failure,
@ -69,15 +69,29 @@ def gateway_health_check_task() -> None:
result = await session.execute(select(ExternalChatAccount)) result = await session.execute(select(ExternalChatAccount))
accounts = list(result.scalars()) accounts = list(result.scalars())
for account in accounts: for account in accounts:
token = account_token(account)
if not token or account.platform != ExternalChatPlatform.TELEGRAM:
continue
try: try:
metadata = await TelegramAdapter(token).validate_credentials() bundle = resolve_platform_bundle(account)
metadata = await bundle.adapter.validate_credentials()
account.health_status = ExternalChatHealthStatus.OK account.health_status = ExternalChatHealthStatus.OK
account.bot_username = metadata.get("username") if account.platform == ExternalChatPlatform.TELEGRAM:
account.bot_username = metadata.get("username")
elif account.platform == ExternalChatPlatform.WHATSAPP:
cursor_state = dict(account.cursor_state or {})
for key in (
"quality_rating",
"account_review_status",
"status",
):
if key in metadata:
cursor_state[key] = metadata[key]
account.cursor_state = cursor_state
except Exception: except Exception:
logger.warning("External chat Telegram health check failed", exc_info=True) logger.warning(
"External chat health check failed platform=%s account_id=%s",
account.platform.value,
account.id,
exc_info=True,
)
account.health_status = ExternalChatHealthStatus.FAILING account.health_status = ExternalChatHealthStatus.FAILING
record_gateway_health_check_failure(platform=account.platform.value) record_gateway_health_check_failure(platform=account.platform.value)
account.last_health_check_at = datetime.now(UTC) account.last_health_check_at = datetime.now(UTC)