From 2cf444be025cd0c3c3657b6648261904cfc06dd5 Mon Sep 17 00:00:00 2001 From: Tyler Yust Date: Mon, 19 Jan 2026 18:50:22 -0800 Subject: [PATCH] Step 4 (Needs Review) --- docs/channels/bluebubbles.md | 198 +++++++++++++++--- docs/channels/index.md | 4 +- extensions/bluebubbles/src/channel.ts | 15 +- .../plugins/status-issues/bluebubbles.ts | 100 +++++++++ src/plugin-sdk/index.ts | 3 + 5 files changed, 279 insertions(+), 41 deletions(-) create mode 100644 src/channels/plugins/status-issues/bluebubbles.ts diff --git a/docs/channels/bluebubbles.md b/docs/channels/bluebubbles.md index a2b96dbe3..cc6f26e31 100644 --- a/docs/channels/bluebubbles.md +++ b/docs/channels/bluebubbles.md @@ -1,55 +1,199 @@ --- -summary: "iMessage via BlueBubbles macOS server (REST send/receive, typing, reactions, pairing)." +summary: "iMessage via BlueBubbles macOS server (REST send/receive, typing, reactions, pairing, advanced actions)." read_when: - Setting up BlueBubbles channel - Troubleshooting webhook pairing + - Configuring iMessage on macOS --- # BlueBubbles (macOS REST) -Status: bundled plugin (disabled by default) that talks to the BlueBubbles macOS server over HTTP. +Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel. ## Overview -- Runs on macOS via the BlueBubbles helper app (`https://bluebubbles.app`). +- Runs on macOS via the BlueBubbles helper app ([bluebubbles.app](https://bluebubbles.app)). - Clawdbot talks to it through its REST API (`GET /api/v1/ping`, `POST /message/text`, `POST /chat/:id/*`). - Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls. - Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible). - Pairing/allowlist works the same way as other channels (`/start/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes. -- Reactions are surfaced as system events just like Slack/Telegram so agents can “mention” them before replying. +- Reactions are surfaced as system events just like Slack/Telegram so agents can "mention" them before replying. +- Advanced features: edit, unsend, reply threading, message effects, group management. ## Quick start -1. Install the BlueBubbles server on your Mac (follows the app store instructions at `https://bluebubbles.app/install`). -2. In the BlueBubbles config, enable the web API and set a password for `guid`/`password`. -3. Configure Clawdbot: +1. Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)). +2. In the BlueBubbles config, enable the web API and set a password. +3. Run `clawdbot onboard` and select BlueBubbles, or configure manually: ```json5 { channels: { bluebubbles: { enabled: true, - serverUrl: "http://bluebubbles-host:1234", + serverUrl: "http://192.168.1.100:1234", password: "example-password", - webhookPath: "/bluebubbles-webhook", - actions: { reactions: true } + webhookPath: "/bluebubbles-webhook" } } } ``` -4. Point BlueBubbles webhooks to your gateway (example: `http://your-gateway-host/bluebubbles-webhook?password=`). +4. Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=`). 5. Start the gateway; it will register the webhook handler and start pairing. -## Configuration notes -- `channels.bluebubbles.serverUrl`: base URL of the BlueBubbles REST API. -- `channels.bluebubbles.password`: password that BlueBubbles expects on every request (`?password=...` or header). -- `channels.bluebubbles.webhookPath`: HTTP path the gateway exposes for BlueBubbles webhooks. -- `channels.bluebubbles.dmPolicy` / `groupPolicy` + `allowFrom`/`groupAllowFrom` behave like other channels; pairing/allowlist info is stored in `/pairing`. -- `channels.bluebubbles.actions.reactions` toggles whether the gateway enqueues system events for reactions/tapbacks. -- `channels.bluebubbles.textChunkLimit` overrides the default 4k limit. -- `channels.bluebubbles.mediaMaxMb` controls the max size of inbound attachments saved for analysis (default 8MB). +## Onboarding +BlueBubbles is available in the interactive setup wizard: +``` +clawdbot onboard +``` -## How it works -- Outbound replies: `sendMessageBlueBubbles` resolves a chat GUID via `/api/v1/chat/query` and posts to `/api/v1/message/text`. Typing (`/api/v1/chat//typing`) and read receipts (`/api/v1/chat//read`) are sent before/after responses. -- Webhooks: BlueBubbles POSTs JSON payloads with `type` and `data`. The plugin ignores non-message events (typing indicator, read status) and extracts `chatGuid` from `data.chats[0].guid`. -- Reactions/tapbacks generate `BlueBubbles reaction added/removed` system events so agents can mention them. Agents can also trigger tapbacks via the `react` action with `messageId`, `emoji`, and a `to`/`chatGuid`. -- Attachments are downloaded via the REST API and stored in the inbound media cache; text-less messages are converted into `` placeholders so the agent knows something was sent. +The wizard prompts for: +- **Server URL** (required): BlueBubbles server address (e.g., `http://192.168.1.100:1234`) +- **Password** (required): API password from BlueBubbles Server settings +- **Webhook path** (optional): Defaults to `/bluebubbles-webhook` +- **DM policy**: pairing, allowlist, open, or disabled +- **Allow list**: Phone numbers, emails, or chat targets + +You can also add BlueBubbles via CLI: +``` +clawdbot channels add bluebubbles --http-url http://192.168.1.100:1234 --password +``` + +## Access control (DMs + groups) +DMs: +- Default: `channels.bluebubbles.dmPolicy = "pairing"`. +- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). +- Approve via: + - `clawdbot pairing list bluebubbles` + - `clawdbot pairing approve bluebubbles ` +- Pairing is the default token exchange. Details: [Pairing](/start/pairing) + +Groups: +- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`). +- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set. + +### Mention gating (groups) +BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior: +- Uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) to detect mentions. +- When `requireMention` is enabled for a group, the agent only responds when mentioned. +- Control commands from authorized senders bypass mention gating. + +Per-group configuration: +```json5 +{ + channels: { + bluebubbles: { + groupPolicy: "allowlist", + groupAllowFrom: ["+15555550123"], + groups: { + "*": { requireMention: true }, // default for all groups + "iMessage;-;chat123": { requireMention: false } // override for specific group + } + } + } +} +``` + +### Command gating +- Control commands (e.g., `/config`, `/model`) require authorization. +- Uses `allowFrom` and `groupAllowFrom` to determine command authorization. +- Authorized senders can run control commands even without mentioning in groups. + +## Typing + read receipts +- **Typing indicators**: Sent automatically before and during response generation. +- **Read receipts**: Controlled by `channels.bluebubbles.sendReadReceipts` (default: `true`). + +```json5 +{ + channels: { + bluebubbles: { + sendReadReceipts: false // disable read receipts + } + } +} +``` + +## Advanced actions +BlueBubbles supports advanced message actions when enabled in config: + +```json5 +{ + channels: { + bluebubbles: { + actions: { + reactions: true, // tapbacks (default: true) + edit: true, // edit sent messages (macOS 13+) + unsend: true, // unsend messages (macOS 13+) + reply: true, // reply threading by message GUID + sendWithEffect: true, // message effects (slam, loud, etc.) + renameGroup: true, // rename group chats + addParticipant: true, // add participants to groups + removeParticipant: true, // remove participants from groups + leaveGroup: true, // leave group chats + sendAttachment: true // send attachments/media + } + } + } +} +``` + +Available actions: +- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`) +- **edit**: Edit a sent message (`messageId`, `text`) +- **unsend**: Unsend a message (`messageId`) +- **reply**: Reply to a specific message (`messageId`, `text`, `to`) +- **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`) +- **renameGroup**: Rename a group chat (`chatGuid`, `displayName`) +- **addParticipant**: Add someone to a group (`chatGuid`, `address`) +- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`) +- **leaveGroup**: Leave a group chat (`chatGuid`) +- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`) + +## Block streaming +Control whether responses are sent as a single message or streamed in blocks: +```json5 +{ + channels: { + bluebubbles: { + blockStreaming: true // enable block streaming (default behavior) + } + } +} +``` + +## Media + limits +- Inbound attachments are downloaded and stored in the media cache. +- Media cap via `channels.bluebubbles.mediaMaxMb` (default: 8 MB). +- Outbound text is chunked to `channels.bluebubbles.textChunkLimit` (default: 4000 chars). + +## Configuration reference +Full configuration: [Configuration](/gateway/configuration) + +Provider options: +- `channels.bluebubbles.enabled`: Enable/disable the channel. +- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL. +- `channels.bluebubbles.password`: API password. +- `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`). +- `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`). +- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`). +- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`). +- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist. +- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.). +- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`). +- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `true`). +- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000). +- `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8). +- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables). +- `channels.bluebubbles.dmHistoryLimit`: DM history limit. +- `channels.bluebubbles.actions`: Enable/disable specific actions. +- `channels.bluebubbles.accounts`: Multi-account configuration. + +Related global options: +- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`). +- `messages.responsePrefix`. + +## Addressing / delivery targets +Prefer `chat_guid` for stable routing: +- `chat_guid:iMessage;-;+15555550123` (preferred for groups) +- `chat_id:123` +- `chat_identifier:...` +- Direct handles: `+15555550123`, `user@example.com` ## Security - Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted. @@ -57,8 +201,10 @@ Status: bundled plugin (disabled by default) that talks to the BlueBubbles macOS - Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN. ## Troubleshooting -- If Voice/typing events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`. +- If typing/read events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`. - Pairing codes expire after one hour; use `clawdbot pairing list bluebubbles` and `clawdbot pairing approve bluebubbles `. - Reactions require the BlueBubbles private API (`POST /api/v1/message/react`); ensure the server version exposes it. +- Edit/unsend require macOS 13+ and a compatible BlueBubbles server version. +- For status/health info: `clawdbot status --all` or `clawdbot status --deep`. -For general channel workflow reference, see [/channels/index] and the [[plugins|/plugin]] guide. +For general channel workflow reference, see [Channels](/channels) and the [Plugins](/plugins) guide. diff --git a/docs/channels/index.md b/docs/channels/index.md index aa5c8fd2f..4b4a2af10 100644 --- a/docs/channels/index.md +++ b/docs/channels/index.md @@ -16,8 +16,8 @@ Text is supported everywhere; media and reactions vary by channel. - [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs. - [Slack](/channels/slack) — Bolt SDK; workspace apps. - [Signal](/channels/signal) — signal-cli; privacy-focused. -- [iMessage](/channels/imessage) — macOS only; native integration. -- [BlueBubbles](/channels/bluebubbles) — iMessage via BlueBubbles macOS server (bundled plugin, disabled by default). +- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management). +- [iMessage](/channels/imessage) — macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups). - [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately). - [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately). - [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately). diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index 2d0b4471b..2d487f6b6 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -5,6 +5,7 @@ import type { ChannelAccountSnapshot, ChannelPlugin, ClawdbotConfig } from "claw import { applyAccountNameToChannelSection, buildChannelConfigSchema, + collectBlueBubblesStatusIssues, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, formatPairingApproveHint, @@ -331,19 +332,7 @@ export const bluebubblesPlugin: ChannelPlugin = { lastStopAt: null, lastError: null, }, - collectStatusIssues: (accounts) => - accounts.flatMap((account) => { - const lastError = typeof account.lastError === "string" ? account.lastError.trim() : ""; - if (!lastError) return []; - return [ - { - channel: "bluebubbles", - accountId: account.accountId, - kind: "runtime", - message: `Channel error: ${lastError}`, - }, - ]; - }), + collectStatusIssues: collectBlueBubblesStatusIssues, buildChannelSummary: ({ snapshot }) => ({ configured: snapshot.configured ?? false, baseUrl: snapshot.baseUrl ?? null, diff --git a/src/channels/plugins/status-issues/bluebubbles.ts b/src/channels/plugins/status-issues/bluebubbles.ts new file mode 100644 index 000000000..5f5cadd84 --- /dev/null +++ b/src/channels/plugins/status-issues/bluebubbles.ts @@ -0,0 +1,100 @@ +import type { ChannelAccountSnapshot, ChannelStatusIssue } from "../types.js"; +import { asString, isRecord } from "./shared.js"; + +type BlueBubblesAccountStatus = { + accountId?: unknown; + enabled?: unknown; + configured?: unknown; + running?: unknown; + baseUrl?: unknown; + lastError?: unknown; + probe?: unknown; +}; + +type BlueBubblesProbeResult = { + ok?: boolean; + status?: number | null; + error?: string | null; +}; + +function readBlueBubblesAccountStatus( + value: ChannelAccountSnapshot, +): BlueBubblesAccountStatus | null { + if (!isRecord(value)) return null; + return { + accountId: value.accountId, + enabled: value.enabled, + configured: value.configured, + running: value.running, + baseUrl: value.baseUrl, + lastError: value.lastError, + probe: value.probe, + }; +} + +function readBlueBubblesProbeResult(value: unknown): BlueBubblesProbeResult | null { + if (!isRecord(value)) return null; + return { + ok: typeof value.ok === "boolean" ? value.ok : undefined, + status: typeof value.status === "number" ? value.status : null, + error: asString(value.error) ?? null, + }; +} + +export function collectBlueBubblesStatusIssues( + accounts: ChannelAccountSnapshot[], +): ChannelStatusIssue[] { + const issues: ChannelStatusIssue[] = []; + for (const entry of accounts) { + const account = readBlueBubblesAccountStatus(entry); + if (!account) continue; + const accountId = asString(account.accountId) ?? "default"; + const enabled = account.enabled !== false; + if (!enabled) continue; + + const configured = account.configured === true; + const running = account.running === true; + const lastError = asString(account.lastError); + const probe = readBlueBubblesProbeResult(account.probe); + + // Check for unconfigured accounts + if (!configured) { + issues.push({ + channel: "bluebubbles", + accountId, + kind: "config", + message: "Not configured (missing serverUrl or password).", + fix: "Run: clawdbot channels add bluebubbles --http-url --password ", + }); + continue; + } + + // Check for probe failures + if (probe && probe.ok === false) { + const errorDetail = probe.error + ? `: ${probe.error}` + : probe.status + ? ` (HTTP ${probe.status})` + : ""; + issues.push({ + channel: "bluebubbles", + accountId, + kind: "runtime", + message: `BlueBubbles server unreachable${errorDetail}`, + fix: "Check that the BlueBubbles server is running and accessible. Verify serverUrl and password in your config.", + }); + } + + // Check for runtime errors + if (running && lastError) { + issues.push({ + channel: "bluebubbles", + accountId, + kind: "runtime", + message: `Channel error: ${lastError}`, + fix: "Check gateway logs for details. If the webhook is failing, verify the webhook URL is configured in BlueBubbles server settings.", + }); + } + } + return issues; +} diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index ab13cf448..251dc0fda 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -245,3 +245,6 @@ export { normalizeWhatsAppMessagingTarget, } from "../channels/plugins/normalize/whatsapp.js"; export { collectWhatsAppStatusIssues } from "../channels/plugins/status-issues/whatsapp.js"; + +// Channel: BlueBubbles +export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js";