feat(channel): add mention_only option for Telegram groups
Adds mention_only config option to Telegram channel, allowing the bot to only respond to messages that @-mention the bot in group chats. Direct messages are always processed regardless of this setting. Behavior: - When mention_only = true: Bot only responds to group messages containing @botname - When mention_only = false (default): Bot responds to all allowed messages - DM/private chats always work regardless of mention_only setting Implementation: - Fetch and cache bot username from Telegram API on startup - Check for @botname mention in group messages - Strip mention from message content before processing Config example: [channels.telegram] bot_token = "your_token" mention_only = true Changes: - src/config/schema.rs: Add mention_only to TelegramConfig - src/channels/telegram.rs: Implement mention_only logic + 6 new tests - src/channels/mod.rs: Update factory calls - src/cron/scheduler.rs: Update constructor call - src/onboard/wizard.rs: Update wizard config - src/daemon/mod.rs: Update test config - src/integrations/registry.rs: Update test config - TESTING_TELEGRAM.md: Add mention_only test section - CHANGELOG.md: Document feature Risk: medium Backward compatible: Yes (default: false)
This commit is contained in:
parent
3b75c6cc42
commit
c0a80ad656
10 changed files with 264 additions and 54 deletions
|
|
@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
value if the input used the legacy `enc:` format
|
value if the input used the legacy `enc:` format
|
||||||
- `SecretStore::needs_migration()` — Check if a value uses the legacy `enc:` format
|
- `SecretStore::needs_migration()` — Check if a value uses the legacy `enc:` format
|
||||||
- `SecretStore::is_secure_encrypted()` — Check if a value uses the secure `enc2:` format
|
- `SecretStore::is_secure_encrypted()` — Check if a value uses the secure `enc2:` format
|
||||||
|
- **Telegram mention_only mode** — New config option `mention_only` for Telegram channel.
|
||||||
|
When enabled, bot only responds to messages that @-mention the bot in group chats.
|
||||||
|
Direct messages always work regardless of this setting. Default: `false`.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
- `enc:` prefix for encrypted secrets — Use `enc2:` (ChaCha20-Poly1305) instead.
|
- `enc:` prefix for encrypted secrets — Use `enc2:` (ChaCha20-Poly1305) instead.
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,22 @@ After running automated tests, perform these manual checks:
|
||||||
- Verify: No "Too Many Requests" errors
|
- Verify: No "Too Many Requests" errors
|
||||||
- Verify: Responses have delays
|
- Verify: Responses have delays
|
||||||
|
|
||||||
5. **Error logging**
|
5. **Mention-only mode (group chats)**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Edit ~/.zeroclaw/config.toml
|
||||||
|
[channels.telegram]
|
||||||
|
mention_only = true
|
||||||
|
```
|
||||||
|
|
||||||
|
- Add bot to a group chat
|
||||||
|
- Send message without @botname mention
|
||||||
|
- Verify: Bot does not respond
|
||||||
|
- Send message with @botname mention
|
||||||
|
- Verify: Bot responds and mention is stripped
|
||||||
|
- DM/private chat should always work regardless of mention_only
|
||||||
|
|
||||||
|
6. **Error logging**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
RUST_LOG=debug zeroclaw channel start
|
RUST_LOG=debug zeroclaw channel start
|
||||||
|
|
@ -225,7 +240,7 @@ Expected values after all fixes:
|
||||||
| Message split overhead | <50ms | Check logs for timing |
|
| Message split overhead | <50ms | Check logs for timing |
|
||||||
| Memory usage | <10MB | `ps aux \| grep zeroclaw` |
|
| Memory usage | <10MB | `ps aux \| grep zeroclaw` |
|
||||||
| Binary size | ~3-4MB | `ls -lh target/release/zeroclaw` |
|
| Binary size | ~3-4MB | `ls -lh target/release/zeroclaw` |
|
||||||
| Unit test coverage | 24/24 pass | `cargo test telegram --lib` |
|
| Unit test coverage | 61/61 pass | `cargo test telegram --lib` |
|
||||||
|
|
||||||
## 🐛 Debugging Failed Tests
|
## 🐛 Debugging Failed Tests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -942,7 +942,11 @@ pub async fn doctor_channels(config: Config) -> Result<()> {
|
||||||
channels.push((
|
channels.push((
|
||||||
"Telegram",
|
"Telegram",
|
||||||
Arc::new(
|
Arc::new(
|
||||||
TelegramChannel::new(tg.bot_token.clone(), tg.allowed_users.clone())
|
TelegramChannel::new(
|
||||||
|
tg.bot_token.clone(),
|
||||||
|
tg.allowed_users.clone(),
|
||||||
|
tg.mention_only,
|
||||||
|
)
|
||||||
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms),
|
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
@ -1262,7 +1266,11 @@ pub async fn start_channels(config: Config) -> Result<()> {
|
||||||
|
|
||||||
if let Some(ref tg) = config.channels_config.telegram {
|
if let Some(ref tg) = config.channels_config.telegram {
|
||||||
channels.push(Arc::new(
|
channels.push(Arc::new(
|
||||||
TelegramChannel::new(tg.bot_token.clone(), tg.allowed_users.clone())
|
TelegramChannel::new(
|
||||||
|
tg.bot_token.clone(),
|
||||||
|
tg.allowed_users.clone(),
|
||||||
|
tg.mention_only,
|
||||||
|
)
|
||||||
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms),
|
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -305,10 +305,12 @@ pub struct TelegramChannel {
|
||||||
stream_mode: StreamMode,
|
stream_mode: StreamMode,
|
||||||
draft_update_interval_ms: u64,
|
draft_update_interval_ms: u64,
|
||||||
last_draft_edit: Mutex<std::collections::HashMap<String, std::time::Instant>>,
|
last_draft_edit: Mutex<std::collections::HashMap<String, std::time::Instant>>,
|
||||||
|
mention_only: bool,
|
||||||
|
bot_username: Mutex<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TelegramChannel {
|
impl TelegramChannel {
|
||||||
pub fn new(bot_token: String, allowed_users: Vec<String>) -> Self {
|
pub fn new(bot_token: String, allowed_users: Vec<String>, mention_only: bool) -> Self {
|
||||||
let normalized_allowed = Self::normalize_allowed_users(allowed_users);
|
let normalized_allowed = Self::normalize_allowed_users(allowed_users);
|
||||||
let pairing = if normalized_allowed.is_empty() {
|
let pairing = if normalized_allowed.is_empty() {
|
||||||
let guard = PairingGuard::new(true, &[]);
|
let guard = PairingGuard::new(true, &[]);
|
||||||
|
|
@ -330,6 +332,8 @@ impl TelegramChannel {
|
||||||
draft_update_interval_ms: 1000,
|
draft_update_interval_ms: 1000,
|
||||||
last_draft_edit: Mutex::new(std::collections::HashMap::new()),
|
last_draft_edit: Mutex::new(std::collections::HashMap::new()),
|
||||||
typing_handle: Mutex::new(None),
|
typing_handle: Mutex::new(None),
|
||||||
|
mention_only,
|
||||||
|
bot_username: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -443,6 +447,70 @@ impl TelegramChannel {
|
||||||
format!("https://api.telegram.org/bot{}/{method}", self.bot_token)
|
format!("https://api.telegram.org/bot{}/{method}", self.bot_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn fetch_bot_username(&self) -> anyhow::Result<String> {
|
||||||
|
let resp = self.client.get(self.api_url("getMe")).send().await?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
anyhow::bail!("Failed to fetch bot info: {}", resp.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: serde_json::Value = resp.json().await?;
|
||||||
|
let username = data
|
||||||
|
.get("result")
|
||||||
|
.and_then(|r| r.get("username"))
|
||||||
|
.and_then(|u| u.as_str())
|
||||||
|
.context("Bot username not found in response")?;
|
||||||
|
|
||||||
|
Ok(username.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_bot_username(&self) -> Option<String> {
|
||||||
|
{
|
||||||
|
let cache = self.bot_username.lock();
|
||||||
|
if let Some(ref username) = *cache {
|
||||||
|
return Some(username.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.fetch_bot_username().await {
|
||||||
|
Ok(username) => {
|
||||||
|
let mut cache = self.bot_username.lock();
|
||||||
|
*cache = Some(username.clone());
|
||||||
|
Some(username)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch bot username: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_bot_mention(text: &str, bot_username: &str) -> bool {
|
||||||
|
let mention = format!("@{}", bot_username);
|
||||||
|
text.contains(&mention)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_incoming_content(text: &str, bot_username: &str) -> Option<String> {
|
||||||
|
let mention = format!("@{}", bot_username);
|
||||||
|
let normalized = text.replace(&mention, " ");
|
||||||
|
let normalized = normalized.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||||
|
|
||||||
|
if normalized.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_group_message(message: &serde_json::Value) -> bool {
|
||||||
|
message
|
||||||
|
.get("chat")
|
||||||
|
.and_then(|c| c.get("type"))
|
||||||
|
.and_then(|t| t.as_str())
|
||||||
|
.map(|t| t == "group" || t == "supergroup")
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn is_user_allowed(&self, username: &str) -> bool {
|
fn is_user_allowed(&self, username: &str) -> bool {
|
||||||
let identity = Self::normalize_identity(username);
|
let identity = Self::normalize_identity(username);
|
||||||
self.allowed_users
|
self.allowed_users
|
||||||
|
|
@ -645,6 +713,18 @@ Allowlist Telegram username (without '@') or numeric user ID.",
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let is_group = Self::is_group_message(message);
|
||||||
|
if self.mention_only && is_group {
|
||||||
|
let bot_username = self.bot_username.lock();
|
||||||
|
if let Some(ref bot_username) = *bot_username {
|
||||||
|
if !Self::contains_bot_mention(text, bot_username) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let chat_id = message
|
let chat_id = message
|
||||||
.get("chat")
|
.get("chat")
|
||||||
.and_then(|chat| chat.get("id"))
|
.and_then(|chat| chat.get("id"))
|
||||||
|
|
@ -669,11 +749,23 @@ Allowlist Telegram username (without '@') or numeric user ID.",
|
||||||
chat_id.clone()
|
chat_id.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let content = if self.mention_only && is_group {
|
||||||
|
let bot_username = self.bot_username.lock();
|
||||||
|
if let Some(ref bot_username) = *bot_username {
|
||||||
|
Self::normalize_incoming_content(text, bot_username)
|
||||||
|
.unwrap_or_else(|| text.to_string())
|
||||||
|
} else {
|
||||||
|
text.to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
Some(ChannelMessage {
|
Some(ChannelMessage {
|
||||||
id: format!("telegram_{chat_id}_{message_id}"),
|
id: format!("telegram_{chat_id}_{message_id}"),
|
||||||
sender: sender_identity,
|
sender: sender_identity,
|
||||||
reply_target,
|
reply_target,
|
||||||
content: text.to_string(),
|
content,
|
||||||
channel: "telegram".to_string(),
|
channel: "telegram".to_string(),
|
||||||
timestamp: std::time::SystemTime::now()
|
timestamp: std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
|
@ -1522,6 +1614,10 @@ impl Channel for TelegramChannel {
|
||||||
async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
|
async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
|
||||||
let mut offset: i64 = 0;
|
let mut offset: i64 = 0;
|
||||||
|
|
||||||
|
if self.mention_only {
|
||||||
|
let _ = self.get_bot_username().await;
|
||||||
|
}
|
||||||
|
|
||||||
tracing::info!("Telegram channel listening for messages...");
|
tracing::info!("Telegram channel listening for messages...");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -1672,20 +1768,20 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_channel_name() {
|
fn telegram_channel_name() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
assert_eq!(ch.name(), "telegram");
|
assert_eq!(ch.name(), "telegram");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn typing_handle_starts_as_none() {
|
fn typing_handle_starts_as_none() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let guard = ch.typing_handle.lock();
|
let guard = ch.typing_handle.lock();
|
||||||
assert!(guard.is_none());
|
assert!(guard.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stop_typing_clears_handle() {
|
async fn stop_typing_clears_handle() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
|
|
||||||
// Manually insert a dummy handle
|
// Manually insert a dummy handle
|
||||||
{
|
{
|
||||||
|
|
@ -1704,7 +1800,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn start_typing_replaces_previous_handle() {
|
async fn start_typing_replaces_previous_handle() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
|
|
||||||
// Insert a dummy handle first
|
// Insert a dummy handle first
|
||||||
{
|
{
|
||||||
|
|
@ -1723,10 +1819,10 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn supports_draft_updates_respects_stream_mode() {
|
fn supports_draft_updates_respects_stream_mode() {
|
||||||
let off = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let off = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
assert!(!off.supports_draft_updates());
|
assert!(!off.supports_draft_updates());
|
||||||
|
|
||||||
let partial = TelegramChannel::new("fake-token".into(), vec!["*".into()])
|
let partial = TelegramChannel::new("fake-token".into(), vec!["*".into()], false)
|
||||||
.with_streaming(StreamMode::Partial, 750);
|
.with_streaming(StreamMode::Partial, 750);
|
||||||
assert!(partial.supports_draft_updates());
|
assert!(partial.supports_draft_updates());
|
||||||
assert_eq!(partial.draft_update_interval_ms, 750);
|
assert_eq!(partial.draft_update_interval_ms, 750);
|
||||||
|
|
@ -1734,7 +1830,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn send_draft_returns_none_when_stream_mode_off() {
|
async fn send_draft_returns_none_when_stream_mode_off() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let id = ch
|
let id = ch
|
||||||
.send_draft(&SendMessage::new("draft", "123"))
|
.send_draft(&SendMessage::new("draft", "123"))
|
||||||
.await
|
.await
|
||||||
|
|
@ -1744,7 +1840,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn update_draft_rate_limit_short_circuits_network() {
|
async fn update_draft_rate_limit_short_circuits_network() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()])
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false)
|
||||||
.with_streaming(StreamMode::Partial, 60_000);
|
.with_streaming(StreamMode::Partial, 60_000);
|
||||||
ch.last_draft_edit
|
ch.last_draft_edit
|
||||||
.lock()
|
.lock()
|
||||||
|
|
@ -1756,7 +1852,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn update_draft_utf8_truncation_is_safe_for_multibyte_text() {
|
async fn update_draft_utf8_truncation_is_safe_for_multibyte_text() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()])
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false)
|
||||||
.with_streaming(StreamMode::Partial, 0);
|
.with_streaming(StreamMode::Partial, 0);
|
||||||
let long_emoji_text = "😀".repeat(TELEGRAM_MAX_MESSAGE_LENGTH + 20);
|
let long_emoji_text = "😀".repeat(TELEGRAM_MAX_MESSAGE_LENGTH + 20);
|
||||||
|
|
||||||
|
|
@ -1770,7 +1866,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn finalize_draft_invalid_message_id_falls_back_to_chunk_send() {
|
async fn finalize_draft_invalid_message_id_falls_back_to_chunk_send() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()])
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false)
|
||||||
.with_streaming(StreamMode::Partial, 0);
|
.with_streaming(StreamMode::Partial, 0);
|
||||||
let long_text = "a".repeat(TELEGRAM_MAX_MESSAGE_LENGTH + 64);
|
let long_text = "a".repeat(TELEGRAM_MAX_MESSAGE_LENGTH + 64);
|
||||||
|
|
||||||
|
|
@ -1782,7 +1878,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_api_url() {
|
fn telegram_api_url() {
|
||||||
let ch = TelegramChannel::new("123:ABC".into(), vec![]);
|
let ch = TelegramChannel::new("123:ABC".into(), vec![], false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ch.api_url("getMe"),
|
ch.api_url("getMe"),
|
||||||
"https://api.telegram.org/bot123:ABC/getMe"
|
"https://api.telegram.org/bot123:ABC/getMe"
|
||||||
|
|
@ -1791,32 +1887,32 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_allowed_wildcard() {
|
fn telegram_user_allowed_wildcard() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["*".into()], false);
|
||||||
assert!(ch.is_user_allowed("anyone"));
|
assert!(ch.is_user_allowed("anyone"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_allowed_specific() {
|
fn telegram_user_allowed_specific() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["alice".into(), "bob".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["alice".into(), "bob".into()], false);
|
||||||
assert!(ch.is_user_allowed("alice"));
|
assert!(ch.is_user_allowed("alice"));
|
||||||
assert!(!ch.is_user_allowed("eve"));
|
assert!(!ch.is_user_allowed("eve"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_allowed_with_at_prefix_in_config() {
|
fn telegram_user_allowed_with_at_prefix_in_config() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["@alice".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["@alice".into()], false);
|
||||||
assert!(ch.is_user_allowed("alice"));
|
assert!(ch.is_user_allowed("alice"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_denied_empty() {
|
fn telegram_user_denied_empty() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec![]);
|
let ch = TelegramChannel::new("t".into(), vec![], false);
|
||||||
assert!(!ch.is_user_allowed("anyone"));
|
assert!(!ch.is_user_allowed("anyone"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_exact_match_not_substring() {
|
fn telegram_user_exact_match_not_substring() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["alice".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["alice".into()], false);
|
||||||
assert!(!ch.is_user_allowed("alice_bot"));
|
assert!(!ch.is_user_allowed("alice_bot"));
|
||||||
assert!(!ch.is_user_allowed("alic"));
|
assert!(!ch.is_user_allowed("alic"));
|
||||||
assert!(!ch.is_user_allowed("malice"));
|
assert!(!ch.is_user_allowed("malice"));
|
||||||
|
|
@ -1824,13 +1920,13 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_empty_string_denied() {
|
fn telegram_user_empty_string_denied() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["alice".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["alice".into()], false);
|
||||||
assert!(!ch.is_user_allowed(""));
|
assert!(!ch.is_user_allowed(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_case_sensitive() {
|
fn telegram_user_case_sensitive() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["Alice".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["Alice".into()], false);
|
||||||
assert!(ch.is_user_allowed("Alice"));
|
assert!(ch.is_user_allowed("Alice"));
|
||||||
assert!(!ch.is_user_allowed("alice"));
|
assert!(!ch.is_user_allowed("alice"));
|
||||||
assert!(!ch.is_user_allowed("ALICE"));
|
assert!(!ch.is_user_allowed("ALICE"));
|
||||||
|
|
@ -1838,7 +1934,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_wildcard_with_specific_users() {
|
fn telegram_wildcard_with_specific_users() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["alice".into(), "*".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["alice".into(), "*".into()], false);
|
||||||
assert!(ch.is_user_allowed("alice"));
|
assert!(ch.is_user_allowed("alice"));
|
||||||
assert!(ch.is_user_allowed("bob"));
|
assert!(ch.is_user_allowed("bob"));
|
||||||
assert!(ch.is_user_allowed("anyone"));
|
assert!(ch.is_user_allowed("anyone"));
|
||||||
|
|
@ -1846,25 +1942,25 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_allowed_by_numeric_id_identity() {
|
fn telegram_user_allowed_by_numeric_id_identity() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["123456789".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["123456789".into()], false);
|
||||||
assert!(ch.is_any_user_allowed(["unknown", "123456789"]));
|
assert!(ch.is_any_user_allowed(["unknown", "123456789"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_user_denied_when_none_of_identities_match() {
|
fn telegram_user_denied_when_none_of_identities_match() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["alice".into(), "987654321".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["alice".into(), "987654321".into()], false);
|
||||||
assert!(!ch.is_any_user_allowed(["unknown", "123456789"]));
|
assert!(!ch.is_any_user_allowed(["unknown", "123456789"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_pairing_enabled_with_empty_allowlist() {
|
fn telegram_pairing_enabled_with_empty_allowlist() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec![]);
|
let ch = TelegramChannel::new("t".into(), vec![], false);
|
||||||
assert!(ch.pairing_code_active());
|
assert!(ch.pairing_code_active());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_pairing_disabled_with_nonempty_allowlist() {
|
fn telegram_pairing_disabled_with_nonempty_allowlist() {
|
||||||
let ch = TelegramChannel::new("t".into(), vec!["alice".into()]);
|
let ch = TelegramChannel::new("t".into(), vec!["alice".into()], false);
|
||||||
assert!(!ch.pairing_code_active());
|
assert!(!ch.pairing_code_active());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1940,7 +2036,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_update_message_uses_chat_id_as_reply_target() {
|
fn parse_update_message_uses_chat_id_as_reply_target() {
|
||||||
let ch = TelegramChannel::new("token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("token".into(), vec!["*".into()], false);
|
||||||
let update = serde_json::json!({
|
let update = serde_json::json!({
|
||||||
"update_id": 1,
|
"update_id": 1,
|
||||||
"message": {
|
"message": {
|
||||||
|
|
@ -1968,7 +2064,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_update_message_allows_numeric_id_without_username() {
|
fn parse_update_message_allows_numeric_id_without_username() {
|
||||||
let ch = TelegramChannel::new("token".into(), vec!["555".into()]);
|
let ch = TelegramChannel::new("token".into(), vec!["555".into()], false);
|
||||||
let update = serde_json::json!({
|
let update = serde_json::json!({
|
||||||
"update_id": 2,
|
"update_id": 2,
|
||||||
"message": {
|
"message": {
|
||||||
|
|
@ -1993,7 +2089,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_update_message_extracts_thread_id_for_forum_topic() {
|
fn parse_update_message_extracts_thread_id_for_forum_topic() {
|
||||||
let ch = TelegramChannel::new("token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("token".into(), vec!["*".into()], false);
|
||||||
let update = serde_json::json!({
|
let update = serde_json::json!({
|
||||||
"update_id": 3,
|
"update_id": 3,
|
||||||
"message": {
|
"message": {
|
||||||
|
|
@ -2024,7 +2120,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_api_url_send_document() {
|
fn telegram_api_url_send_document() {
|
||||||
let ch = TelegramChannel::new("123:ABC".into(), vec![]);
|
let ch = TelegramChannel::new("123:ABC".into(), vec![], false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ch.api_url("sendDocument"),
|
ch.api_url("sendDocument"),
|
||||||
"https://api.telegram.org/bot123:ABC/sendDocument"
|
"https://api.telegram.org/bot123:ABC/sendDocument"
|
||||||
|
|
@ -2033,7 +2129,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_api_url_send_photo() {
|
fn telegram_api_url_send_photo() {
|
||||||
let ch = TelegramChannel::new("123:ABC".into(), vec![]);
|
let ch = TelegramChannel::new("123:ABC".into(), vec![], false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ch.api_url("sendPhoto"),
|
ch.api_url("sendPhoto"),
|
||||||
"https://api.telegram.org/bot123:ABC/sendPhoto"
|
"https://api.telegram.org/bot123:ABC/sendPhoto"
|
||||||
|
|
@ -2042,7 +2138,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_api_url_send_video() {
|
fn telegram_api_url_send_video() {
|
||||||
let ch = TelegramChannel::new("123:ABC".into(), vec![]);
|
let ch = TelegramChannel::new("123:ABC".into(), vec![], false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ch.api_url("sendVideo"),
|
ch.api_url("sendVideo"),
|
||||||
"https://api.telegram.org/bot123:ABC/sendVideo"
|
"https://api.telegram.org/bot123:ABC/sendVideo"
|
||||||
|
|
@ -2051,7 +2147,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_api_url_send_audio() {
|
fn telegram_api_url_send_audio() {
|
||||||
let ch = TelegramChannel::new("123:ABC".into(), vec![]);
|
let ch = TelegramChannel::new("123:ABC".into(), vec![], false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ch.api_url("sendAudio"),
|
ch.api_url("sendAudio"),
|
||||||
"https://api.telegram.org/bot123:ABC/sendAudio"
|
"https://api.telegram.org/bot123:ABC/sendAudio"
|
||||||
|
|
@ -2060,7 +2156,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_api_url_send_voice() {
|
fn telegram_api_url_send_voice() {
|
||||||
let ch = TelegramChannel::new("123:ABC".into(), vec![]);
|
let ch = TelegramChannel::new("123:ABC".into(), vec![], false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ch.api_url("sendVoice"),
|
ch.api_url("sendVoice"),
|
||||||
"https://api.telegram.org/bot123:ABC/sendVoice"
|
"https://api.telegram.org/bot123:ABC/sendVoice"
|
||||||
|
|
@ -2072,7 +2168,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_document_bytes_builds_correct_form() {
|
async fn telegram_send_document_bytes_builds_correct_form() {
|
||||||
// This test verifies the method doesn't panic and handles bytes correctly
|
// This test verifies the method doesn't panic and handles bytes correctly
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let file_bytes = b"Hello, this is a test file content".to_vec();
|
let file_bytes = b"Hello, this is a test file content".to_vec();
|
||||||
|
|
||||||
// The actual API call will fail (no real server), but we verify the method exists
|
// The actual API call will fail (no real server), but we verify the method exists
|
||||||
|
|
@ -2093,7 +2189,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_photo_bytes_builds_correct_form() {
|
async fn telegram_send_photo_bytes_builds_correct_form() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
// Minimal valid PNG header bytes
|
// Minimal valid PNG header bytes
|
||||||
let file_bytes = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
let file_bytes = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
||||||
|
|
||||||
|
|
@ -2106,7 +2202,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_document_by_url_builds_correct_json() {
|
async fn telegram_send_document_by_url_builds_correct_json() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
|
|
||||||
let result = ch
|
let result = ch
|
||||||
.send_document_by_url(
|
.send_document_by_url(
|
||||||
|
|
@ -2122,7 +2218,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_photo_by_url_builds_correct_json() {
|
async fn telegram_send_photo_by_url_builds_correct_json() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
|
|
||||||
let result = ch
|
let result = ch
|
||||||
.send_photo_by_url("123456", None, "https://example.com/image.jpg", None)
|
.send_photo_by_url("123456", None, "https://example.com/image.jpg", None)
|
||||||
|
|
@ -2135,7 +2231,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_document_nonexistent_file() {
|
async fn telegram_send_document_nonexistent_file() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let path = Path::new("/nonexistent/path/to/file.txt");
|
let path = Path::new("/nonexistent/path/to/file.txt");
|
||||||
|
|
||||||
let result = ch.send_document("123456", None, path, None).await;
|
let result = ch.send_document("123456", None, path, None).await;
|
||||||
|
|
@ -2151,7 +2247,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_photo_nonexistent_file() {
|
async fn telegram_send_photo_nonexistent_file() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let path = Path::new("/nonexistent/path/to/photo.jpg");
|
let path = Path::new("/nonexistent/path/to/photo.jpg");
|
||||||
|
|
||||||
let result = ch.send_photo("123456", None, path, None).await;
|
let result = ch.send_photo("123456", None, path, None).await;
|
||||||
|
|
@ -2161,7 +2257,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_video_nonexistent_file() {
|
async fn telegram_send_video_nonexistent_file() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let path = Path::new("/nonexistent/path/to/video.mp4");
|
let path = Path::new("/nonexistent/path/to/video.mp4");
|
||||||
|
|
||||||
let result = ch.send_video("123456", None, path, None).await;
|
let result = ch.send_video("123456", None, path, None).await;
|
||||||
|
|
@ -2171,7 +2267,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_audio_nonexistent_file() {
|
async fn telegram_send_audio_nonexistent_file() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let path = Path::new("/nonexistent/path/to/audio.mp3");
|
let path = Path::new("/nonexistent/path/to/audio.mp3");
|
||||||
|
|
||||||
let result = ch.send_audio("123456", None, path, None).await;
|
let result = ch.send_audio("123456", None, path, None).await;
|
||||||
|
|
@ -2181,7 +2277,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_voice_nonexistent_file() {
|
async fn telegram_send_voice_nonexistent_file() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let path = Path::new("/nonexistent/path/to/voice.ogg");
|
let path = Path::new("/nonexistent/path/to/voice.ogg");
|
||||||
|
|
||||||
let result = ch.send_voice("123456", None, path, None).await;
|
let result = ch.send_voice("123456", None, path, None).await;
|
||||||
|
|
@ -2269,7 +2365,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_document_bytes_with_caption() {
|
async fn telegram_send_document_bytes_with_caption() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let file_bytes = b"test content".to_vec();
|
let file_bytes = b"test content".to_vec();
|
||||||
|
|
||||||
// With caption
|
// With caption
|
||||||
|
|
@ -2293,7 +2389,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_photo_bytes_with_caption() {
|
async fn telegram_send_photo_bytes_with_caption() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let file_bytes = vec![0x89, 0x50, 0x4E, 0x47];
|
let file_bytes = vec![0x89, 0x50, 0x4E, 0x47];
|
||||||
|
|
||||||
// With caption
|
// With caption
|
||||||
|
|
@ -2319,7 +2415,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_document_bytes_empty_file() {
|
async fn telegram_send_document_bytes_empty_file() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let file_bytes: Vec<u8> = vec![];
|
let file_bytes: Vec<u8> = vec![];
|
||||||
|
|
||||||
let result = ch
|
let result = ch
|
||||||
|
|
@ -2332,7 +2428,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_document_bytes_empty_filename() {
|
async fn telegram_send_document_bytes_empty_filename() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let file_bytes = b"content".to_vec();
|
let file_bytes = b"content".to_vec();
|
||||||
|
|
||||||
let result = ch
|
let result = ch
|
||||||
|
|
@ -2345,7 +2441,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn telegram_send_document_bytes_empty_chat_id() {
|
async fn telegram_send_document_bytes_empty_chat_id() {
|
||||||
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
|
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()], false);
|
||||||
let file_bytes = b"content".to_vec();
|
let file_bytes = b"content".to_vec();
|
||||||
|
|
||||||
let result = ch
|
let result = ch
|
||||||
|
|
@ -2516,4 +2612,78 @@ mod tests {
|
||||||
let result = strip_tool_call_tags(input);
|
let result = strip_tool_call_tags(input);
|
||||||
assert_eq!(result, "");
|
assert_eq!(result, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telegram_contains_bot_mention_finds_mention() {
|
||||||
|
assert!(TelegramChannel::contains_bot_mention(
|
||||||
|
"Hello @mybot",
|
||||||
|
"mybot"
|
||||||
|
));
|
||||||
|
assert!(TelegramChannel::contains_bot_mention(
|
||||||
|
"@mybot help",
|
||||||
|
"mybot"
|
||||||
|
));
|
||||||
|
assert!(TelegramChannel::contains_bot_mention(
|
||||||
|
"Hey @mybot how are you?",
|
||||||
|
"mybot"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telegram_contains_bot_mention_no_false_positives() {
|
||||||
|
assert!(!TelegramChannel::contains_bot_mention(
|
||||||
|
"Hello @otherbot",
|
||||||
|
"mybot"
|
||||||
|
));
|
||||||
|
assert!(!TelegramChannel::contains_bot_mention(
|
||||||
|
"Hello mybot",
|
||||||
|
"mybot"
|
||||||
|
));
|
||||||
|
assert!(!TelegramChannel::contains_bot_mention("", "mybot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telegram_normalize_incoming_content_strips_mention() {
|
||||||
|
let result = TelegramChannel::normalize_incoming_content("@mybot hello", "mybot");
|
||||||
|
assert_eq!(result, Some("hello".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telegram_normalize_incoming_content_handles_multiple_mentions() {
|
||||||
|
let result = TelegramChannel::normalize_incoming_content("@mybot @mybot test", "mybot");
|
||||||
|
assert_eq!(result, Some("test".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telegram_normalize_incoming_content_returns_none_for_empty() {
|
||||||
|
let result = TelegramChannel::normalize_incoming_content("@mybot", "mybot");
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telegram_is_group_message_detects_groups() {
|
||||||
|
let group_msg = serde_json::json!({
|
||||||
|
"chat": { "type": "group" }
|
||||||
|
});
|
||||||
|
assert!(TelegramChannel::is_group_message(&group_msg));
|
||||||
|
|
||||||
|
let supergroup_msg = serde_json::json!({
|
||||||
|
"chat": { "type": "supergroup" }
|
||||||
|
});
|
||||||
|
assert!(TelegramChannel::is_group_message(&supergroup_msg));
|
||||||
|
|
||||||
|
let private_msg = serde_json::json!({
|
||||||
|
"chat": { "type": "private" }
|
||||||
|
});
|
||||||
|
assert!(!TelegramChannel::is_group_message(&private_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telegram_mention_only_enabled_by_config() {
|
||||||
|
let ch = TelegramChannel::new("token".into(), vec!["*".into()], true);
|
||||||
|
assert!(ch.mention_only);
|
||||||
|
|
||||||
|
let ch_disabled = TelegramChannel::new("token".into(), vec!["*".into()], false);
|
||||||
|
assert!(!ch_disabled.mention_only);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ mod tests {
|
||||||
allowed_users: vec!["alice".into()],
|
allowed_users: vec!["alice".into()],
|
||||||
stream_mode: StreamMode::default(),
|
stream_mode: StreamMode::default(),
|
||||||
draft_update_interval_ms: 1000,
|
draft_update_interval_ms: 1000,
|
||||||
|
mention_only: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let discord = DiscordConfig {
|
let discord = DiscordConfig {
|
||||||
|
|
|
||||||
|
|
@ -1465,6 +1465,10 @@ pub struct TelegramConfig {
|
||||||
/// Minimum interval (ms) between draft message edits to avoid rate limits.
|
/// Minimum interval (ms) between draft message edits to avoid rate limits.
|
||||||
#[serde(default = "default_draft_update_interval_ms")]
|
#[serde(default = "default_draft_update_interval_ms")]
|
||||||
pub draft_update_interval_ms: u64,
|
pub draft_update_interval_ms: u64,
|
||||||
|
/// When true, only respond to messages that @-mention the bot in groups.
|
||||||
|
/// Direct messages are always processed.
|
||||||
|
#[serde(default)]
|
||||||
|
pub mention_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -2535,6 +2539,7 @@ default_temperature = 0.7
|
||||||
allowed_users: vec!["user1".into()],
|
allowed_users: vec!["user1".into()],
|
||||||
stream_mode: StreamMode::default(),
|
stream_mode: StreamMode::default(),
|
||||||
draft_update_interval_ms: default_draft_update_interval_ms(),
|
draft_update_interval_ms: default_draft_update_interval_ms(),
|
||||||
|
mention_only: false,
|
||||||
}),
|
}),
|
||||||
discord: None,
|
discord: None,
|
||||||
slack: None,
|
slack: None,
|
||||||
|
|
@ -2808,6 +2813,7 @@ tool_dispatcher = "xml"
|
||||||
allowed_users: vec!["alice".into(), "bob".into()],
|
allowed_users: vec!["alice".into(), "bob".into()],
|
||||||
stream_mode: StreamMode::Partial,
|
stream_mode: StreamMode::Partial,
|
||||||
draft_update_interval_ms: 500,
|
draft_update_interval_ms: 500,
|
||||||
|
mention_only: false,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&tc).unwrap();
|
let json = serde_json::to_string(&tc).unwrap();
|
||||||
let parsed: TelegramConfig = serde_json::from_str(&json).unwrap();
|
let parsed: TelegramConfig = serde_json::from_str(&json).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,11 @@ async fn deliver_if_configured(config: &Config, job: &CronJob, output: &str) ->
|
||||||
.telegram
|
.telegram
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| anyhow::anyhow!("telegram channel not configured"))?;
|
.ok_or_else(|| anyhow::anyhow!("telegram channel not configured"))?;
|
||||||
let channel = TelegramChannel::new(tg.bot_token.clone(), tg.allowed_users.clone());
|
let channel = TelegramChannel::new(
|
||||||
|
tg.bot_token.clone(),
|
||||||
|
tg.allowed_users.clone(),
|
||||||
|
tg.mention_only,
|
||||||
|
);
|
||||||
channel.send(&SendMessage::new(output, target)).await?;
|
channel.send(&SendMessage::new(output, target)).await?;
|
||||||
}
|
}
|
||||||
"discord" => {
|
"discord" => {
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,7 @@ mod tests {
|
||||||
allowed_users: vec![],
|
allowed_users: vec![],
|
||||||
stream_mode: crate::config::StreamMode::default(),
|
stream_mode: crate::config::StreamMode::default(),
|
||||||
draft_update_interval_ms: 1000,
|
draft_update_interval_ms: 1000,
|
||||||
|
mention_only: false,
|
||||||
});
|
});
|
||||||
assert!(has_supervised_channels(&config));
|
assert!(has_supervised_channels(&config));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -790,6 +790,7 @@ mod tests {
|
||||||
allowed_users: vec!["user".into()],
|
allowed_users: vec!["user".into()],
|
||||||
stream_mode: StreamMode::default(),
|
stream_mode: StreamMode::default(),
|
||||||
draft_update_interval_ms: 1000,
|
draft_update_interval_ms: 1000,
|
||||||
|
mention_only: false,
|
||||||
});
|
});
|
||||||
let entries = all_integrations();
|
let entries = all_integrations();
|
||||||
let tg = entries.iter().find(|e| e.name == "Telegram").unwrap();
|
let tg = entries.iter().find(|e| e.name == "Telegram").unwrap();
|
||||||
|
|
|
||||||
|
|
@ -2667,6 +2667,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
|
||||||
allowed_users,
|
allowed_users,
|
||||||
stream_mode: StreamMode::default(),
|
stream_mode: StreamMode::default(),
|
||||||
draft_update_interval_ms: 1000,
|
draft_update_interval_ms: 1000,
|
||||||
|
mention_only: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue