import hashlib import hmac import json import logging from http import HTTPStatus from fastapi import APIRouter, Request, HTTPException, Response from yuxi.channel.extensions.bluebubbles.monitor import process_inbound_message logger = logging.getLogger(__name__) def _constant_time_compare(a: str, b: str) -> bool: return hmac.compare_digest(a.encode() if isinstance(a, str) else a, b.encode() if isinstance(b, str) else b) class BlueBubblesWebhookHandler: def __init__( self, webhook_secret: str = "", account_id: str = "default", dedupe_store=None, debounce_manager=None, on_message=None, send_read_receipt=None, ): self.webhook_secret = webhook_secret self.account_id = account_id self.dedupe_store = dedupe_store self.debounce_manager = debounce_manager self.on_message = on_message self.send_read_receipt = send_read_receipt async def handle_webhook(self, request: Request) -> Response: if self.webhook_secret: sig = request.headers.get("X-BB-Signature", "") body = await request.body() expected = hmac.new( self.webhook_secret.encode(), body, hashlib.sha256, ).hexdigest() if not _constant_time_compare(sig, expected): raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid signature") try: body = await request.body() data = json.loads(body) except json.JSONDecodeError: raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid JSON") if isinstance(data, list): for item in data: await process_inbound_message( item, self.account_id, dedupe_store=self.dedupe_store, debounce_manager=self.debounce_manager, on_message=self.on_message, send_read_receipt=self.send_read_receipt, ) else: await process_inbound_message( data, self.account_id, dedupe_store=self.dedupe_store, debounce_manager=self.debounce_manager, on_message=self.on_message, send_read_receipt=self.send_read_receipt, ) return Response(status_code=HTTPStatus.OK) def create_webhook_router( path: str = "/bluebubbles-webhook", webhook_secret: str = "", account_id: str = "default", **kwargs, ) -> APIRouter: router = APIRouter() handler = BlueBubblesWebhookHandler( webhook_secret=webhook_secret, account_id=account_id, **kwargs, ) @router.post(path) async def _webhook(request: Request): return await handler.handle_webhook(request) return router