The Telegram channel was ignoring the ack_reactions setting because it sent setMessageReaction API calls directly in its polling loop, bypassing the top-level channels_config.ack_reactions check. - Add optional ack_reactions field to TelegramConfig so it can be set under [channels_config.telegram] without "unknown key" warnings - Add ack_reactions field and with_ack_reactions() builder to TelegramChannel, defaulting to true - Guard try_add_ack_reaction_nonblocking() behind self.ack_reactions - Wire channel-level override with fallback to top-level default - Add config deserialization and channel behavior tests
This commit is contained in:
parent
733acca191
commit
c6f94fda4f
@ -3229,12 +3229,16 @@ fn build_channel_by_id(config: &Config, channel_id: &str) -> Result<Arc<dyn Chan
|
||||
.telegram
|
||||
.as_ref()
|
||||
.context("Telegram channel is not configured")?;
|
||||
let ack = tg
|
||||
.ack_reactions
|
||||
.unwrap_or(config.channels_config.ack_reactions);
|
||||
Ok(Arc::new(
|
||||
TelegramChannel::new(
|
||||
tg.bot_token.clone(),
|
||||
tg.allowed_users.clone(),
|
||||
tg.mention_only,
|
||||
)
|
||||
.with_ack_reactions(ack)
|
||||
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms)
|
||||
.with_transcription(config.transcription.clone())
|
||||
.with_workspace_dir(config.workspace_dir.clone()),
|
||||
@ -3322,6 +3326,9 @@ fn collect_configured_channels(
|
||||
let mut channels = Vec::new();
|
||||
|
||||
if let Some(ref tg) = config.channels_config.telegram {
|
||||
let ack = tg
|
||||
.ack_reactions
|
||||
.unwrap_or(config.channels_config.ack_reactions);
|
||||
channels.push(ConfiguredChannel {
|
||||
display_name: "Telegram",
|
||||
channel: Arc::new(
|
||||
@ -3330,6 +3337,7 @@ fn collect_configured_channels(
|
||||
tg.allowed_users.clone(),
|
||||
tg.mention_only,
|
||||
)
|
||||
.with_ack_reactions(ack)
|
||||
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms)
|
||||
.with_transcription(config.transcription.clone())
|
||||
.with_workspace_dir(config.workspace_dir.clone()),
|
||||
@ -8668,6 +8676,7 @@ This is an example JSON object for profile settings."#;
|
||||
draft_update_interval_ms: 1000,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
});
|
||||
match build_channel_by_id(&config, "telegram") {
|
||||
Ok(channel) => assert_eq!(channel.name(), "telegram"),
|
||||
|
||||
@ -332,6 +332,7 @@ pub struct TelegramChannel {
|
||||
transcription: Option<crate::config::TranscriptionConfig>,
|
||||
voice_transcriptions: Mutex<std::collections::HashMap<String, String>>,
|
||||
workspace_dir: Option<std::path::PathBuf>,
|
||||
ack_reactions: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -370,9 +371,16 @@ impl TelegramChannel {
|
||||
transcription: None,
|
||||
voice_transcriptions: Mutex::new(std::collections::HashMap::new()),
|
||||
workspace_dir: None,
|
||||
ack_reactions: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure whether Telegram-native acknowledgement reactions are sent.
|
||||
pub fn with_ack_reactions(mut self, enabled: bool) -> Self {
|
||||
self.ack_reactions = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure workspace directory for saving downloaded attachments.
|
||||
pub fn with_workspace_dir(mut self, dir: std::path::PathBuf) -> Self {
|
||||
self.workspace_dir = Some(dir);
|
||||
@ -2689,13 +2697,15 @@ Ensure only one `zeroclaw` process is using this bot token."
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some((reaction_chat_id, reaction_message_id)) =
|
||||
Self::extract_update_message_target(update)
|
||||
{
|
||||
self.try_add_ack_reaction_nonblocking(
|
||||
reaction_chat_id,
|
||||
reaction_message_id,
|
||||
);
|
||||
if self.ack_reactions {
|
||||
if let Some((reaction_chat_id, reaction_message_id)) =
|
||||
Self::extract_update_message_target(update)
|
||||
{
|
||||
self.try_add_ack_reaction_nonblocking(
|
||||
reaction_chat_id,
|
||||
reaction_message_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Send "typing" indicator immediately when we receive a message
|
||||
@ -4681,4 +4691,24 @@ mod tests {
|
||||
// the agent loop will return ProviderCapabilityError before calling
|
||||
// the provider, and the channel will send "⚠️ Error: ..." to the user.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ack_reactions_defaults_to_true() {
|
||||
let ch = TelegramChannel::new("token".into(), vec!["*".into()], false);
|
||||
assert!(ch.ack_reactions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_ack_reactions_false_disables_reactions() {
|
||||
let ch =
|
||||
TelegramChannel::new("token".into(), vec!["*".into()], false).with_ack_reactions(false);
|
||||
assert!(!ch.ack_reactions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_ack_reactions_true_keeps_reactions() {
|
||||
let ch =
|
||||
TelegramChannel::new("token".into(), vec!["*".into()], false).with_ack_reactions(true);
|
||||
assert!(ch.ack_reactions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ mod tests {
|
||||
draft_update_interval_ms: 1000,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
};
|
||||
|
||||
let discord = DiscordConfig {
|
||||
|
||||
@ -4511,6 +4511,11 @@ pub struct TelegramConfig {
|
||||
/// Direct messages are always processed.
|
||||
#[serde(default)]
|
||||
pub mention_only: bool,
|
||||
/// Override for the top-level `ack_reactions` setting. When `None`, the
|
||||
/// channel falls back to `[channels_config].ack_reactions`. When set
|
||||
/// explicitly, it takes precedence.
|
||||
#[serde(default)]
|
||||
pub ack_reactions: Option<bool>,
|
||||
}
|
||||
|
||||
impl ChannelConfig for TelegramConfig {
|
||||
@ -8360,6 +8365,7 @@ default_temperature = 0.7
|
||||
draft_update_interval_ms: default_draft_update_interval_ms(),
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
}),
|
||||
discord: None,
|
||||
slack: None,
|
||||
@ -8942,6 +8948,7 @@ tool_dispatcher = "xml"
|
||||
draft_update_interval_ms: 500,
|
||||
interrupt_on_new_message: true,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
};
|
||||
let json = serde_json::to_string(&tc).unwrap();
|
||||
let parsed: TelegramConfig = serde_json::from_str(&json).unwrap();
|
||||
@ -11256,6 +11263,7 @@ require_otp_to_resume = true
|
||||
draft_update_interval_ms: default_draft_update_interval_ms(),
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
});
|
||||
|
||||
// Save (triggers encryption)
|
||||
@ -11811,4 +11819,67 @@ require_otp_to_resume = true
|
||||
"Debug output must show [REDACTED] for client_secret"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn telegram_config_ack_reactions_false_deserializes() {
|
||||
let toml_str = r#"
|
||||
bot_token = "123:ABC"
|
||||
allowed_users = ["alice"]
|
||||
ack_reactions = false
|
||||
"#;
|
||||
let cfg: TelegramConfig = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(cfg.ack_reactions, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn telegram_config_ack_reactions_true_deserializes() {
|
||||
let toml_str = r#"
|
||||
bot_token = "123:ABC"
|
||||
allowed_users = ["alice"]
|
||||
ack_reactions = true
|
||||
"#;
|
||||
let cfg: TelegramConfig = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(cfg.ack_reactions, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn telegram_config_ack_reactions_missing_defaults_to_none() {
|
||||
let toml_str = r#"
|
||||
bot_token = "123:ABC"
|
||||
allowed_users = ["alice"]
|
||||
"#;
|
||||
let cfg: TelegramConfig = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(cfg.ack_reactions, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn telegram_config_ack_reactions_channel_overrides_top_level() {
|
||||
let tg_toml = r#"
|
||||
bot_token = "123:ABC"
|
||||
allowed_users = ["alice"]
|
||||
ack_reactions = false
|
||||
"#;
|
||||
let tg: TelegramConfig = toml::from_str(tg_toml).unwrap();
|
||||
let top_level_ack = true;
|
||||
let effective = tg.ack_reactions.unwrap_or(top_level_ack);
|
||||
assert!(
|
||||
!effective,
|
||||
"channel-level false must override top-level true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn telegram_config_ack_reactions_falls_back_to_top_level() {
|
||||
let tg_toml = r#"
|
||||
bot_token = "123:ABC"
|
||||
allowed_users = ["alice"]
|
||||
"#;
|
||||
let tg: TelegramConfig = toml::from_str(tg_toml).unwrap();
|
||||
let top_level_ack = false;
|
||||
let effective = tg.ack_reactions.unwrap_or(top_level_ack);
|
||||
assert!(
|
||||
!effective,
|
||||
"must fall back to top-level false when channel omits field"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -642,6 +642,7 @@ mod tests {
|
||||
draft_update_interval_ms: 1000,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
});
|
||||
assert!(has_supervised_channels(&config));
|
||||
}
|
||||
@ -755,6 +756,7 @@ mod tests {
|
||||
draft_update_interval_ms: 1000,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
});
|
||||
|
||||
let target = resolve_heartbeat_delivery(&config).unwrap();
|
||||
@ -771,6 +773,7 @@ mod tests {
|
||||
draft_update_interval_ms: 1000,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
});
|
||||
|
||||
let target = resolve_heartbeat_delivery(&config).unwrap();
|
||||
|
||||
@ -840,6 +840,7 @@ mod tests {
|
||||
draft_update_interval_ms: 1000,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
});
|
||||
let entries = all_integrations();
|
||||
let tg = entries.iter().find(|e| e.name == "Telegram").unwrap();
|
||||
|
||||
@ -3683,6 +3683,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||
draft_update_interval_ms: 1000,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
ack_reactions: None,
|
||||
});
|
||||
}
|
||||
ChannelMenuChoice::Discord => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user