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:
parent
3239a4a270
commit
e73e1e6371
@ -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()),
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user