fix(telegram): harden mention-only matching and retry cache
This commit is contained in:
parent
c0a80ad656
commit
cfa7215688
1 changed files with 156 additions and 16 deletions
|
|
@ -485,21 +485,75 @@ impl TelegramChannel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_telegram_username_char(ch: char) -> bool {
|
||||||
|
ch.is_ascii_alphanumeric() || ch == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_bot_mention_spans(text: &str, bot_username: &str) -> Vec<(usize, usize)> {
|
||||||
|
let bot_username = bot_username.trim_start_matches('@');
|
||||||
|
if bot_username.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
|
||||||
|
for (at_idx, ch) in text.char_indices() {
|
||||||
|
if ch != '@' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if at_idx > 0 {
|
||||||
|
let prev = text[..at_idx].chars().next_back().unwrap_or(' ');
|
||||||
|
if Self::is_telegram_username_char(prev) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let username_start = at_idx + 1;
|
||||||
|
let mut username_end = username_start;
|
||||||
|
|
||||||
|
for (rel_idx, candidate_ch) in text[username_start..].char_indices() {
|
||||||
|
if Self::is_telegram_username_char(candidate_ch) {
|
||||||
|
username_end = username_start + rel_idx + candidate_ch.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if username_end == username_start {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mention_username = &text[username_start..username_end];
|
||||||
|
if mention_username.eq_ignore_ascii_case(bot_username) {
|
||||||
|
spans.push((at_idx, username_end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spans
|
||||||
|
}
|
||||||
|
|
||||||
fn contains_bot_mention(text: &str, bot_username: &str) -> bool {
|
fn contains_bot_mention(text: &str, bot_username: &str) -> bool {
|
||||||
let mention = format!("@{}", bot_username);
|
!Self::find_bot_mention_spans(text, bot_username).is_empty()
|
||||||
text.contains(&mention)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_incoming_content(text: &str, bot_username: &str) -> Option<String> {
|
fn normalize_incoming_content(text: &str, bot_username: &str) -> Option<String> {
|
||||||
let mention = format!("@{}", bot_username);
|
let spans = Self::find_bot_mention_spans(text, bot_username);
|
||||||
let normalized = text.replace(&mention, " ");
|
if spans.is_empty() {
|
||||||
let normalized = normalized.split_whitespace().collect::<Vec<_>>().join(" ");
|
let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||||
|
return (!normalized.is_empty()).then_some(normalized);
|
||||||
if normalized.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(normalized)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut normalized = String::with_capacity(text.len());
|
||||||
|
let mut cursor = 0;
|
||||||
|
for (start, end) in spans {
|
||||||
|
normalized.push_str(&text[cursor..start]);
|
||||||
|
cursor = end;
|
||||||
|
}
|
||||||
|
normalized.push_str(&text[cursor..]);
|
||||||
|
|
||||||
|
let normalized = normalized.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||||
|
(!normalized.is_empty()).then_some(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_group_message(message: &serde_json::Value) -> bool {
|
fn is_group_message(message: &serde_json::Value) -> bool {
|
||||||
|
|
@ -751,12 +805,8 @@ Allowlist Telegram username (without '@') or numeric user ID.",
|
||||||
|
|
||||||
let content = if self.mention_only && is_group {
|
let content = if self.mention_only && is_group {
|
||||||
let bot_username = self.bot_username.lock();
|
let bot_username = self.bot_username.lock();
|
||||||
if let Some(ref bot_username) = *bot_username {
|
let bot_username = bot_username.as_ref()?;
|
||||||
Self::normalize_incoming_content(text, bot_username)
|
Self::normalize_incoming_content(text, bot_username)?
|
||||||
.unwrap_or_else(|| text.to_string())
|
|
||||||
} else {
|
|
||||||
text.to_string()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
text.to_string()
|
text.to_string()
|
||||||
};
|
};
|
||||||
|
|
@ -1621,6 +1671,13 @@ impl Channel for TelegramChannel {
|
||||||
tracing::info!("Telegram channel listening for messages...");
|
tracing::info!("Telegram channel listening for messages...");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
if self.mention_only {
|
||||||
|
let missing_username = self.bot_username.lock().is_none();
|
||||||
|
if missing_username {
|
||||||
|
let _ = self.get_bot_username().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let url = self.api_url("getUpdates");
|
let url = self.api_url("getUpdates");
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
"offset": offset,
|
"offset": offset,
|
||||||
|
|
@ -2627,6 +2684,10 @@ mod tests {
|
||||||
"Hey @mybot how are you?",
|
"Hey @mybot how are you?",
|
||||||
"mybot"
|
"mybot"
|
||||||
));
|
));
|
||||||
|
assert!(TelegramChannel::contains_bot_mention(
|
||||||
|
"Hello @MyBot, can you help?",
|
||||||
|
"mybot"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -2639,6 +2700,10 @@ mod tests {
|
||||||
"Hello mybot",
|
"Hello mybot",
|
||||||
"mybot"
|
"mybot"
|
||||||
));
|
));
|
||||||
|
assert!(!TelegramChannel::contains_bot_mention(
|
||||||
|
"Hello @mybot2",
|
||||||
|
"mybot"
|
||||||
|
));
|
||||||
assert!(!TelegramChannel::contains_bot_mention("", "mybot"));
|
assert!(!TelegramChannel::contains_bot_mention("", "mybot"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2660,6 +2725,81 @@ mod tests {
|
||||||
assert_eq!(result, None);
|
assert_eq!(result, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_update_message_mention_only_group_requires_exact_mention() {
|
||||||
|
let ch = TelegramChannel::new("token".into(), vec!["*".into()], true);
|
||||||
|
{
|
||||||
|
let mut cache = ch.bot_username.lock();
|
||||||
|
*cache = Some("mybot".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let update = serde_json::json!({
|
||||||
|
"update_id": 10,
|
||||||
|
"message": {
|
||||||
|
"message_id": 44,
|
||||||
|
"text": "hello @mybot2",
|
||||||
|
"from": {
|
||||||
|
"id": 555,
|
||||||
|
"username": "alice"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"id": -100_200_300,
|
||||||
|
"type": "group"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(ch.parse_update_message(&update).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_update_message_mention_only_group_strips_mention_and_drops_empty() {
|
||||||
|
let ch = TelegramChannel::new("token".into(), vec!["*".into()], true);
|
||||||
|
{
|
||||||
|
let mut cache = ch.bot_username.lock();
|
||||||
|
*cache = Some("mybot".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let update = serde_json::json!({
|
||||||
|
"update_id": 11,
|
||||||
|
"message": {
|
||||||
|
"message_id": 45,
|
||||||
|
"text": "Hi @MyBot status please",
|
||||||
|
"from": {
|
||||||
|
"id": 555,
|
||||||
|
"username": "alice"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"id": -100_200_300,
|
||||||
|
"type": "group"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let parsed = ch
|
||||||
|
.parse_update_message(&update)
|
||||||
|
.expect("mention should parse");
|
||||||
|
assert_eq!(parsed.content, "Hi status please");
|
||||||
|
|
||||||
|
let empty_update = serde_json::json!({
|
||||||
|
"update_id": 12,
|
||||||
|
"message": {
|
||||||
|
"message_id": 46,
|
||||||
|
"text": "@mybot",
|
||||||
|
"from": {
|
||||||
|
"id": 555,
|
||||||
|
"username": "alice"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"id": -100_200_300,
|
||||||
|
"type": "group"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(ch.parse_update_message(&empty_update).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn telegram_is_group_message_detects_groups() {
|
fn telegram_is_group_message_detects_groups() {
|
||||||
let group_msg = serde_json::json!({
|
let group_msg = serde_json::json!({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue