from __future__ import annotations import json import logging from collections.abc import Awaitable, Callable logger = logging.getLogger(__name__) MAX_WEBHOOK_BODY_BYTES = 1 * 1024 * 1024 DEFAULT_WEBHOOK_PATH = "/api/messages" ActivityHandler = Callable[[dict], Awaitable[dict]] def parse_webhook_body(body: bytes, max_size: int = MAX_WEBHOOK_BODY_BYTES) -> dict: if len(body) > max_size: raise WebhookPayloadTooLargeError(len(body), max_size) try: data = json.loads(body) except json.JSONDecodeError as e: raise WebhookInvalidJsonError(str(e)) from e if not isinstance(data, dict): raise WebhookInvalidPayloadError("Payload must be a JSON object") return data class WebhookError(Exception): pass class WebhookPayloadTooLargeError(WebhookError): def __init__(self, size: int, limit: int): self.size = size self.limit = limit super().__init__(f"Payload too large: {size} > {limit}") class WebhookInvalidJsonError(WebhookError): def __init__(self, detail: str): self.detail = detail super().__init__(f"Invalid JSON: {detail}") class WebhookInvalidPayloadError(WebhookError): pass def create_webhook_app( bot_app_id: str, adapter, conversation_store, message_handler: Callable[[dict], Awaitable[dict]], jwt_verifier, ) -> object: from fastapi import Depends, FastAPI, HTTPException, Request, Response from fastapi.responses import JSONResponse app = FastAPI(title="MS Teams Webhook", docs_url=None, redoc_url=None) async def verify_jwt_dependency(request: Request): authorization = request.headers.get("Authorization", "") if not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing Bearer token") token = authorization[7:] if not token: raise HTTPException(status_code=401, detail="Empty Bearer token") try: payload = await jwt_verifier(token) request.state.jwt_payload = payload return payload except Exception as e: logger.warning("JWT verification failed: %s", e) raise HTTPException(status_code=401, detail="Invalid token") from e @app.post(DEFAULT_WEBHOOK_PATH) async def handle_messages( request: Request, _jwt: dict = Depends(verify_jwt_dependency), ): body = await request.body() try: activity_data = parse_webhook_body(body) except WebhookPayloadTooLargeError: return Response(status_code=413) except WebhookError: return JSONResponse(status_code=400, content={"error": "Bad Request"}) result = await message_handler(activity_data) return JSONResponse(content=result) @app.get(DEFAULT_WEBHOOK_PATH) async def health_check(): return {"status": "ok", "channel": "msteams"} return app