From 7d938bfa209a97f129f2f1c2df6802a3cbb8437a Mon Sep 17 00:00:00 2001 From: daopunk Date: Fri, 14 Aug 2026 01:13:52 -0500 Subject: [PATCH] chore: drop leftover two-bot copy and vestigial role wiring Live product is one Signal number. Stop keying menus and invite policy off a retired transcription role. --- crates/signal-bot/src/commands/help.rs | 62 +++++++++---------- crates/signal-bot/src/commands/menu_locale.rs | 29 ++++----- .../signal-bot/src/commands/product_menus.rs | 21 +++++-- crates/signal-bot/src/commands/verify.rs | 42 +++++-------- .../signal-bot/src/group_invite_acceptor.rs | 20 ------ crates/signal-bot/src/handlers_setup.rs | 12 +--- crates/signal-bot/src/main.rs | 4 +- docs/language-threads.md | 6 +- docs/solutions/README.md | 2 +- docs/solutions/signal-mobile-menus.md | 4 +- 10 files changed, 84 insertions(+), 118 deletions(-) diff --git a/crates/signal-bot/src/commands/help.rs b/crates/signal-bot/src/commands/help.rs index 9aa8dfa..2a29b74 100644 --- a/crates/signal-bot/src/commands/help.rs +++ b/crates/signal-bot/src/commands/help.rs @@ -4,7 +4,6 @@ use crate::commands::menu_locale::{ help_menu, info_menu, is_exact_command, thread_help_menu, thread_info_menu, }; use crate::commands::CommandHandler; -use crate::config::BotRole; use crate::error::AppResult; use crate::group_preferences_store::GroupPreferencesStore; use async_trait::async_trait; @@ -13,13 +12,17 @@ use std::sync::Arc; const NOT_THREAD_MSG: &str = "!commands is only available in a Language Thread."; -pub struct HelpHandler { - role: BotRole, -} +pub struct HelpHandler; impl HelpHandler { - pub fn new(role: BotRole) -> Self { - Self { role } + pub fn new() -> Self { + Self + } +} + +impl Default for HelpHandler { + fn default() -> Self { + Self::new() } } @@ -36,7 +39,7 @@ impl CommandHandler for HelpHandler { async fn execute(&self, message: &BotMessage) -> AppResult { let _ = message; - Ok(help_menu(self.role).into()) + Ok(help_menu().into()) } } @@ -75,12 +78,11 @@ impl CommandHandler for CommandsHandler { /// Same menus as [`HelpHandler`], with per-command explanations and blank-line breaks. pub struct InfoHandler { group_prefs: Arc, - role: BotRole, } impl InfoHandler { - pub fn new(group_prefs: Arc, role: BotRole) -> Self { - Self { group_prefs, role } + pub fn new(group_prefs: Arc) -> Self { + Self { group_prefs } } } @@ -95,14 +97,12 @@ impl CommandHandler for InfoHandler { } async fn execute(&self, message: &BotMessage) -> AppResult { - if self.role == BotRole::Translation { - if let Some(group_id) = message.group_id.as_deref() { - if self.group_prefs.lookup_sidecar(group_id).is_some() { - return Ok(thread_info_menu().into()); - } + if let Some(group_id) = message.group_id.as_deref() { + if self.group_prefs.lookup_sidecar(group_id).is_some() { + return Ok(thread_info_menu().into()); } } - Ok(info_menu(self.role).into()) + Ok(info_menu().into()) } } @@ -135,25 +135,21 @@ mod tests { } #[tokio::test] - async fn help_returns_role_specific_menu() { - let transcription = HelpHandler::new(BotRole::Transcription); - let translation = HelpHandler::new(BotRole::Translation); - - assert!(transcription.matches(&dm("!help"))); - assert!(!transcription.matches(&dm("!help-threads"))); - assert!(!transcription.matches(&dm("!help-in-chat"))); - let t = transcription.execute(&dm("!help")).await.unwrap(); - assert!(t.contains("!transcribe")); - assert!(t.contains("!help-transcription")); - assert!(!t.contains("!privacy")); - assert!(!t.contains("!translate-me-on")); + async fn help_returns_hub_menu() { + let handler = HelpHandler::new(); - let t = translation.execute(&dm("!help")).await.unwrap(); + assert!(handler.matches(&dm("!help"))); + assert!(!handler.matches(&dm("!help-threads"))); + assert!(!handler.matches(&dm("!help-in-chat"))); + let t = handler.execute(&dm("!help")).await.unwrap(); assert!(t.contains("!translation-threads")); assert!(t.contains("!translation-in-chat")); assert!(t.contains("!transcription")); assert!(t.contains("!privacy")); assert!(t.contains("!info")); + assert!(t.contains("!help-transcription")); + assert!(!t.contains("!transcribe-on")); + assert!(!t.contains("!translate-me-on")); assert!(!t.contains("Voice notes in this chat")); assert!(!t.contains("!set-en")); } @@ -161,7 +157,7 @@ mod tests { #[tokio::test] async fn info_returns_described_hub() { let store = GroupPreferencesStore::new_in_memory(0); - let handler = InfoHandler::new(store, BotRole::Translation); + let handler = InfoHandler::new(store); assert!(handler.matches(&dm("!info"))); let out = handler.execute(&dm("!info")).await.unwrap(); assert!(out.contains("!translation-threads\n ")); @@ -176,7 +172,7 @@ mod tests { async fn help_in_sidecar_returns_hub_menu() { let store = GroupPreferencesStore::new_in_memory(0); store.set_sidecar("main-1", "it", "group.it".into(), "it-internal".into()); - let handler = HelpHandler::new(BotRole::Translation); + let handler = HelpHandler::new(); let out = handler .execute(&group("!help", "it-internal")) .await @@ -220,7 +216,7 @@ mod tests { async fn info_in_sidecar_returns_thread_info() { let store = GroupPreferencesStore::new_in_memory(0); store.set_sidecar("main-1", "it", "group.it".into(), "it-internal".into()); - let handler = InfoHandler::new(store, BotRole::Translation); + let handler = InfoHandler::new(store); let out = handler .execute(&group("!info", "it-internal")) .await @@ -232,7 +228,7 @@ mod tests { #[tokio::test] async fn help_in_main_stays_hub() { - let handler = HelpHandler::new(BotRole::Translation); + let handler = HelpHandler::new(); let out = handler.execute(&group("!help", "main-1")).await.unwrap(); assert!(out.contains("!translation-threads")); assert!(!out.contains("!rename")); diff --git a/crates/signal-bot/src/commands/menu_locale.rs b/crates/signal-bot/src/commands/menu_locale.rs index 5466635..614637e 100644 --- a/crates/signal-bot/src/commands/menu_locale.rs +++ b/crates/signal-bot/src/commands/menu_locale.rs @@ -4,21 +4,18 @@ //! Command-list layout follows the Signal mobile menu standard: //! [`docs/solutions/signal-mobile-menus.md`](../../../../docs/solutions/signal-mobile-menus.md). -use crate::config::BotRole; +pub fn help_menu() -> &'static str { + HELP_HUB +} -pub fn help_menu(role: BotRole) -> &'static str { - match role { - BotRole::Transcription => HELP_TRANSCRIPTION, - BotRole::Translation => HELP_HUB, - } +/// Voice product menu (`!transcription`). +pub fn transcription_menu() -> &'static str { + HELP_TRANSCRIPTION } -/// Hub descriptive menu (translation bot only; transcription uses `!transcription`). -pub fn info_menu(role: BotRole) -> &'static str { - match role { - BotRole::Translation => INFO_HUB, - BotRole::Transcription => unreachable!("transcription bot does not register !info"), - } +/// Hub descriptive menu (`!info`). +pub fn info_menu() -> &'static str { + INFO_HUB } pub fn thread_help_menu() -> &'static str { @@ -302,7 +299,7 @@ mod tests { #[test] fn help_translation_is_hub() { - let h = help_menu(BotRole::Translation); + let h = help_menu(); assert!(h.contains("!translation-threads")); assert!(h.contains("!translation-in-chat")); assert!(h.contains("!transcription")); @@ -317,7 +314,7 @@ mod tests { #[test] fn info_hub_has_breaks_and_descriptions() { - let h = info_menu(BotRole::Translation); + let h = info_menu(); assert!(h.contains("!translation-threads\n ")); assert!(h.contains("!translation-in-chat\n ")); assert!(h.contains("!transcription\n ")); @@ -439,7 +436,7 @@ mod tests { #[test] fn help_transcription_covers_voice() { - let h = help_menu(BotRole::Transcription); + let h = transcription_menu(); assert!(h.contains("!transcribe")); assert!(h.contains("!transcribe-on")); assert!(h.contains("!transcribe-off")); @@ -472,7 +469,7 @@ mod tests { assert!(!translation_threads_menu().contains("!verify")); assert!(!translation_in_chat_menu(true).contains("!verify")); assert!(!translation_in_chat_menu(false).contains("!verify")); - assert!(!help_menu(BotRole::Translation).contains("!verify")); + assert!(!help_menu().contains("!verify")); assert!(!thread_help_menu().contains("!verify")); } diff --git a/crates/signal-bot/src/commands/product_menus.rs b/crates/signal-bot/src/commands/product_menus.rs index d960973..55296aa 100644 --- a/crates/signal-bot/src/commands/product_menus.rs +++ b/crates/signal-bot/src/commands/product_menus.rs @@ -1,12 +1,11 @@ //! Product menus: `!translation-threads`, `!translation-in-chat`, `!transcription`, redirects. use crate::commands::menu_locale::{ - help_in_chat_guide, help_menu, help_threads_guide, help_transcription_guide, is_exact_command, - is_translation_in_chat_menu_command, is_translation_threads_menu_command, + help_in_chat_guide, help_threads_guide, help_transcription_guide, is_exact_command, + is_translation_in_chat_menu_command, is_translation_threads_menu_command, transcription_menu, translation_in_chat_menu, translation_split_redirect, translation_threads_menu, }; use crate::commands::CommandHandler; -use crate::config::BotRole; use crate::error::AppResult; use async_trait::async_trait; use signal_client::BotMessage; @@ -117,7 +116,7 @@ impl CommandHandler for TranscriptionMenuHandler { } async fn execute(&self, _message: &BotMessage) -> AppResult { - Ok(help_menu(BotRole::Transcription).into()) + Ok(transcription_menu().into()) } } @@ -367,4 +366,18 @@ mod tests { assert!(transcription.contains("Whisper")); assert!(transcription.contains("!transcribe")); } + + #[tokio::test] + async fn transcription_menu_returns_voice_commands() { + let out = TranscriptionMenuHandler::new() + .execute(&msg("!transcription")) + .await + .unwrap(); + assert!(out.contains("!transcribe-on")); + assert!(out.contains("!transcribe-off")); + assert!(out.contains("!transcribe")); + assert!(out.contains("!help-transcription")); + assert!(!out.contains("!privacy")); + assert!(!out.contains("!translation-threads")); + } } diff --git a/crates/signal-bot/src/commands/verify.rs b/crates/signal-bot/src/commands/verify.rs index 78ac0d7..3317b41 100644 --- a/crates/signal-bot/src/commands/verify.rs +++ b/crates/signal-bot/src/commands/verify.rs @@ -1,7 +1,6 @@ //! Verify command - provides cryptographic attestation proofs. use crate::commands::CommandHandler; -use crate::config::BotRole; use crate::error::AppResult; use async_trait::async_trait; use dstack_client::DstackClient; @@ -31,17 +30,14 @@ impl OperatorAddresses { pub struct VerifyHandler { dstack: Arc, - /// Kept so callers still pass `BotRole`; challenge prefix is unified. - _role: BotRole, /// Optional operator addresses to display. operator_addresses: Option, } impl VerifyHandler { - pub fn new(dstack: Arc, role: BotRole) -> Self { + pub fn new(dstack: Arc) -> Self { Self { dstack, - _role: role, operator_addresses: None, } } @@ -49,12 +45,10 @@ impl VerifyHandler { /// Create handler with operator addresses to display. pub fn with_operator_addresses( dstack: Arc, - role: BotRole, addresses: OperatorAddresses, ) -> Self { Self { dstack, - _role: role, operator_addresses: Some(addresses), } } @@ -307,31 +301,26 @@ impl CommandHandler for VerifyHandler { mod tests { use super::*; - fn create_test_handler(role: BotRole) -> VerifyHandler { - VerifyHandler::new(Arc::new(DstackClient::new("/fake")), role) + fn create_test_handler() -> VerifyHandler { + VerifyHandler::new(Arc::new(DstackClient::new("/fake"))) } #[test] fn prefixed_challenge_uses_bot_label() { - let tr = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); assert_eq!( - tr.prefixed_challenge(Some("hello".into())), + handler.prefixed_challenge(Some("hello".into())), "Bread Bot: hello" ); assert_eq!( - tr.prefixed_challenge(None), + handler.prefixed_challenge(None), "Bread Bot: no-challenge-provided" ); - let tx = create_test_handler(BotRole::Transcription); - assert_eq!( - tx.prefixed_challenge(Some("hello".into())), - "Bread Bot: hello" - ); } #[test] fn test_parse_challenge_with_nonce() { - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); assert_eq!( handler.parse_challenge("!verify abc123"), @@ -345,7 +334,7 @@ mod tests { #[test] fn test_parse_challenge_without_nonce() { - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); assert_eq!(handler.parse_challenge("!verify"), None); assert_eq!(handler.parse_challenge("!verify "), None); @@ -353,7 +342,7 @@ mod tests { #[test] fn test_format_response_not_in_tee() { - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); let result = AttestationResult { in_tee: false, error: Some("Not running in TEE".into()), @@ -373,7 +362,7 @@ mod tests { let expected_hex = hex::encode(challenge.as_bytes()); - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); let result = AttestationResult { in_tee: true, compose_hash: Some("abc123".into()), @@ -403,7 +392,7 @@ mod tests { let expected_hash = hasher.finalize(); let expected_hex = hex::encode(expected_hash); - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); let result = AttestationResult { in_tee: true, compose_hash: Some("abc123".into()), @@ -424,7 +413,7 @@ mod tests { #[test] fn test_format_response_with_challenge() { - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); let challenge = "my-nonce"; let report_data_hex = hex::encode(challenge.as_bytes()); @@ -451,7 +440,7 @@ mod tests { #[test] fn test_format_response_without_challenge() { - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); let result = AttestationResult { in_tee: true, compose_hash: Some("abc123".into()), @@ -468,7 +457,7 @@ mod tests { #[test] fn test_verification_instructions_present() { - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); let result = AttestationResult { in_tee: true, compose_hash: Some("abc123".into()), @@ -495,7 +484,6 @@ mod tests { fn test_operator_addresses_displayed() { let handler = VerifyHandler::with_operator_addresses( Arc::new(DstackClient::new("/fake")), - BotRole::Translation, OperatorAddresses { base: Some("0xABC123".into()), near: Some("operator.near".into()), @@ -524,7 +512,7 @@ mod tests { #[tokio::test] async fn execute_reports_not_in_tee() { - let handler = create_test_handler(BotRole::Translation); + let handler = create_test_handler(); let msg = BotMessage { source: "+15550002222".into(), source_number: Some("+15550002222".into()), diff --git a/crates/signal-bot/src/group_invite_acceptor.rs b/crates/signal-bot/src/group_invite_acceptor.rs index 7a2dab3..1325bb5 100644 --- a/crates/signal-bot/src/group_invite_acceptor.rs +++ b/crates/signal-bot/src/group_invite_acceptor.rs @@ -2,7 +2,6 @@ //! //! Unified bot: accept any pending invite/request (`AcceptAll`). -use crate::config::BotRole; use signal_client::{Group, SignalClient}; use std::sync::Arc; use std::time::Duration; @@ -18,13 +17,6 @@ pub enum InvitePolicy { AcceptAll, } -impl InvitePolicy { - /// Unified bot always auto-accepts group invites. - pub fn for_role(_role: BotRole) -> Self { - Self::AcceptAll - } -} - /// Whether this account should `POST .../join` for `group`. pub fn should_join(group: &Group, self_identity: &str, _policy: &InvitePolicy) -> bool { group.is_pending_for(self_identity) @@ -141,18 +133,6 @@ mod tests { serde_json::from_value(group_json("group.abc==", members, pending, admins)).unwrap() } - #[test] - fn policy_for_role() { - assert_eq!( - InvitePolicy::for_role(BotRole::Translation), - InvitePolicy::AcceptAll - ); - assert_eq!( - InvitePolicy::for_role(BotRole::Transcription), - InvitePolicy::AcceptAll - ); - } - #[test] fn translation_accepts_any_pending() { let policy = InvitePolicy::AcceptAll; diff --git a/crates/signal-bot/src/handlers_setup.rs b/crates/signal-bot/src/handlers_setup.rs index a21ea2a..48bf0ce 100644 --- a/crates/signal-bot/src/handlers_setup.rs +++ b/crates/signal-bot/src/handlers_setup.rs @@ -177,15 +177,9 @@ pub async fn build_translation_handlers( signal.clone(), ))); handlers.push(Box::new(CommandsHandler::new(group_prefs.clone()))); - handlers.push(Box::new(VerifyHandler::new( - dstack.clone(), - BotRole::Translation, - ))); - handlers.push(Box::new(HelpHandler::new(BotRole::Translation))); - handlers.push(Box::new(InfoHandler::new( - group_prefs, - BotRole::Translation, - ))); + handlers.push(Box::new(VerifyHandler::new(dstack.clone()))); + handlers.push(Box::new(HelpHandler::new())); + handlers.push(Box::new(InfoHandler::new(group_prefs))); handlers.push(Box::new(PrivacyHandler::new())); info!("Unified bot: hub menus + voice + in-chat + Language Threads"); diff --git a/crates/signal-bot/src/main.rs b/crates/signal-bot/src/main.rs index c31f0eb..af34b7e 100644 --- a/crates/signal-bot/src/main.rs +++ b/crates/signal-bot/src/main.rs @@ -1,4 +1,4 @@ -//! Signal product bots — transcription or translation (see `BOT__ROLE`). +//! Signal product bot — `BOT__ROLE=translation` (transcription is retired). use anyhow::Context; use dstack_client::DstackClient; @@ -65,7 +65,7 @@ async fn main() -> AppResult<()> { info!("Registered {} command handlers", handlers.len()); { - let policy = InvitePolicy::for_role(config.bot.role); + let policy = InvitePolicy::AcceptAll; let signal_invites = signal.clone(); let phone = config.signal.phone_number.clone(); tokio::spawn(async move { diff --git a/docs/language-threads.md b/docs/language-threads.md index 2c3fd01..8c9ae04 100644 --- a/docs/language-threads.md +++ b/docs/language-threads.md @@ -39,15 +39,13 @@ N=1 (one sidecar) uses the same relay rules as N=3 — add another language late | `!commands` | Sidecar only | Compact Language Thread command list | | `!list-langs` | Any | Language codes | | `!help-threads` | Any | How Language Threads works (use case + flow) | -| `!help` / `!privacy` | Any | Hub menus (`!help` is always the Bread Bot hub; `!privacy` and `!verify` on translation bot) | +| `!help` / `!privacy` | Any | Hub menus (`!help` is always the Bread Bot hub; `!privacy` and `!verify` on this bot) | Menus: `!help` → `!translation-threads`. English-only for now (multi-language UI deferred). Aliases: `!translation-me-thread es`. -**Also on the translation bot:** [in-chat translation](in-chat-translation.md) (`!translate-all-on` / `!translate-me-on` / quote `!translate`) — same-group only, not a sidecar bridge. **Mutually exclusive with Language Threads** at setup time (refuse + `!enable-threads` / `!enable-in-chat` switch path). - -**Not registered on translation (worker CVM handles these):** `!ask`, DM chat, voice/`!transcribe*`, `!transcription` product menu on the transcription bot, `!models`. +**Also on this bot:** [in-chat translation](in-chat-translation.md) (`!translate-all-on` / `!translate-me-on` / quote `!translate`) — same-group only, not a sidecar bridge. **Mutually exclusive with Language Threads** at setup time (refuse + `!enable-threads` / `!enable-in-chat` switch path). Voice (`!transcription` / `!transcribe*`) is registered here too. Menus: `!help` → `!translation-threads` / `!translation-in-chat`. `!in-chat` opens the in-chat menu; `!translation` redirects to both. diff --git a/docs/solutions/README.md b/docs/solutions/README.md index f063dca..bf24e2e 100644 --- a/docs/solutions/README.md +++ b/docs/solutions/README.md @@ -6,7 +6,7 @@ Each solved problem becomes searchable markdown so the next `/ce-brainstorm` / ` ## Architecture -- [CPU TEE Whisper does not scale](architecture-patterns/2026-08-13-cpu-tee-whisper-does-not-scale.md) — remote NEAR Whisper Large V3; one CVM; two bot processes; never re-home Whisper in a CPU TEE. +- [CPU TEE Whisper does not scale](architecture-patterns/2026-08-13-cpu-tee-whisper-does-not-scale.md) — remote NEAR Whisper Large V3; one CVM; one bot process; never re-home Whisper in a CPU TEE. ## Product / UX diff --git a/docs/solutions/signal-mobile-menus.md b/docs/solutions/signal-mobile-menus.md index a9234fa..3f3a72c 100644 --- a/docs/solutions/signal-mobile-menus.md +++ b/docs/solutions/signal-mobile-menus.md @@ -14,7 +14,7 @@ Menus are **English-only** for now; multi-language UI is deferred. 2. **Section header** (optional) + optional one-line blurb. 3. **Each command** on its own line. No `cmd — desc` on one line. 4. Prefer plain `!command` lines over `- !command` bullets. -5. **Hub / nav lists** (e.g. main `!help` on the translation bot): commands only — skip indented descriptions when they would just restate the command name. +5. **Hub / nav lists** (e.g. main `!help` on Bread Bot): commands only — skip indented descriptions when they would just restate the command name. 6. **Product / how-to lists** (e.g. `!translation-in-chat`, transcription toggles): put a short description on the next line, indented with two spaces (aim ≤~40 chars). 7. **`!help` footer** is the bare command — no “Main menu” / “Show this menu” line (`!help` is implicit). 8. Prose blocks (privacy explanations, invite/status messages) stay paragraphs; only **command lists** use the forms above. @@ -54,7 +54,7 @@ Optional one-line blurb. Language Thread sidecars use `!commands` for the thread menu (rename / leave / info). Hub `!help` always returns the Bread Bot menu, including when sent from a sidecar. -Voice transcription uses `!transcription` on the **transcription** bot for its product menu (not `!help`). Hub `!help` / `!info` / `!privacy` stay on the **translation** bot only. See [two-cvm-architecture.md — Bot hierarchy](../two-cvm-architecture.md#bot-hierarchy). +Voice transcription uses `!transcription` for its product menu (not hub `!help`). Hub `!help` / `!info` / `!privacy` and `!transcription` all live on the same bot. See [two-cvm-architecture.md](../two-cvm-architecture.md). ## When adding menus