From 185759de1fa66923fb024b1d6f3ebda9a09eca04 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 29 May 2026 10:20:43 +0530 Subject: [PATCH] feat(gateway): register multi-platform gateway routes --- surfsense_backend/app/routes/__init__.py | 4 ++ .../app/routes/gateway_webhook_routes.py | 63 ++++++++++++++----- .../app/tasks/celery_tasks/gateway_tasks.py | 30 ++++++--- 3 files changed, 72 insertions(+), 25 deletions(-) diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index f46b6fc65..369c988c7 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -19,6 +19,8 @@ from .editor_routes import router as editor_router from .export_routes import router as export_router from .folders_routes import router as folders_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 ( router as google_calendar_add_connector_router, ) @@ -70,6 +72,8 @@ router.include_router(export_router) router.include_router(documents_router) router.include_router(folders_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(new_chat_router) # Chat with assistant-ui persistence router.include_router(agent_revert_router) # POST /threads/{id}/revert/{action_id} diff --git a/surfsense_backend/app/routes/gateway_webhook_routes.py b/surfsense_backend/app/routes/gateway_webhook_routes.py index f9b6acc93..5508e534c 100644 --- a/surfsense_backend/app/routes/gateway_webhook_routes.py +++ b/surfsense_backend/app/routes/gateway_webhook_routes.py @@ -7,6 +7,7 @@ import logging import uuid from datetime import UTC, datetime from typing import Any +from urllib.parse import quote from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel @@ -16,14 +17,17 @@ from starlette.responses import Response from app.config import config from app.db import ( - ExternalChatBindingState, - ExternalChatBinding, - ExternalChatPlatform, ExternalChatAccount, + ExternalChatBinding, + ExternalChatBindingState, + ExternalChatPlatform, User, 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.inbox import persist_inbound_event, telegram_event_dedupe_key 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), session: AsyncSession = Depends(get_async_session), ) -> 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() + 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() binding = ExternalChatBinding( account_id=account.id, @@ -149,13 +181,10 @@ async def start_binding( await session.commit() 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( binding_id=binding.id, code=code, - deep_link=f"https://t.me/{username}?start={code}", + deep_link=deep_link, expires_at=expires_at, ) @@ -166,21 +195,21 @@ async def list_bindings( session: AsyncSession = Depends(get_async_session), ) -> list[dict[str, Any]]: result = await session.execute( - select(ExternalChatBinding).where( - ExternalChatBinding.user_id == user.id - ) + select(ExternalChatBinding, ExternalChatAccount) + .join(ExternalChatAccount, ExternalChatBinding.account_id == ExternalChatAccount.id) + .where(ExternalChatBinding.user_id == user.id) ) return [ { "id": binding.id, - "platform": "telegram", + "platform": account.platform.value, "state": binding.state.value, "search_space_id": binding.search_space_id, "external_display_name": binding.external_display_name, "external_username": binding.external_username, "suspended_reason": binding.suspended_reason, } - for binding in result.scalars() + for binding, account in result.all() ] diff --git a/surfsense_backend/app/tasks/celery_tasks/gateway_tasks.py b/surfsense_backend/app/tasks/celery_tasks/gateway_tasks.py index aeb3d721e..1c2bb166f 100644 --- a/surfsense_backend/app/tasks/celery_tasks/gateway_tasks.py +++ b/surfsense_backend/app/tasks/celery_tasks/gateway_tasks.py @@ -9,14 +9,14 @@ from sqlalchemy import select, update from app.celery_app import celery_app from app.db import ( + ExternalChatAccount, ExternalChatEventStatus, ExternalChatHealthStatus, ExternalChatInboundEvent, ExternalChatPlatform, - ExternalChatAccount, ) -from app.gateway.accounts import account_token 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.observability.metrics import ( record_gateway_health_check_failure, @@ -69,15 +69,29 @@ def gateway_health_check_task() -> None: result = await session.execute(select(ExternalChatAccount)) accounts = list(result.scalars()) for account in accounts: - token = account_token(account) - if not token or account.platform != ExternalChatPlatform.TELEGRAM: - continue try: - metadata = await TelegramAdapter(token).validate_credentials() + bundle = resolve_platform_bundle(account) + metadata = await bundle.adapter.validate_credentials() 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: - 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 record_gateway_health_check_failure(platform=account.platform.value) account.last_health_check_at = datetime.now(UTC)