- Register whatsapp_router in gateway main.py (GET + POST /whatsapp/webhook) - Implement is_clearly_off_topic() tier 1 keyword scoping gate - Implement build_off_topic_reply() canned redirect message builder - Full webhook handler: verify -> normalize -> tenant -> rate limit -> dedup -> scope -> media -> dispatch - Outbound delivery via send_whatsapp_message() and send_whatsapp_media() - Media download from Meta API and storage in MinIO with tenant-prefixed keys - 14 new passing scoping tests
117 lines
4.0 KiB
Python
117 lines
4.0 KiB
Python
"""
|
|
Channel Gateway — FastAPI application.
|
|
|
|
Mounts the slack-bolt AsyncApp as a sub-application at /slack/events.
|
|
Registers the WhatsApp webhook router at /whatsapp/webhook.
|
|
|
|
Port: 8001
|
|
|
|
Endpoints:
|
|
POST /slack/events — Slack Events API webhook (handled by slack-bolt)
|
|
GET /whatsapp/webhook — WhatsApp hub challenge verification
|
|
POST /whatsapp/webhook — WhatsApp inbound message webhook
|
|
GET /health — Health check
|
|
|
|
Startup sequence:
|
|
1. Create Redis connection
|
|
2. Create slack-bolt AsyncApp (signing_secret=...)
|
|
3. Register Slack event handlers
|
|
4. Mount slack-bolt request handler at /slack/events
|
|
5. Include WhatsApp router
|
|
6. Expose /health
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import FastAPI, Request, Response
|
|
from redis.asyncio import Redis
|
|
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
|
|
from slack_bolt.async_app import AsyncApp
|
|
|
|
from gateway.channels.slack import register_slack_handlers
|
|
from gateway.channels.whatsapp import whatsapp_router
|
|
from shared.config import settings
|
|
from shared.db import async_session_factory
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FastAPI app
|
|
# ---------------------------------------------------------------------------
|
|
app = FastAPI(
|
|
title="Konstruct Channel Gateway",
|
|
description="Unified ingress for all messaging platforms",
|
|
version="0.1.0",
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slack bolt app — initialized at module import time.
|
|
# signing_secret="" is safe for local dev/testing; set via env in production.
|
|
# ---------------------------------------------------------------------------
|
|
slack_app = AsyncApp(
|
|
token=settings.slack_bot_token or None,
|
|
signing_secret=settings.slack_signing_secret or None,
|
|
# In HTTP mode (Events API), token_verification_enabled must be True
|
|
# slack-bolt validates signing_secret on every inbound request
|
|
)
|
|
|
|
# Async Redis client — shared across all request handlers
|
|
_redis: Redis | None = None # type: ignore[type-arg]
|
|
|
|
|
|
def _get_redis() -> Redis: # type: ignore[type-arg]
|
|
"""Return the module-level Redis client, creating it if necessary."""
|
|
global _redis
|
|
if _redis is None:
|
|
_redis = Redis.from_url(settings.redis_url, decode_responses=True)
|
|
return _redis
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Register Slack event handlers
|
|
# ---------------------------------------------------------------------------
|
|
register_slack_handlers(
|
|
slack_app=slack_app,
|
|
redis=_get_redis(),
|
|
get_session=async_session_factory,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slack request handler — adapts slack-bolt AsyncApp to FastAPI
|
|
# ---------------------------------------------------------------------------
|
|
slack_handler = AsyncSlackRequestHandler(slack_app)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Register channel routers
|
|
# ---------------------------------------------------------------------------
|
|
app.include_router(whatsapp_router)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Routes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@app.post("/slack/events")
|
|
async def slack_events(request: Request) -> Response:
|
|
"""
|
|
Slack Events API webhook endpoint.
|
|
|
|
slack-bolt's AsyncSlackRequestHandler handles:
|
|
- Slack signature verification (X-Slack-Signature)
|
|
- URL verification challenge (type=url_verification)
|
|
- Event routing to registered handlers
|
|
|
|
CRITICAL: This endpoint MUST return HTTP 200 within 3 seconds.
|
|
All LLM/heavy work is dispatched to Celery inside the event handlers.
|
|
"""
|
|
return await slack_handler.handle(request)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health() -> dict[str, str]:
|
|
"""Health check endpoint."""
|
|
return {"status": "ok", "service": "gateway"}
|