Skip to main content

webex_message_handler/
mention_parser.rs

1//! Parse Webex `<spark-mention>` tags from decrypted HTML.
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use std::collections::HashSet;
6
7/// Mentions extracted from message HTML.
8#[derive(Debug, Clone, Default)]
9pub struct ParsedMentions {
10    /// Person UUIDs mentioned via @mention in the message.
11    pub mentioned_people: Vec<String>,
12    /// Group mention types (e.g. "all") in the message.
13    pub mentioned_groups: Vec<String>,
14}
15
16static MENTION_RE: Lazy<Regex> = Lazy::new(|| {
17    Regex::new(r#"(?i)<spark-mention[^>]*data-object-type="([^"]*)"[^>]*>"#).unwrap()
18});
19
20static PERSON_ID_RE: Lazy<Regex> = Lazy::new(|| {
21    Regex::new(r#"(?i)data-object-id="([^"]*)""#).unwrap()
22});
23
24static GROUP_TYPE_RE: Lazy<Regex> = Lazy::new(|| {
25    Regex::new(r#"(?i)data-group-type="([^"]*)""#).unwrap()
26});
27
28/// Extract mentioned people and groups from decrypted HTML.
29///
30/// Parses `<spark-mention>` tags to find person UUIDs and group mention
31/// types (e.g. "all"). Duplicates are removed.
32pub fn parse_mentions(html: Option<&str>) -> ParsedMentions {
33    let mut result = ParsedMentions::default();
34    let html = match html {
35        Some(h) if !h.is_empty() => h,
36        _ => return result,
37    };
38
39    let mut seen = HashSet::new();
40
41    for cap in MENTION_RE.captures_iter(html) {
42        let tag = cap.get(0).unwrap().as_str();
43        let object_type = &cap[1];
44
45        if object_type == "person" {
46            if let Some(id_cap) = PERSON_ID_RE.captures(tag) {
47                let id = &id_cap[1];
48                if !id.is_empty() && seen.insert(id.to_string()) {
49                    result.mentioned_people.push(id.to_string());
50                }
51            }
52        } else if object_type == "groupMention" {
53            if let Some(group_cap) = GROUP_TYPE_RE.captures(tag) {
54                let group_type = &group_cap[1];
55                if !group_type.is_empty() && seen.insert(format!("group:{group_type}")) {
56                    result.mentioned_groups.push(group_type.to_string());
57                }
58            }
59        }
60    }
61
62    result
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn test_single_person_mention() {
71        let html = r#"<p><spark-mention data-object-type="person" data-object-id="abc-123">Alice</spark-mention> hello</p>"#;
72        let m = parse_mentions(Some(html));
73        assert_eq!(m.mentioned_people, vec!["abc-123"]);
74        assert!(m.mentioned_groups.is_empty());
75    }
76
77    #[test]
78    fn test_group_mention() {
79        let html = r#"<p><spark-mention data-object-type="groupMention" data-group-type="all">All</spark-mention> hello</p>"#;
80        let m = parse_mentions(Some(html));
81        assert!(m.mentioned_people.is_empty());
82        assert_eq!(m.mentioned_groups, vec!["all"]);
83    }
84
85    #[test]
86    fn test_mixed_mentions() {
87        let html = r#"<p><spark-mention data-object-type="person" data-object-id="p1">Alice</spark-mention> and <spark-mention data-object-type="groupMention" data-group-type="all">All</spark-mention></p>"#;
88        let m = parse_mentions(Some(html));
89        assert_eq!(m.mentioned_people, vec!["p1"]);
90        assert_eq!(m.mentioned_groups, vec!["all"]);
91    }
92
93    #[test]
94    fn test_duplicate_dedup() {
95        let html = r#"<spark-mention data-object-type="person" data-object-id="p1">A</spark-mention> <spark-mention data-object-type="person" data-object-id="p1">A</spark-mention>"#;
96        let m = parse_mentions(Some(html));
97        assert_eq!(m.mentioned_people, vec!["p1"]);
98    }
99
100    #[test]
101    fn test_empty_html() {
102        let m = parse_mentions(None);
103        assert!(m.mentioned_people.is_empty());
104        assert!(m.mentioned_groups.is_empty());
105    }
106
107    #[test]
108    fn test_no_mentions() {
109        let m = parse_mentions(Some("<p>Hello world</p>"));
110        assert!(m.mentioned_people.is_empty());
111        assert!(m.mentioned_groups.is_empty());
112    }
113}