fix(config): add missing WhatsApp Web policy config keys (#4131)

* fix(config): add missing WhatsApp Web policy config keys (mode, dm_policy, group_policy, self_chat_mode)

* fix(onboard): add missing WhatsApp policy fields to wizard struct literals

The new mode, dm_policy, group_policy, and self_chat_mode fields added
to WhatsAppConfig need default values in the onboard wizard's struct
initializers to avoid E0063 compilation errors.
This commit is contained in:
Argenis 2026-03-21 06:04:21 -04:00 committed by Roman Tataurov
parent 3239a4a270
commit e73e1e6371
No known key found for this signature in database
GPG Key ID: 70A51EF3185C334B
5 changed files with 196 additions and 4 deletions

View File

@ -3818,6 +3818,10 @@ fn collect_configured_channels(
wa.pair_phone.clone(),
wa.pair_code.clone(),
wa.allowed_numbers.clone(),
wa.mode.clone(),
wa.dm_policy.clone(),
wa.group_policy.clone(),
wa.self_chat_mode,
)
.with_transcription(config.transcription.clone())
.with_tts(config.tts.clone()),

View File

@ -58,6 +58,14 @@ pub struct WhatsAppWebChannel {
pair_code: Option<String>,
/// Allowed phone numbers (E.164 format) or "*" for all
allowed_numbers: Vec<String>,
/// Usage mode (business vs personal policy filtering)
mode: crate::config::WhatsAppWebMode,
/// DM policy when mode = personal
dm_policy: crate::config::WhatsAppChatPolicy,
/// Group policy when mode = personal
group_policy: crate::config::WhatsAppChatPolicy,
/// Whether to always respond in self-chat when mode = personal
self_chat_mode: bool,
/// Bot handle for shutdown
bot_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
/// Client handle for sending messages and typing indicators
@ -86,18 +94,30 @@ impl WhatsAppWebChannel {
/// * `pair_phone` - Optional phone number for pair code linking (format: "15551234567")
/// * `pair_code` - Optional custom pair code (leave empty for auto-generated)
/// * `allowed_numbers` - Phone numbers allowed to interact (E.164 format) or "*" for all
/// * `mode` - Usage mode (business or personal)
/// * `dm_policy` - DM policy when mode = personal
/// * `group_policy` - Group policy when mode = personal
/// * `self_chat_mode` - Whether to always respond in self-chat when mode = personal
#[cfg(feature = "whatsapp-web")]
pub fn new(
session_path: String,
pair_phone: Option<String>,
pair_code: Option<String>,
allowed_numbers: Vec<String>,
mode: crate::config::WhatsAppWebMode,
dm_policy: crate::config::WhatsAppChatPolicy,
group_policy: crate::config::WhatsAppChatPolicy,
self_chat_mode: bool,
) -> Self {
Self {
session_path,
pair_phone,
pair_code,
allowed_numbers,
mode,
dm_policy,
group_policy,
self_chat_mode,
bot_handle: Arc::new(Mutex::new(None)),
client: Arc::new(Mutex::new(None)),
tx: Arc::new(Mutex::new(None)),
@ -625,6 +645,10 @@ impl Channel for WhatsAppWebChannel {
let session_revoked_clone = session_revoked.clone();
let transcription_config = self.transcription.clone();
let voice_chats = self.voice_chats.clone();
let wa_mode = self.mode.clone();
let wa_dm_policy = self.dm_policy.clone();
let wa_group_policy = self.group_policy.clone();
let wa_self_chat_mode = self.self_chat_mode;
let mut builder = Bot::builder()
.with_backend(backend)
@ -638,6 +662,9 @@ impl Channel for WhatsAppWebChannel {
let session_revoked = session_revoked_clone.clone();
let transcription_config = transcription_config.clone();
let voice_chats = voice_chats.clone();
let wa_mode = wa_mode.clone();
let wa_dm_policy = wa_dm_policy.clone();
let wa_group_policy = wa_group_policy.clone();
async move {
match event {
Event::Message(msg, info) => {
@ -674,6 +701,61 @@ impl Channel for WhatsAppWebChannel {
}
};
// ── Personal-mode chat-type policy filtering ──
if wa_mode == crate::config::WhatsAppWebMode::Personal {
let is_group = chat.contains("@g.us");
// Self-chat: the chat JID user part matches
// the sender's user part (message to "Notes
// to Self").
let sender_user = sender_jid.user();
let chat_user = chat
.split_once('@')
.map(|(u, _)| u)
.unwrap_or(&chat);
let is_self_chat = !is_group && sender_user == chat_user;
if is_self_chat {
if !wa_self_chat_mode {
tracing::debug!(
"WhatsApp Web: ignoring self-chat message (self_chat_mode=false)"
);
return;
}
// self_chat_mode=true: always process, skip further policy checks
} else if is_group {
match wa_group_policy {
crate::config::WhatsAppChatPolicy::Ignore => {
tracing::debug!(
"WhatsApp Web: ignoring group message (group_policy=ignore)"
);
return;
}
crate::config::WhatsAppChatPolicy::All => {
// allow unconditionally
}
crate::config::WhatsAppChatPolicy::Allowlist => {
// already filtered by allowed_numbers above
}
}
} else {
// DM (non-self)
match wa_dm_policy {
crate::config::WhatsAppChatPolicy::Ignore => {
tracing::debug!(
"WhatsApp Web: ignoring DM (dm_policy=ignore)"
);
return;
}
crate::config::WhatsAppChatPolicy::All => {
// allow unconditionally
}
crate::config::WhatsAppChatPolicy::Allowlist => {
// already filtered by allowed_numbers above
}
}
}
}
// Attempt voice note transcription (ptt = push-to-talk = voice note)
let voice_text = if let Some(ref audio) = msg.audio_message {
if audio.ptt == Some(true) {
@ -977,6 +1059,10 @@ impl WhatsAppWebChannel {
_pair_phone: Option<String>,
_pair_code: Option<String>,
_allowed_numbers: Vec<String>,
_mode: crate::config::WhatsAppWebMode,
_dm_policy: crate::config::WhatsAppChatPolicy,
_group_policy: crate::config::WhatsAppChatPolicy,
_self_chat_mode: bool,
) -> Self {
Self { _private: () }
}
@ -1043,6 +1129,10 @@ mod tests {
None,
None,
vec!["+1234567890".into()],
crate::config::WhatsAppWebMode::default(),
crate::config::WhatsAppChatPolicy::default(),
crate::config::WhatsAppChatPolicy::default(),
false,
)
}
@ -1064,7 +1154,16 @@ mod tests {
#[test]
#[cfg(feature = "whatsapp-web")]
fn whatsapp_web_number_allowed_wildcard() {
let ch = WhatsAppWebChannel::new("/tmp/test.db".into(), None, None, vec!["*".into()]);
let ch = WhatsAppWebChannel::new(
"/tmp/test.db".into(),
None,
None,
vec!["*".into()],
crate::config::WhatsAppWebMode::default(),
crate::config::WhatsAppChatPolicy::default(),
crate::config::WhatsAppChatPolicy::default(),
false,
);
assert!(ch.is_number_allowed("+1234567890"));
assert!(ch.is_number_allowed("+9999999999"));
}
@ -1072,7 +1171,16 @@ mod tests {
#[test]
#[cfg(feature = "whatsapp-web")]
fn whatsapp_web_number_denied_empty() {
let ch = WhatsAppWebChannel::new("/tmp/test.db".into(), None, None, vec![]);
let ch = WhatsAppWebChannel::new(
"/tmp/test.db".into(),
None,
None,
vec![],
crate::config::WhatsAppWebMode::default(),
crate::config::WhatsAppChatPolicy::default(),
crate::config::WhatsAppChatPolicy::default(),
false,
);
// Empty allowlist means "deny all" (matches channel-wide allowlist policy).
assert!(!ch.is_number_allowed("+1234567890"));
}

View File

@ -27,7 +27,8 @@ pub use schema::{
StorageConfig, StorageProviderConfig, StorageProviderSection, StreamMode, SwarmConfig,
SwarmStrategy, TelegramConfig, TextBrowserConfig, ToolFilterGroup, ToolFilterGroupMode,
TranscriptionConfig, TtsConfig, TunnelConfig, VerifiableIntentConfig, WebFetchConfig,
WebSearchConfig, WebhookConfig, WorkspaceConfig, DEFAULT_GWS_SERVICES,
WebSearchConfig, WebhookConfig, WhatsAppChatPolicy, WhatsAppWebMode, WorkspaceConfig,
DEFAULT_GWS_SERVICES,
};
pub fn name_and_presence<T: traits::ChannelConfig>(channel: Option<&T>) -> (&'static str, bool) {

View File

@ -5112,6 +5112,36 @@ impl ChannelConfig for SignalConfig {
}
}
/// WhatsApp Web usage mode.
///
/// `Personal` treats the account as a personal phone — the bot only responds to
/// incoming messages that pass the DM/group/self-chat policy filters.
/// `Business` (default) responds to all incoming messages, subject only to the
/// `allowed_numbers` allowlist.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum WhatsAppWebMode {
/// Respond to all messages passing the allowlist (default).
#[default]
Business,
/// Apply per-chat-type policies (dm_policy, group_policy, self_chat_mode).
Personal,
}
/// Policy for a particular WhatsApp chat type (DMs or groups) when
/// `mode = "personal"`.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum WhatsAppChatPolicy {
/// Only respond to senders on the `allowed_numbers` list (default).
#[default]
Allowlist,
/// Ignore all messages in this chat type.
Ignore,
/// Respond to every message regardless of allowlist.
All,
}
/// WhatsApp channel configuration (Cloud API or Web mode).
///
/// Set `phone_number_id` for Cloud API mode, or `session_path` for Web mode.
@ -5148,6 +5178,23 @@ pub struct WhatsAppConfig {
/// Allowed phone numbers (E.164 format: +1234567890) or "*" for all
#[serde(default)]
pub allowed_numbers: Vec<String>,
/// Usage mode for WhatsApp Web: "business" (default) or "personal".
/// In personal mode the bot applies dm_policy, group_policy, and
/// self_chat_mode to decide which chats to respond in.
#[serde(default)]
pub mode: WhatsAppWebMode,
/// Policy for direct messages when mode = "personal".
/// "allowlist" (default) | "ignore" | "all".
#[serde(default)]
pub dm_policy: WhatsAppChatPolicy,
/// Policy for group chats when mode = "personal".
/// "allowlist" (default) | "ignore" | "all".
#[serde(default)]
pub group_policy: WhatsAppChatPolicy,
/// When true and mode = "personal", always respond to messages in the
/// user's own self-chat (Notes to Self). Defaults to false.
#[serde(default)]
pub self_chat_mode: bool,
}
impl ChannelConfig for WhatsAppConfig {
@ -10273,6 +10320,10 @@ channel_id = "C123"
pair_phone: None,
pair_code: None,
allowed_numbers: vec!["+1234567890".into(), "+9876543210".into()],
mode: WhatsAppWebMode::default(),
dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false,
};
let json = serde_json::to_string(&wc).unwrap();
let parsed: WhatsAppConfig = serde_json::from_str(&json).unwrap();
@ -10293,6 +10344,10 @@ channel_id = "C123"
pair_phone: None,
pair_code: None,
allowed_numbers: vec!["+1".into()],
mode: WhatsAppWebMode::default(),
dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false,
};
let toml_str = toml::to_string(&wc).unwrap();
let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap();
@ -10318,6 +10373,10 @@ channel_id = "C123"
pair_phone: None,
pair_code: None,
allowed_numbers: vec!["*".into()],
mode: WhatsAppWebMode::default(),
dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false,
};
let toml_str = toml::to_string(&wc).unwrap();
let parsed: WhatsAppConfig = toml::from_str(&toml_str).unwrap();
@ -10335,6 +10394,10 @@ channel_id = "C123"
pair_phone: None,
pair_code: None,
allowed_numbers: vec!["+1".into()],
mode: WhatsAppWebMode::default(),
dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false,
};
assert!(wc.is_ambiguous_config());
assert_eq!(wc.backend_type(), "cloud");
@ -10351,6 +10414,10 @@ channel_id = "C123"
pair_phone: None,
pair_code: None,
allowed_numbers: vec![],
mode: WhatsAppWebMode::default(),
dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false,
};
assert!(!wc.is_ambiguous_config());
assert_eq!(wc.backend_type(), "web");
@ -10377,6 +10444,10 @@ channel_id = "C123"
pair_phone: None,
pair_code: None,
allowed_numbers: vec!["+1".into()],
mode: WhatsAppWebMode::default(),
dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false,
}),
linq: None,
wati: None,

View File

@ -3,7 +3,7 @@ use crate::cli_input::Input;
use crate::config::schema::{default_nostr_relays, NostrConfig};
use crate::config::schema::{
DingTalkConfig, IrcConfig, LarkReceiveMode, LinqConfig, NextcloudTalkConfig, QQConfig,
SignalConfig, StreamMode, WhatsAppConfig,
SignalConfig, StreamMode, WhatsAppChatPolicy, WhatsAppConfig, WhatsAppWebMode,
};
use crate::config::{
AutonomyConfig, BrowserConfig, ChannelsConfig, ComposioConfig, Config, DiscordConfig,
@ -4379,6 +4379,10 @@ fn setup_channels() -> Result<ChannelsConfig> {
pair_code: (!pair_code.trim().is_empty())
.then(|| pair_code.trim().to_string()),
allowed_numbers,
mode: WhatsAppWebMode::default(),
dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false,
});
println!(
@ -4480,6 +4484,10 @@ fn setup_channels() -> Result<ChannelsConfig> {
pair_phone: None,
pair_code: None,
allowed_numbers,
mode: WhatsAppWebMode::default(),
dm_policy: WhatsAppChatPolicy::default(),
group_policy: WhatsAppChatPolicy::default(),
self_chat_mode: false,
});
}
ChannelMenuChoice::Linq => {