Skip to content

fix: harden Discord poll handling and refresh docs#407

Merged
jamiepine merged 2 commits intospacedriveapp:mainfrom
vsumner:fix/discord-poll-validation-docs
Mar 12, 2026
Merged

fix: harden Discord poll handling and refresh docs#407
jamiepine merged 2 commits intospacedriveapp:mainfrom
vsumner:fix/discord-poll-validation-docs

Conversation

@vsumner
Copy link
Contributor

@vsumner vsumner commented Mar 12, 2026

Summary

  • harden Discord poll handling so invalid poll payloads degrade safely instead of breaking replies
  • add regression coverage for Discord rich-message poll prep and quiet-mode mention/reply behavior
  • refresh Docker and Discord docs for the unified image model and quiet mode behavior

Changes

  • sanitize poll payloads in reply and validate again in the Discord adapter before send
  • share rich-message preparation across Discord respond/broadcast and derive text fallback from cards when needed
  • extract quiet-mode helpers for targeted tests and document quiet mode in the Discord setup docs
  • fix two clippy findings in send_message_to_another_channel uncovered by just gate-pr

Test Plan

  • just preflight
  • just gate-pr
  • cargo test test_prepare_rich_message_parts_drops_invalid_poll_but_keeps_text -- --nocapture
  • cargo test quiet_mode_invocation_uses_discord_mention_and_reply_metadata -- --nocapture

P1/P2 Finding Closure

  • P2 invalid Discord poll payloads no longer break replies: src/tools/reply.rs + src/messaging/discord.rs; verified by cargo test test_prepare_rich_message_parts_drops_invalid_poll_but_keeps_text -- --nocapture and just gate-pr (pass)
  • P2 quiet mode mention/reply routing now has explicit regression coverage: src/agent/channel.rs; verified by cargo test quiet_mode_invocation_uses_discord_mention_and_reply_metadata -- --nocapture and just gate-pr (pass)
  • P1 stale Docker docs updated to the unified image model: docs/docker.md, docs/content/docs/(getting-started)/docker.mdx, docs/metrics.md, docs/content/docs/(deployment)/metrics.mdx; verified by rg -n \"slim|full image|image variants\" docs/docker.md docs/metrics.md docs/content/docs/\\(getting-started\\)/docker.mdx docs/content/docs/\\(getting-started\\)/quickstart.mdx docs/content/docs/\\(deployment\\)/metrics.mdx and just gate-pr (pass; only legacy deprecation notes remain)

Residual Risk

  • Repo-wide coverage reporting still does not exist, so this improves the risky branches without quantifying overall coverage.

Note

Addresses Discord poll validation hardening, rich-message test coverage, and documentation updates for the unified image model. Includes sanitization of invalid poll payloads in both the reply tool and Discord adapter, new regression tests for quiet-mode routing, and updates to Docker/metrics documentation. All gates pass.

Written by Tembo for commit bfaf70f.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 12, 2026

Walkthrough

This PR consolidates Docker image variants (slim/full) into a unified latest image model, updates related documentation, and refactors message handling code to support Discord quiet mode functionality and poll payload normalization.

Changes

Cohort / File(s) Summary
Docker Image Unification
docs/content/docs/(deployment)/metrics.mdx, docs/content/docs/(getting-started)/docker.mdx, docs/content/docs/(getting-started)/quickstart.mdx, docs/docker.md, docs/metrics.md
Updated Docker image references from variant tags (slim/full) to unified latest image; removed image variant sections and updated build/deployment instructions to reflect single image model with on-demand browser binary caching.
Discord Quiet Mode Documentation
docs/content/docs/(messaging)/discord-setup.mdx
Added "Quiet mode" subsection describing /quiet and /active slash commands to toggle listen-only behavior where bot replies only to slash commands, mentions, and replies.
Message Invocation Logic Refactoring
src/agent/channel.rs
Extracted inline invocation logic into helper functions (compute_listen_mode_invocation, looks_like_liveness_ping, should_send_discord_quiet_mode_ping_ack, should_send_quiet_mode_fallback) and introduced QuietModeFallbackState struct; expanded tests for new helpers.
Discord Message Composition Refactor
src/messaging/discord.rs
Introduced RichMessageParts struct and prepare_rich_message_parts helper to centralize message component handling (text, cards, interactive elements, poll); refactored build_poll to return Option with stricter validation (min 2 answers, max 10, blank question rejection).
Poll Payload Normalization
src/tools/reply.rs
Added normalize_poll_payload helper function to validate and normalize poll data (trim question/answers, enforce minimum 2 answers, filter empty answers); updated ReplyTool.call to use normalized poll instead of raw argument.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #207: Aligns with Docker image variant consolidation, updating documentation and image tag semantics from slim/full to unified latest approach.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main changes: hardening Discord poll handling and refreshing documentation.
Description check ✅ Passed The description is well-structured and directly related to the changeset, providing clear summaries of fixes, tests, and verification steps for all major changes.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +224 to +245
let question = poll.question.trim().to_string();
if question.is_empty() {
return None;
}

