feat(tools/bb): reply, edit, and unsend message actions#2583
feat(tools/bb): reply, edit, and unsend message actions#2583maxtongwang wants to merge 90 commits intozeroclaw-labs:mainfrom
Conversation
Adds @claude mention support on PRs using anthropics/claude-code-action@v1. Mirrors the workflow from maxtongwang/openclaw. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds use_sticky_comment: true to claude-review workflow so all review updates are consolidated into a single comment instead of flooding the PR with multiple comments. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds BlueBubblesChannel — iMessage via self-hosted BlueBubbles macOS server. - Webhook mode: receives new-message events at POST /bluebubbles - Sender normalization: strips imessage:/sms:/auto: prefixes, lowercases emails - fromMe caching: FIFO VecDeque+HashMap cache with reply context injection - Optional inbound webhook auth: Authorization: Bearer via webhook_secret config - Outbound send: POST /api/v1/message/text with ?password= query param + tempGuid - Health check: GET /api/v1/ping (matches OpenClaw probeBlueBubbles) - constant_time_eq() for secret comparison; manual Debug redacts password Closes #1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a second BlueBubbles endpoint for a personal Apple ID that stores incoming iMessages to shared memory but never calls the LLM or sends a reply. This enables multi-tenant iMessage setups where one ZeroClaw process handles both a bot account (POST /bluebubbles) and a personal account (POST /bluebubbles-personal) from two BlueBubbles instances. Changes: - src/config/schema.rs: add bluebubbles_personal: Option<BlueBubblesConfig> to ChannelsConfig (reuses existing BlueBubblesConfig struct) - src/gateway/mod.rs: add bluebubbles_personal field to AppState, wire construction, register POST /bluebubbles-personal route, and implement handle_bluebubbles_personal_webhook handler Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- [build] rustc-wrapper = "sccache" in .cargo/config.toml — caches compiled crates across clean builds; sccache auto-starts on first use - [profile.dev] split-debuginfo = "unpacked" in Cargo.toml — skips dsymutil on macOS, eliminating post-link debug-symbol packaging delay Note: mold 2.40.4 dropped Mach-O support so it cannot be used as a macOS linker; split-debuginfo is the primary link-time win on macOS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert Discord-style markdown in LLM responses to BB Private API
attributedBody segments for iMessage rendering:
- **bold** → {bold: true}
- *italic* → {italic: true}
- ~~strikethrough~~ → {strikethrough: true}
- __underline__ → {underline: true}
Markers nest arbitrarily; plain-text fallback strips markers.
Update system prompt to instruct LLM to use rich markdown + emoji.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add start_typing/stop_typing to BlueBubblesChannel using BB Private API
POST /api/v1/chat/{chatGuid}/typing. Indicator refreshes every 4s since
BB typing events expire in ~5s. Gateway handler starts indicator before
LLM call and stops it (both success and error paths) before sending reply.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- markdown_to_attributed_body: convert **bold**, *italic*, ~~strike~~, __underline__, `code` (→bold), # headers (→bold), ``` blocks (plain) into BB Private API attributedBody segments - start_typing/stop_typing: background loop refreshing BB typing indicator every 4s while LLM processes; aborted on response - EFFECT_MAP + extract_effect(): LLM can append [EFFECT:name] tags (slam, loud, gentle, invisible-ink, confetti, balloons, fireworks, lasers, love, celebration, echo, spotlight); stripped from content, passed as effectId to BB Private API - Updated channel_delivery_instructions with full style + effect guide - 38 unit tests passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ignore_senders to BlueBubblesConfig and BlueBubblesChannel - Personal webhook handler skips storing messages where sender is in ignore_senders - Configure ignore_senders = ["tongtong901005@gmail.com"] in config.toml - Fix release-fast profile: lto=false, codegen-units=16, opt-level=2 for fast local builds with sccache support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Direct LLM to use web_fetch on canonical sources (ESPN, wttr.in, etc.) for sports, weather, news — not just web_search snippets - Remove "be concise" instruction that was cutting the tool loop short - Instruct bot to complete full research before replying - Keep all text style and effect guidance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l endpoint Port all three CodeRabbit improvements from feat/channel-bluebubbles to fork main and extend them consistently to the bluebubbles_personal endpoint: - typing_handles: replace single Mutex<Option<JoinHandle>> with Mutex<HashMap<String, JoinHandle>> keyed by chat GUID so concurrent conversations do not cancel each other's typing indicators - is_sender_ignored(): new method checked before is_sender_allowed() in parse_webhook_payload so ignore_senders always takes precedence over the allowlist; removes the now-redundant inline ignore check from the personal handler since parse_webhook_payload handles it upstream - Secret wiring: add bluebubbles and bluebubbles_personal password + webhook_secret to decrypt_channel_secrets and encrypt_channel_secrets - Debug impl: add ignore_senders field to BlueBubblesConfig debug output - Tests: add three unit tests covering ignore_senders exact match, precedence over allowlist, and empty-list no-op behaviour (41 total) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Conflicts resolved: - .gitignore: keep fork's test-config.toml entry alongside upstream result - Cargo.lock: accept upstream (no new deps from BB channel) - src/config/schema.rs: keep BlueBubblesConfig + bluebubbles/personal fields alongside upstream GitHubConfig; wire both into encrypt/decrypt - src/gateway/mod.rs: keep BB + personal handlers alongside upstream GitHub handler; update run_gateway_chat_with_tools calls to 3-arg signature; add missing BB fields to new GitHub test fixtures Also fixes: - src/providers/cursor.rs: add missing quota_metadata field (pre-existing upstream bug now caught by stricter ChatResponse struct) - BB handler: update sanitize_gateway_response to 3-arg form matching upstream API change (adds leak_guard parameter) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BlueBubbles channel now downloads and transcribes audio attachments when `[transcription]` is configured. iMessage sends voice memos in Core Audio Format (CAF) which Groq Whisper does not accept natively; CAF files are converted to WAV via ffmpeg before upload. Changes: - transcription.rs: add `convert_caf_to_wav` — detects CAF by extension, writes to temp files (seekable input required), runs `ffmpeg -ar 16000 -ac 1`, cleans up on all exit paths; returns Ok(None) for non-CAF so callers reuse original bytes - bluebubbles.rs: add `transcription` field, `with_transcription` builder, `AudioAttachment` struct, `extract_audio_attachments`, `download_attachment` (percent-encoding inline), `download_and_transcribe`, and public async `parse_webhook_payload_with_transcription`; gracefully falls back to `<media:audio>` placeholder when transcription is disabled, ffmpeg is absent, or the API call fails - gateway/mod.rs: wire `config.transcription.clone()` into both BB channel constructions; replace synchronous `parse_webhook_payload` call with async `parse_webhook_payload_with_transcription` in both webhook handlers No new Cargo dependencies — uses tokio::process and tokio::fs already present. No new config keys — wired through the existing `[transcription]` block. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors OpenClaw approach: read attachment from `original_path` in BB webhook payload (local filesystem, no REST download) and shell out to the `whisper` CLI. Python whisper handles CAF (iMessage voice memos) natively via ffmpeg — no pre-conversion step needed. - Remove convert_caf_to_wav + ffmpeg + Groq HTTP download path - Add transcribe_audio_local: resolves whisper in PATH and common Homebrew/system paths, runs with --model turbo --output_format txt, reads <tmpdir>/<stem>.txt, cleans up on all exit paths - extract_audio_attachments: use originalPath instead of guid - transcribe_local: read file directly, no REST API call - Fix pre-existing build break: add OauthConfig/GoogleOauthConfig schema types + re-export so oauth.rs compiles Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
BB webhook attachments do not include originalPath; extract guid instead
and download bytes via /api/v1/attachment/{guid}/download, then write to
a temp file and pass to the local whisper CLI for transcription.
BB converts CAF→MP3 before webhooking so the download is typically
audio/mpeg — no pre-conversion needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. Instead of REST API download, read the BB-converted MP3 directly from
the local filesystem at the predictable path:
~/Library/Application Support/bluebubbles-server/Convert/{guid}/{name}.mp3
BB converts CAF→MP3 before sending the webhook; since ZeroClaw runs on
the same Mac as BB, no network call is needed.
2. Spawn webhook processing in background (tokio::spawn) so the handler
returns 200 immediately. BB times out webhooks at 30 s, but whisper
transcription takes longer — caused 408 timeouts with the previous
synchronous approach.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BB's transferName in webhooks is inconsistent — sometimes includes .mp3
already, causing triple-extension paths. Instead of constructing the
filename from transferName, scan the Convert/{guid}/ directory for the
first audio file (mp3/m4a/aac/wav/ogg). This is robust to any BB
filename convention.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace incorrect \!escaped.contains('&') assertion — HTML entity encoding uses & characters (&, <, etc.), so this always failed. Assert contains("&") instead.
Replaces broken filesystem-based audio transcription with BB REST API download. Adds dual-backend transcription (local whisper CLI primary, Groq API fallback), inbound tapback surfacing via updated-message webhooks, and outbound reactions via add_reaction/remove_reaction on the Channel trait. Key changes: - download_attachment_bytes: 25 MB cap (Content-Length + body), 30s HTTP timeout - resolve_whisper_bin: OnceLock cache — binary search runs once at startup - parse_tapback_event: surfaces ❤️/👍/👎/😂/‼️ /❓ as [Tapback] system messages - add_reaction/remove_reaction: BB Private API /api/v1/message/react - 58 tests, all green
fetch_with_http_provider used Policy::none() but returned Ok(redirected_url) instead of fetching the target. Replace response with the fetched redirect target and fall through to shared body processing.
Brings in upstream commits through 3726d82. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> # Conflicts: # .cargo/config.toml # .gitignore # Cargo.toml # src/channels/bluebubbles.rs # src/channels/mod.rs # src/config/mod.rs # src/config/schema.rs # src/gateway/mod.rs
…nc parse Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng indicator (#14) - Replace slow Python whisper (~30–90 s) with whisper-cli (whisper.cpp, Metal-accelerated, ~1–2 s) for local audio transcription. - Pre-convert all non-WAV audio to 16 kHz mono WAV via ffmpeg; the brew build of whisper-cli only reliably reads WAV. - Use OnceLock for binary/model path resolution to avoid repeated lookups. - Start typing indicator before transcription runs so users see feedback immediately rather than waiting for the full whisper phase. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…k reactions, and whisper-cli fast-path Extends the BlueBubbles iMessage channel with the following capabilities on top of the existing base implementation: - REST API audio download: fetches voice memo attachments directly from the BlueBubbles server via its REST API, enabling reliable access without filesystem access. - Tapback reactions: parses and surfaces reaction events (heart, thumbs up, thumbs down, ha ha, !!, ?) so the agent can understand user sentiment and react appropriately. - Rich text / attributed body: parses Apple's attributedBody format to extract plain text from styled iMessage content, improving message fidelity for formatted messages and inline attachments. - Typing indicator before transcription: sends a typing indicator immediately when a voice memo is detected, giving users real-time feedback before the potentially slower transcription step. - Local whisper-cli fast-path (whisper.cpp): prefers the locally installed `whisper-cli` (from whisper.cpp, e.g. via `brew install whisper-cpp`) over the Groq API when available. On Apple Silicon with Metal acceleration this cuts transcription latency from 30-90 s to ~1-2 s. WAV files are passed directly; non-WAV audio is converted via ffmpeg before passing to whisper-cli. Falls back to the Groq API automatically when whisper-cli is not installed. - Stderr diagnostics: surfaces whisper-cli stderr output in agent logs when transcription fails, making it easier to debug environment issues. Files changed: - src/channels/bluebubbles.rs: AudioAttachment struct, REST download, tapback parsing, rich text extraction, typing indicator wiring - src/channels/transcription.rs: local whisper-cli fast-path with ffmpeg pre-conversion and graceful Groq API fallback - src/gateway/mod.rs: wire .with_transcription() into channel construction; call parse_webhook_payload_with_transcription (async)
- Redact reqwest password URL from error messages with `.without_url()` in download_attachment_bytes, add_reaction, and remove_reaction - Replace racy `as_nanos()` temp-file names with `uuid::Uuid::new_v4()` in bluebubbles.rs (audio download) and transcription.rs (ffmpeg/whisper) - Gate typing indicator on fromMe/allowlist checks before `start_typing()` to avoid leaking presence for ignored/disallowed senders - Add `tokio::time::timeout(120s)` + `kill_on_drop(true)` for ffmpeg, whisper-cli, and python whisper subprocesses to bound execution time - Add Intel Homebrew model paths (`/usr/local/opt/whisper-cpp/...`) to MODELS array alongside Apple Silicon paths - Move transcription+LLM loop into `tokio::spawn` background task and return 202 ACCEPTED immediately to stay within gateway timeout budget Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t build and icon match arm
Adds BlueBubblesSendAttachmentTool allowing the LLM to send images, audio, and documents via iMessage through the BB Private API multipart endpoint (/api/v1/message/attachment). Parameters: chat_guid, filename, data_base64, mime_type, caption (optional), as_voice (optional). Base64 is decoded before upload. Invalid base64 returns a structured error without making a network call. Registered in all_tools_with_runtime alongside BlueBubblesGroupTool when channels_config.bluebubbles is present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 50 MiB size cap on data_base64 before base64 decode (OOM guard) - MIME type failure is now a hard error (was: silent empty-file fallback) - Remove mime_type from schema required[] — it defaults to octet-stream - Update mime_type description to document the default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace .expect() with .unwrap_or_else(|_| reqwest::Client::new()) so client-build failure (TLS misconfiguration) degrades gracefully instead of panicking at tool registration time - Add .trim() to chat_guid and filename validation to reject whitespace- only inputs consistently with other BB tool validators Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Result on transport error
… and mime_type inputs
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
BlueBubbles' Private API voice memo flag expects an audio attachment. Validate mime_type starts with "audio/" when as_voice=true and return a clear ToolResult error instead of silently sending an invalid request.
Adds BlueBubblesMessageTool with action-dispatch pattern for three iMessage mutation operations: reply (thread-aware), edit message text, and retract/unsend a sent message. Registered in tool factory when BlueBubbles channel is configured. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Require non-empty action with fail-fast guard (was: silent default) - Trim message_id before empty check - Make chat_guid optional in schema; validate + use only inside reply branch (edit and unsend only need message_id — forcing chat_guid confused LLM agents) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Trim text before empty check in reply and edit (consistent with action/message_id/chat_guid pattern) - Add comment on partIndex: 0 to clarify multi-part unsend not supported Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace .expect() with .unwrap_or_else(|_| reqwest::Client::new()) so client-build failure (TLS misconfiguration) degrades gracefully instead of panicking at tool registration time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ths and mod.rs registration
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
♻️ Duplicate comments (6)
src/tools/bluebubbles_send_attachment.rs (2)
161-165:⚠️ Potential issue | 🟡 MinorTrim
captionbefore using it in the multipart form.Whitespace-only captions currently pass through and are sent as message text. Trim at ingestion to keep sanitization consistent with other string inputs.
As per coding guidelines: `src/tools/**/*.rs` requires validating and sanitizing all inputs.🧼 Suggested patch
let caption = args .get("caption") .and_then(|v| v.as_str()) + .map(str::trim) .unwrap_or("") .to_string();Also applies to: 217-219
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/bluebubbles_send_attachment.rs` around lines 161 - 165, The caption string extracted via args.get("caption").and_then(|v| v.as_str()).unwrap_or("").to_string() must be trimmed immediately to strip whitespace-only values; replace the current assignment with one that calls .trim() (and consider .to_string() after trimming) so empty/whitespace captions become empty strings before being added to the multipart form, and apply the same trimming/sanitization to the other caption extraction occurrences around the code referenced (similar to lines 217-219) to keep input handling consistent; update any downstream logic that checks for empty captions to rely on the trimmed value (e.g., the local variable caption).
128-191:⚠️ Potential issue | 🟡 MinorEnforce a hard decoded-byte limit after base64 decode.
Line 128 checks encoded size only. Add an explicit
file_bytes.len()cap after Line 182 so boundary behavior is exact and deterministic.As per coding guidelines: `src/{security,runtime,gateway,tools}/**/*.rs` changes should include boundary/failure-mode validation.📏 Suggested patch
- const MAX_ATTACHMENT_B64_LEN: usize = 34 * 1024 * 1024; // ~25 MiB decoded (base64 overhead ~4/3) + const MAX_ATTACHMENT_BYTES: usize = 25 * 1024 * 1024; + const MAX_ATTACHMENT_B64_LEN: usize = ((MAX_ATTACHMENT_BYTES + 2) / 3) * 4; @@ let file_bytes = match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data_b64) { Ok(b) => b, @@ }; + if file_bytes.len() > MAX_ATTACHMENT_BYTES { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!( + "decoded attachment exceeds maximum allowed size ({MAX_ATTACHMENT_BYTES} bytes)" + )), + }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/bluebubbles_send_attachment.rs` around lines 128 - 191, The code only enforces an encoded-size cap (MAX_ATTACHMENT_B64_LEN) but not a hard decoded-byte limit; after the base64 decode that produces file_bytes (the Ok(b) arm in the match that assigns file_bytes) add a boundary check like a MAX_ATTACHMENT_DECODED_LEN constant and if file_bytes.len() > MAX_ATTACHMENT_DECODED_LEN return the same ToolResult failure shape with an error message indicating the decoded attachment exceeds the allowed size; place this check immediately after the decode success (where file_bytes is assigned) so failure-mode is deterministic and consistent with other validations (reference the file_bytes variable and the base64::Engine::decode match).src/gateway/mod.rs (4)
2377-2378:⚠️ Potential issue | 🟡 MinorFix stale response-code comment (200 vs 202).
The comment says “Returning 200 immediately”, but the handler returns
StatusCode::ACCEPTED(202) at Line 2485.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/mod.rs` around lines 2377 - 2378, The comment above the handler is stale: it says “Returning 200 immediately” but the code returns StatusCode::ACCEPTED (202). Update the comment to reflect the actual response code (202 / StatusCode::ACCEPTED) and/or reference "Accepted" semantics so it matches the handler that returns StatusCode::ACCEPTED (e.g., in the async request handler where the comment appears).
562-591:⚠️ Potential issue | 🟠 MajorDo not silently disable BlueBubbles on constructor failure.
Line 590-Line 591 converts initialization errors into
None, which can hide misconfiguration and continue with reduced capabilities.Suggested fix
let bluebubbles_channel: Option<Arc<BlueBubblesChannel>> = config .channels_config .bluebubbles .as_ref() @@ .transpose() - .map_err(|e| { - tracing::error!("Failed to initialize BlueBubbles channel: {e}"); - e - }) - .ok() - .flatten(); + .map_err(|e| anyhow::anyhow!("Failed to initialize BlueBubbles channel: {e}"))?;As per coding guidelines:
src/**/*.rs: “Prefer explicitbail!/errors for unsupported or unsafe states; never silently broaden permissions/capabilities.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/mod.rs` around lines 562 - 591, The current chain around BlueBubblesChannel::new maps initialization errors into None (bluebubbles_channel) which silently disables the channel; instead, surface the error so initialization fails loudly: remove the .ok().flatten() pattern and the .map_err(...).ok() swallowing, and return or propagate the error (e.g., using anyhow::bail! or ? from the surrounding function) when BlueBubblesChannel::new (and its .with_policies/.with_transcription) fails, so misconfiguration does not silently continue with reduced capabilities.
2381-2482:⚠️ Potential issue | 🟠 MajorAdd an outer timeout to bound semaphore permit lifetime.
The detached worker keeps the permit until the task finishes; if downstream work hangs, permits can be pinned indefinitely and eventually starve the pool.
Suggested fix
tokio::spawn(async move { let _permit = permit; // released when this task completes - let messages = bb.parse_webhook_payload_with_transcription(&payload).await; + let work = async { + let messages = bb.parse_webhook_payload_with_transcription(&payload).await; - // Delivery receipt: send immediately for ALL inbound group messages, whether - // or not they pass the mention gate. This shows "Delivered" on the sender's - // side as long as ZeroClaw is online. DM delivery is handled by the OS/iMessage - // stack automatically; we only need to fire it explicitly for group chats. - { + // existing background processing body... + { // existing logic unchanged - } + } - for msg in &messages { - // existing logic unchanged - } + for msg in &messages { + // existing logic unchanged + } + }; + + if tokio::time::timeout(Duration::from_secs(180), work) + .await + .is_err() + { + tracing::error!("BlueBubbles background worker timed out"); + } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/mod.rs` around lines 2381 - 2482, The spawned worker currently captures the semaphore permit via let _permit = permit and holds it until the whole async task finishes, so add an outer timeout (using tokio::time::timeout) around the task's main work to bound how long the permit can be held: wrap the body that starts with messages = bb.parse_webhook_payload_with_transcription(&payload).await and includes the for msg in &messages loop and run_gateway_chat_with_tools(...) in a tokio::time::timeout(duration, async move { ... }).await, handle the Err(elapsed) case by logging the timeout and explicitly dropping the permit (i.e., ensuring _permit is dropped) so the semaphore is released, and keep the normal success path unchanged so permit is released when the task completes.
2358-2375:⚠️ Potential issue | 🟠 MajorPlease add explicit tests for the new BlueBubbles
202/503boundary behavior.This path introduces important queue-boundary semantics (accepted vs exhausted) but there is no direct assertion in this file’s tests for those outcomes.
Based on learnings: for
src/security/**/*.rs|src/runtime/**/*.rs|src/gateway/**/*.rs|src/tools/**/*.rs, include at least one boundary/failure-mode validation and document rollback strategy.Also applies to: 2484-2487
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/mod.rs` around lines 2358 - 2375, Add explicit unit/integration tests that assert the BlueBubbles webhook path returns 202 when BB_WORKER_SEMAPHORE.try_acquire_owned() succeeds and returns StatusCode::SERVICE_UNAVAILABLE (503) with the JSON error payload when the semaphore is exhausted; simulate both cases by mocking or temporarily replacing BB_WORKER_SEMAPHORE to provide an owned permit and to force try_acquire_owned() -> Err, then call the handler (the function invoking try_acquire_owned) and assert the response status and body match the expected 202/JSON and 503/{"error":"worker pool exhausted, retry later"} outcomes; also add a short doc comment or test metadata describing the rollback/retry strategy for the queued work in failure mode so the boundary behavior is documented alongside the tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/gateway/mod.rs`:
- Around line 2377-2378: The comment above the handler is stale: it says
“Returning 200 immediately” but the code returns StatusCode::ACCEPTED (202).
Update the comment to reflect the actual response code (202 /
StatusCode::ACCEPTED) and/or reference "Accepted" semantics so it matches the
handler that returns StatusCode::ACCEPTED (e.g., in the async request handler
where the comment appears).
- Around line 562-591: The current chain around BlueBubblesChannel::new maps
initialization errors into None (bluebubbles_channel) which silently disables
the channel; instead, surface the error so initialization fails loudly: remove
the .ok().flatten() pattern and the .map_err(...).ok() swallowing, and return or
propagate the error (e.g., using anyhow::bail! or ? from the surrounding
function) when BlueBubblesChannel::new (and its
.with_policies/.with_transcription) fails, so misconfiguration does not silently
continue with reduced capabilities.
- Around line 2381-2482: The spawned worker currently captures the semaphore
permit via let _permit = permit and holds it until the whole async task
finishes, so add an outer timeout (using tokio::time::timeout) around the task's
main work to bound how long the permit can be held: wrap the body that starts
with messages = bb.parse_webhook_payload_with_transcription(&payload).await and
includes the for msg in &messages loop and run_gateway_chat_with_tools(...) in a
tokio::time::timeout(duration, async move { ... }).await, handle the
Err(elapsed) case by logging the timeout and explicitly dropping the permit
(i.e., ensuring _permit is dropped) so the semaphore is released, and keep the
normal success path unchanged so permit is released when the task completes.
- Around line 2358-2375: Add explicit unit/integration tests that assert the
BlueBubbles webhook path returns 202 when
BB_WORKER_SEMAPHORE.try_acquire_owned() succeeds and returns
StatusCode::SERVICE_UNAVAILABLE (503) with the JSON error payload when the
semaphore is exhausted; simulate both cases by mocking or temporarily replacing
BB_WORKER_SEMAPHORE to provide an owned permit and to force try_acquire_owned()
-> Err, then call the handler (the function invoking try_acquire_owned) and
assert the response status and body match the expected 202/JSON and
503/{"error":"worker pool exhausted, retry later"} outcomes; also add a short
doc comment or test metadata describing the rollback/retry strategy for the
queued work in failure mode so the boundary behavior is documented alongside the
tests.
In `@src/tools/bluebubbles_send_attachment.rs`:
- Around line 161-165: The caption string extracted via
args.get("caption").and_then(|v| v.as_str()).unwrap_or("").to_string() must be
trimmed immediately to strip whitespace-only values; replace the current
assignment with one that calls .trim() (and consider .to_string() after
trimming) so empty/whitespace captions become empty strings before being added
to the multipart form, and apply the same trimming/sanitization to the other
caption extraction occurrences around the code referenced (similar to lines
217-219) to keep input handling consistent; update any downstream logic that
checks for empty captions to rely on the trimmed value (e.g., the local variable
caption).
- Around line 128-191: The code only enforces an encoded-size cap
(MAX_ATTACHMENT_B64_LEN) but not a hard decoded-byte limit; after the base64
decode that produces file_bytes (the Ok(b) arm in the match that assigns
file_bytes) add a boundary check like a MAX_ATTACHMENT_DECODED_LEN constant and
if file_bytes.len() > MAX_ATTACHMENT_DECODED_LEN return the same ToolResult
failure shape with an error message indicating the decoded attachment exceeds
the allowed size; place this check immediately after the decode success (where
file_bytes is assigned) so failure-mode is deterministic and consistent with
other validations (reference the file_bytes variable and the
base64::Engine::decode match).
ℹ️ Review info
Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 52da9d61-69fd-4db9-b69d-ba47be72d4e0
📒 Files selected for processing (8)
docs/channels-reference.mddocs/i18n/vi/channels-reference.mdsrc/channels/bluebubbles.rssrc/gateway/mod.rssrc/tools/bluebubbles_group.rssrc/tools/bluebubbles_message.rssrc/tools/bluebubbles_send_attachment.rssrc/tools/mod.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- src/tools/mod.rs
- src/tools/bluebubbles_group.rs
- docs/i18n/vi/channels-reference.md
…eturn bypass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- channels/bluebubbles: write temp audio file with 0600 permissions to protect voice content on shared hosts; fail-fast validation for add/remove_reaction - gateway/oauth: remove unused redirect field from OAuthStartQuery, fix TOCTOU race in revoke path, log Google token revocation failures - gateway/mod: hash sender PII before logging, fire-and-forget mark_read calls
…-reference - Add §4.1 BlueBubbles config/policy section to el/channels-reference.md - Add §4.15 BlueBubbles config/policy section to vi/channels-reference.md with policy table matching EN runtime contract - Addresses CodeRabbit finding: delivery matrix shows BlueBubbles transport but localized docs lacked matching config contract documentation
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
|
Closing — no longer needed. |
Summary
mainbluebubbles_messagetool with a singleactiondispatch (reply,edit,unsend). Uses BB private-API endpoints: reply viaselectedMessageGuid, edit via/message/{guid}/edit, unsend via/message/{guid}/unsend. Registered in the tool factory alongside group and send-attachment tools.Label Snapshot (required)
risk: mediumsize: Stooltool: bluebubblesChange Metadata
featuretoolLinked Issue
Supersede Attribution (required when
Supersedes #is used)N/A
Validation Evidence (required)
Security Impact (required)
Privacy and Data Hygiene (required)
Compatibility / Migration
i18n Follow-Through (required when docs or user-facing wording changes)
Human Verification (required)
reply,edit,unsend) schema validated against BB API docs;partIndexdefault0documented.message_id, missingtextfor reply/edit, unknown action returns structured error.Side Effects / Blast Radius (required)
src/tools/(new file + mod.rs registration).Agent Collaboration Notes (recommended)
AGENTS.md+CONTRIBUTING.md.Rollback Plan (required)
src/tools/bluebubbles_message.rs+ three-line mod.rs change.BlueBubblesMessageToolabsent from tool list; API failures surface asToolResult { success: false }.Risks and Mitigations
unsendon already-read messages may fail silently on older iOS.ToolResult { success: false, error: Some(...) }.Summary by CodeRabbit
New Features
Documentation
Bug Fixes
Review Fixes (Round 2)
Propagated docs fix from base branch
feat/bluebubbles-transcription-tapbackvia rebase:Chinese command text in English docs:
docs/channels-reference.mdnatural-language approval command examples updated from授权工具 shelltoapprove tool shell.Review Fixes (Round 3)
No-panic constructor:
bluebubbles_message.rsreplaced.expect("valid reqwest client config")with.unwrap_or_else(|_| reqwest::Client::new())to prevent panic on TLS misconfiguration.Also propagated from base branches: transcription temp dir cleanup, error_page title HTML escaping, icon size check on decoded bytes.
Review Fixes (Round 6)
oauth.rs: propagate HTTP client build failure — no silent timeout lossoauth.rs: validate PKCE state before consuming session file (CSRF fix)oauth.rs: use POST form body for token revocation (RFC 7009 compliance)transcription.rs: explicit fail on non-UTF-8 temp paths, not empty-string passthroughtranscription.rs: clean up partial ffmpeg WAV on conversion failurebluebubbles.rs: remove attachment GUID from error logclaude-review.yml: fix step-level PR condition (!= ''→!= null)Review Fixes (Round 7)
bluebubbles_message.rs: explicitanyhow::Result<Self>constructorbluebubbles_message.rs: all 3 transport error paths (reply, edit, unsend) returnOk(ToolResult { success: false })instead of propagatingErrsrc/tools/mod.rs: registration updated to handleanyhow::ResultconstructorReview Fixes (Round 8)
bluebubbles_message.rs: injectArc<SecurityPolicy>— addedcan_act()andrecord_action()checks matching the pattern used by all other side-effect tools (ShellTool,FileReadTool,CronAddTool, etc.)Review Fixes (cargo fmt)
cargo fmt --allto files changed in this PR's diff — collapsed single-use iterator chains and reformatted match arm bodies to comply with project rustfmt settings