feat(discord): add listen_to_bots config and fix model IDs across codebase (#280)

* fix(config): apply env overrides at runtime and fix Docker compose defaults

- Call apply_env_overrides() after Config::load_or_init() in main.rs so
  environment variables (API_KEY, PROVIDER, ZEROCLAW_GATEWAY_PORT, etc.)
  are actually applied at runtime, not just in tests
- Add ZEROCLAW_ALLOW_PUBLIC_BIND env var support for gateway bind policy
- Fix docker-compose.yml: correct volume path (/zeroclaw-data not /data),
  add ZEROCLAW_ALLOW_PUBLIC_BIND=true for container networking, make host
  port configurable via HOST_PORT env var
- Add docker-compose.override.yml to .gitignore for local dev overrides

* feat(discord): add listen_to_bots config and fix model IDs across codebase

Add listen_to_bots field to DiscordConfig so bot messages are processed
when explicitly enabled (defaults to false for backward compat). Remove
ZEROCLAW_MODEL from Dockerfile release stage so config.toml is the
source of truth for model selection. Fix all hardcoded model IDs from
the dated anthropic/claude-sonnet-4-20250514 to the valid OpenRouter
identifier anthropic/claude-sonnet-4.
This commit is contained in:
Vernon Stinebaker 2026-02-16 15:13:36 +08:00 committed by GitHub
parent d5e8fc1652
commit 40c41cf3d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 34 additions and 25 deletions

View file

@ -10,16 +10,18 @@ pub struct DiscordChannel {
bot_token: String,
guild_id: Option<String>,
allowed_users: Vec<String>,
listen_to_bots: bool,
client: reqwest::Client,
typing_handle: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
}
impl DiscordChannel {
pub fn new(bot_token: String, guild_id: Option<String>, allowed_users: Vec<String>) -> Self {
pub fn new(bot_token: String, guild_id: Option<String>, allowed_users: Vec<String>, listen_to_bots: bool) -> Self {
Self {
bot_token,
guild_id,
allowed_users,
listen_to_bots,
client: reqwest::Client::new(),
typing_handle: std::sync::Mutex::new(None),
}
@ -309,8 +311,8 @@ impl Channel for DiscordChannel {
continue;
}
// Skip bot messages
if d.get("author").and_then(|a| a.get("bot")).and_then(serde_json::Value::as_bool).unwrap_or(false) {
// Skip bot messages (unless listen_to_bots is enabled)
if !self.listen_to_bots && d.get("author").and_then(|a| a.get("bot")).and_then(serde_json::Value::as_bool).unwrap_or(false) {
continue;
}
@ -411,7 +413,7 @@ mod tests {
#[test]
fn discord_channel_name() {
let ch = DiscordChannel::new("fake".into(), None, vec![]);
let ch = DiscordChannel::new("fake".into(), None, vec![], false);
assert_eq!(ch.name(), "discord");
}
@ -432,21 +434,21 @@ mod tests {
#[test]
fn empty_allowlist_denies_everyone() {
let ch = DiscordChannel::new("fake".into(), None, vec![]);
let ch = DiscordChannel::new("fake".into(), None, vec![], false);
assert!(!ch.is_user_allowed("12345"));
assert!(!ch.is_user_allowed("anyone"));
}
#[test]
fn wildcard_allows_everyone() {
let ch = DiscordChannel::new("fake".into(), None, vec!["*".into()]);
let ch = DiscordChannel::new("fake".into(), None, vec!["*".into()], false);
assert!(ch.is_user_allowed("12345"));
assert!(ch.is_user_allowed("anyone"));
}
#[test]
fn specific_allowlist_filters() {
let ch = DiscordChannel::new("fake".into(), None, vec!["111".into(), "222".into()]);
let ch = DiscordChannel::new("fake".into(), None, vec!["111".into(), "222".into()], false);
assert!(ch.is_user_allowed("111"));
assert!(ch.is_user_allowed("222"));
assert!(!ch.is_user_allowed("333"));
@ -455,7 +457,7 @@ mod tests {
#[test]
fn allowlist_is_exact_match_not_substring() {
let ch = DiscordChannel::new("fake".into(), None, vec!["111".into()]);
let ch = DiscordChannel::new("fake".into(), None, vec!["111".into()], false);
assert!(!ch.is_user_allowed("1111"));
assert!(!ch.is_user_allowed("11"));
assert!(!ch.is_user_allowed("0111"));
@ -463,20 +465,20 @@ mod tests {
#[test]
fn allowlist_empty_string_user_id() {
let ch = DiscordChannel::new("fake".into(), None, vec!["111".into()]);
let ch = DiscordChannel::new("fake".into(), None, vec!["111".into()], false);
assert!(!ch.is_user_allowed(""));
}
#[test]
fn allowlist_with_wildcard_and_specific() {
let ch = DiscordChannel::new("fake".into(), None, vec!["111".into(), "*".into()]);
let ch = DiscordChannel::new("fake".into(), None, vec!["111".into(), "*".into()], false);
assert!(ch.is_user_allowed("111"));
assert!(ch.is_user_allowed("anyone_else"));
}
#[test]
fn allowlist_case_sensitive() {
let ch = DiscordChannel::new("fake".into(), None, vec!["ABC".into()]);
let ch = DiscordChannel::new("fake".into(), None, vec!["ABC".into()], false);
assert!(ch.is_user_allowed("ABC"));
assert!(!ch.is_user_allowed("abc"));
assert!(!ch.is_user_allowed("Abc"));
@ -651,14 +653,14 @@ mod tests {
#[test]
fn typing_handle_starts_as_none() {
let ch = DiscordChannel::new("fake".into(), None, vec![]);
let ch = DiscordChannel::new("fake".into(), None, vec![], false);
let guard = ch.typing_handle.lock().unwrap();
assert!(guard.is_none());
}
#[tokio::test]
async fn start_typing_sets_handle() {
let ch = DiscordChannel::new("fake".into(), None, vec![]);
let ch = DiscordChannel::new("fake".into(), None, vec![], false);
let _ = ch.start_typing("123456").await;
let guard = ch.typing_handle.lock().unwrap();
assert!(guard.is_some());
@ -666,7 +668,7 @@ mod tests {
#[tokio::test]
async fn stop_typing_clears_handle() {
let ch = DiscordChannel::new("fake".into(), None, vec![]);
let ch = DiscordChannel::new("fake".into(), None, vec![], false);
let _ = ch.start_typing("123456").await;
let _ = ch.stop_typing("123456").await;
let guard = ch.typing_handle.lock().unwrap();
@ -675,14 +677,14 @@ mod tests {
#[tokio::test]
async fn stop_typing_is_idempotent() {
let ch = DiscordChannel::new("fake".into(), None, vec![]);
let ch = DiscordChannel::new("fake".into(), None, vec![], false);
assert!(ch.stop_typing("123456").await.is_ok());
assert!(ch.stop_typing("123456").await.is_ok());
}
#[tokio::test]
async fn start_typing_replaces_existing_task() {
let ch = DiscordChannel::new("fake".into(), None, vec![]);
let ch = DiscordChannel::new("fake".into(), None, vec![], false);
let _ = ch.start_typing("111").await;
let _ = ch.start_typing("222").await;
let guard = ch.typing_handle.lock().unwrap();