From c6ff5e5d30893d812a0c0717baf7ea67d97dea87 Mon Sep 17 00:00:00 2001 From: Osman Mehmood Date: Thu, 9 Apr 2026 12:21:25 +0000 Subject: [PATCH] fix(bluebubbles): auto-register webhook with BlueBubbles server on connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem:** The BlueBubbles iMessage gateway was not receiving incoming messages even though: 1. BlueBubbles Server was properly configured and running 2. Hermes gateway started without errors 3. Webhook listener was started on the configured port The root cause was that the BlueBubbles adapter only started a local webhook listener but never registered the webhook URL with the BlueBubbles server via the API. Without registration, the server doesn't know where to send events. **Fix:** 1. Added _register_webhook() method that POSTs to /api/v1/webhook with the listener URL and event types (new-message, updated-message, message) 2. Added _unregister_webhook() method for clean shutdown 3. Both methods handle the case where webhook listens on 0.0.0.0/127.0.0.1 by using 'localhost' as the external hostname 4. Fixed documentation: 'hermes gateway logs' → 'hermes logs gateway' **API Reference:** https://docs.bluebubbles.app/server/developer-guides/rest-api-and-webhooks **Testing:** - Webhook registration is now automatic when gateway starts - Failed registration logs a warning but doesn't prevent startup - Clean shutdown unregisters the webhook Closes: iMessage gateway not working issue --- gateway/platforms/bluebubbles.py | 94 +++++++++++++++++++ .../docs/user-guide/messaging/bluebubbles.md | 3 +- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/gateway/platforms/bluebubbles.py b/gateway/platforms/bluebubbles.py index 83f94d3bf8..1842729d23 100644 --- a/gateway/platforms/bluebubbles.py +++ b/gateway/platforms/bluebubbles.py @@ -207,9 +207,17 @@ class BlueBubblesAdapter(BasePlatformAdapter): self.webhook_port, self.webhook_path, ) + + # Register webhook with BlueBubbles server + # This is required for the server to know where to send events + await self._register_webhook() + return True async def disconnect(self) -> None: + # Unregister webhook before cleaning up + await self._unregister_webhook() + if self.client: await self.client.aclose() self.client = None @@ -218,6 +226,91 @@ class BlueBubblesAdapter(BasePlatformAdapter): self._runner = None self._mark_disconnected() + async def _register_webhook(self) -> bool: + """Register this webhook URL with the BlueBubbles server. + + BlueBubbles requires webhooks to be registered via API before + it will send events. This method registers our listener URL + for new-message and updated-message events. + """ + if not self.client: + return False + + webhook_url = f"http://{self.webhook_host}:{self.webhook_port}{self.webhook_path}" + # Use host.docker.internal or public IP if webhook is 0.0.0.0/127.0.0.1 + # and server is on a different host + if self.webhook_host in ("0.0.0.0", "127.0.0.1", "localhost", "::"): + # For local development, we need the external IP that BlueBubbles can reach + # Default to localhost for same-machine setups + external_host = "localhost" + webhook_url = f"http://{external_host}:{self.webhook_port}{self.webhook_path}" + + payload = { + "url": webhook_url, + "events": ["new-message", "updated-message", "message"], + } + + try: + res = await self._api_post("/api/v1/webhook", payload) + if res.get("status") == 200: + logger.info( + "[bluebubbles] webhook registered successfully with server: %s", + webhook_url, + ) + return True + else: + logger.warning( + "[bluebubbles] webhook registration returned non-200 status: %s - %s", + res.get("status"), + res.get("message"), + ) + return False + except Exception as exc: + logger.warning( + "[bluebubbles] failed to register webhook with server: %s", + exc, + ) + return False + + async def _unregister_webhook(self) -> bool: + """Unregister this webhook URL from the BlueBubbles server. + + Cleans up the webhook registration when the gateway shuts down. + """ + if not self.client: + return False + + webhook_url = f"http://{self.webhook_host}:{self.webhook_port}{self.webhook_path}" + if self.webhook_host in ("0.0.0.0", "127.0.0.1", "localhost", "::"): + external_host = "localhost" + webhook_url = f"http://{external_host}:{self.webhook_port}{self.webhook_path}" + + try: + # Get current webhooks + webhooks = await self._api_get("/api/v1/webhook") + if webhooks.get("status") == 200: + data = webhooks.get("data", []) + for webhook in data: + if webhook.get("url") == webhook_url: + # Delete this specific webhook + webhook_id = webhook.get("id") + if webhook_id: + res = await self.client.delete( + self._api_url(f"/api/v1/webhook/{webhook_id}") + ) + res.raise_for_status() + logger.info( + "[bluebubbles] webhook unregistered: %s", + webhook_url, + ) + return True + except Exception as exc: + logger.debug( + "[bluebubbles] failed to unregister webhook (non-critical): %s", + exc, + ) + return False + # ------------------------------------------------------------------ # Chat GUID resolution # ------------------------------------------------------------------ @@ -826,3 +919,4 @@ class BlueBubblesAdapter(BasePlatformAdapter): asyncio.create_task(self.mark_read(session_chat_id)) return web.Response(text="ok") + diff --git a/website/docs/user-guide/messaging/bluebubbles.md b/website/docs/user-guide/messaging/bluebubbles.md index cde9690316..f2b240fc7f 100644 --- a/website/docs/user-guide/messaging/bluebubbles.md +++ b/website/docs/user-guide/messaging/bluebubbles.md @@ -135,8 +135,9 @@ Without the Private API, basic text messaging and media still work. ### Messages not arriving - Check that the webhook is registered in BlueBubbles Server → Settings → API → Webhooks - Verify the webhook URL is reachable from the Mac -- Check `hermes gateway logs` for webhook errors +- Check `hermes logs gateway` for webhook errors (or `hermes logs -f` to follow in real-time) ### "Private API helper not connected" - Install the Private API helper: [docs.bluebubbles.app](https://docs.bluebubbles.app/helper-bundle/installation) - Basic messaging works without it — only reactions, typing, and read receipts require it +