let answers: Vec<String> = poll
.answers
.into_iter()
.map(|answer| answer.trim().to_string())
.filter(|answer| !answer.is_empty())
.collect();

if answers.len() < 2 {
return None;
}

Some(crate::Poll {
question,
answers,
allow_multiselect: poll.allow_multiselect,
duration_hours: poll.duration_hours,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor perf/readability nit: this allocates question/answer strings even for polls that get dropped. You can trim/check first and use filter_map to avoid allocating empty answers.

Suggested change
let question = poll.question.trim().to_string();
if question.is_empty() {
return None;
}
let answers: Vec<String> = poll
.answers
.into_iter()
.map(|answer| answer.trim().to_string())
.filter(|answer| !answer.is_empty())
.collect();
if answers.len() < 2 {
return None;
}
Some(crate::Poll {
question,
answers,
allow_multiselect: poll.allow_multiselect,
duration_hours: poll.duration_hours,
})
let crate::Poll {
question,
answers,
allow_multiselect,
duration_hours,
} = poll;
let question = question.trim();
if question.is_empty() {
return None;
}
let answers: Vec<String> = answers
.into_iter()
.filter_map(|answer| {
let answer = answer.trim();
(!answer.is_empty()).then(|| answer.to_string())
})
.collect();
if answers.len() < 2 {
return None;
}
Some(crate::Poll {
question: question.to_string(),
answers,
allow_multiselect,
duration_hours,
})

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/agent/channel.rs`:
- Around line 3125-3146: The helper looks_like_liveness_ping currently uses
broad substring checks and causes directed commands (e.g., "@bot ping deploy")
to be auto-acked; update looks_like_liveness_ping to operate on a normalized,
mention/reply-stripped token list (use compute_listen_mode_invocation or
replicate its mention-stripping) and only return true for exact or near-exact
liveness tokens/phrases (e.g., "ping", "are you there", "yo", "you here") after
tokenization — avoid contains() matches that match inside longer tokens (e.g.,
"shipping" or "deploy-ping"); then ensure
should_send_discord_quiet_mode_ping_ack continues to call the tightened
looks_like_liveness_ping and add a regression test that sends a substantive
directed message containing "ping" (like "@bot ping deploy-web") asserting it is
not treated as a liveness ping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f833fc5-6189-4557-a392-cc05fc53c93e

📥 Commits

Reviewing files that changed from the base of the PR and between 0a97964 and bfaf70f.

📒 Files selected for processing (10)
  • docs/content/docs/(deployment)/metrics.mdx
  • docs/content/docs/(getting-started)/docker.mdx
  • docs/content/docs/(getting-started)/quickstart.mdx
  • docs/content/docs/(messaging)/discord-setup.mdx
  • docs/docker.md
  • docs/metrics.md
  • src/agent/channel.rs
  • src/messaging/discord.rs
  • src/tools/reply.rs
  • src/tools/send_message_to_another_channel.rs

Comment on lines +3125 to +3146
fn looks_like_liveness_ping(text: &str) -> bool {
let text = text.trim().to_lowercase();
text.contains("you here")
|| text.contains("ping")
|| text.ends_with(" yo")
|| text == "yo"
|| text.contains("alive")
|| text.contains("there?")
}

fn should_send_discord_quiet_mode_ping_ack(
message: &InboundMessage,
raw_text: &str,
listen_only_mode: bool,
) -> bool {
if message.source != "discord" || !listen_only_mode {
return false;
}

let (_, invoked_by_mention, invoked_by_reply) =
compute_listen_mode_invocation(message, raw_text);
(invoked_by_mention || invoked_by_reply) && looks_like_liveness_ping(raw_text)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid auto-acking any directed message that contains ping.

looks_like_liveness_ping() uses raw substring checks (contains("ping"), contains("there?"), etc.), so messages like @bot ping deploy-web or @bot shipping ETA? will hit the fast-path and return "yeah i'm here" instead of reaching normal handling. Because both the Telegram shortcut and the Discord quiet-mode shortcut return early on this helper, this can swallow legitimate user requests. Please tighten this to exact/near-exact liveness phrases after tokenization/mention stripping, and add a negative regression for a substantive directed message that contains ping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/channel.rs` around lines 3125 - 3146, The helper
looks_like_liveness_ping currently uses broad substring checks and causes
directed commands (e.g., "@bot ping deploy") to be auto-acked; update
looks_like_liveness_ping to operate on a normalized, mention/reply-stripped
token list (use compute_listen_mode_invocation or replicate its
mention-stripping) and only return true for exact or near-exact liveness
tokens/phrases (e.g., "ping", "are you there", "yo", "you here") after
tokenization — avoid contains() matches that match inside longer tokens (e.g.,
"shipping" or "deploy-ping"); then ensure
should_send_discord_quiet_mode_ping_ack continues to call the tightened
looks_like_liveness_ping and add a regression test that sends a substantive
directed message containing "ping" (like "@bot ping deploy-web") asserting it is
not treated as a liveness ping.

@jamiepine jamiepine enabled auto-merge March 12, 2026 09:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/agent/channel.rs (1)

3178-3200: ⚠️ Potential issue | 🟠 Major

Tighten liveness matching to exact phrases.

looks_like_liveness_ping() still relies on broad substring checks (contains("ping"), contains("there?"), etc.), so substantive directed messages like @bot ping deploy-web or shipping ETA? can still short-circuit into "yeah i'm here" via the Telegram path at Line 1583 and the Discord quiet-mode path at Lines 1592-1595. Please switch this to normalized exact/near-exact liveness phrases and add a negative regression for a directed message that merely contains ping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/channel.rs` around lines 3178 - 3200, The liveness matcher
looks_like_liveness_ping currently uses broad substring checks that misidentify
directed messages (e.g., "@bot ping deploy-web"); change it to normalize the
input (trim, lowercase) and match only exact or very near-exact phrases (e.g.,
"ping", "yo", "are you there", "you here", "alive?") rather than
contains("ping") or contains("there?"); update
should_send_discord_quiet_mode_ping_ack to rely on the tightened
looks_like_liveness_ping and ensure compute_listen_mode_invocation behavior is
preserved, and add a negative regression test asserting that messages like "@bot
ping deploy-web" (and "shipping ETA?") do NOT trigger liveness responses.
🧹 Nitpick comments (1)
src/tools/reply.rs (1)

222-245: Keep reply-tool poll normalization aligned with the Discord validator.

normalize_poll_payload() now trims/filter-validates content, but src/messaging/discord.rs:1172-1208 still applies extra rules (take(10) and duration_hours.clamp(1, 720)). That means the reply tool can return success for a poll payload that the Discord adapter silently changes on send. I’d rather share one normalizer/validator, or mirror those limits here and add a regression for >10 answers / out-of-range duration so the tool and adapter stay in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/reply.rs` around lines 222 - 245, normalize_poll_payload currently
trims and filters answers but does not enforce the same limits as the Discord
adapter (it omits the answers cap and duration clamp), causing divergence;
update normalize_poll_payload to apply answers.truncate(10) or reject when
answers.len() > 10 (prefer rejecting to surface validation errors) and clamp
duration_hours to 1..=720 (or reject if out of range) so the reply tool and
src/messaging/discord.rs behavior match, and add regression tests that submit
>10 answers and out-of-range duration to ensure normalization/validation stays
in sync with the Discord adapter.
🤖 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/agent/channel.rs`:
- Around line 3178-3200: The liveness matcher looks_like_liveness_ping currently
uses broad substring checks that misidentify directed messages (e.g., "@bot ping
deploy-web"); change it to normalize the input (trim, lowercase) and match only
exact or very near-exact phrases (e.g., "ping", "yo", "are you there", "you
here", "alive?") rather than contains("ping") or contains("there?"); update
should_send_discord_quiet_mode_ping_ack to rely on the tightened
looks_like_liveness_ping and ensure compute_listen_mode_invocation behavior is
preserved, and add a negative regression test asserting that messages like "@bot
ping deploy-web" (and "shipping ETA?") do NOT trigger liveness responses.

---

Nitpick comments:
In `@src/tools/reply.rs`:
- Around line 222-245: normalize_poll_payload currently trims and filters
answers but does not enforce the same limits as the Discord adapter (it omits
the answers cap and duration clamp), causing divergence; update
normalize_poll_payload to apply answers.truncate(10) or reject when
answers.len() > 10 (prefer rejecting to surface validation errors) and clamp
duration_hours to 1..=720 (or reject if out of range) so the reply tool and
src/messaging/discord.rs behavior match, and add regression tests that submit
>10 answers and out-of-range duration to ensure normalization/validation stays
in sync with the Discord adapter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1231b545-b63a-4c45-bd84-dea99b99df01

📥 Commits

Reviewing files that changed from the base of the PR and between bfaf70f and a0b5ab1.

📒 Files selected for processing (2)
  • src/agent/channel.rs
  • src/tools/reply.rs

@jamiepine jamiepine disabled auto-merge March 12, 2026 10:40
@jamiepine jamiepine merged commit ccdbce5 into spacedriveapp:main Mar 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants