Skip to main content

slack_blocks_render/
references.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use slack_morphism::prelude::*;
5
6use crate::visitor::{visit_slack_rich_text_block, SlackRichTextBlock, Visitor};
7
8#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
9pub struct SlackReferences {
10    #[serde(default = "HashMap::new")]
11    pub channels: HashMap<SlackChannelId, Option<String>>,
12    #[serde(default = "HashMap::new")]
13    pub users: HashMap<SlackUserId, Option<String>>,
14    #[serde(default = "HashMap::new")]
15    pub usergroups: HashMap<SlackUserGroupId, Option<String>>,
16    #[serde(default = "HashMap::new")]
17    pub emojis: HashMap<SlackEmojiName, Option<SlackEmojiRef>>,
18    #[serde(default)]
19    pub user_id_to_highlight: Option<SlackUserId>,
20    #[serde(default)]
21    pub usergroup_ids_to_highlight: Option<Vec<SlackUserGroupId>>,
22}
23
24impl SlackReferences {
25    pub fn new() -> SlackReferences {
26        SlackReferences {
27            channels: HashMap::new(),
28            users: HashMap::new(),
29            usergroups: HashMap::new(),
30            emojis: HashMap::new(),
31            user_id_to_highlight: None,
32            usergroup_ids_to_highlight: None,
33        }
34    }
35
36    pub fn extend(&mut self, other: SlackReferences) {
37        self.users.extend(other.users);
38        self.usergroups.extend(other.usergroups);
39        self.channels.extend(other.channels);
40        self.emojis.extend(other.emojis);
41        if let Some(other_ids) = other.usergroup_ids_to_highlight {
42            self.usergroup_ids_to_highlight
43                .get_or_insert_with(Vec::new)
44                .extend(other_ids);
45        }
46    }
47
48    pub fn is_empty(&self) -> bool {
49        self.users.is_empty()
50            && self.usergroups.is_empty()
51            && self.channels.is_empty()
52            && self.emojis.is_empty()
53    }
54}
55
56impl Default for SlackReferences {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62struct SlackReferencesFinder {
63    pub slack_references: SlackReferences,
64}
65
66impl SlackReferencesFinder {
67    pub fn new() -> SlackReferencesFinder {
68        SlackReferencesFinder {
69            slack_references: SlackReferences::default(),
70        }
71    }
72}
73
74pub fn find_slack_references_in_blocks(blocks: &[SlackBlock]) -> SlackReferences {
75    let mut finder = SlackReferencesFinder::new();
76    for block in blocks {
77        finder.visit_slack_block(block);
78    }
79    finder.slack_references
80}
81
82impl Visitor for SlackReferencesFinder {
83    fn visit_slack_rich_text_block(&mut self, slack_rich_text_block: &SlackRichTextBlock) {
84        find_slack_references_in_rich_text_block(
85            slack_rich_text_block.json_value.clone(),
86            &mut self.slack_references,
87        );
88        visit_slack_rich_text_block(self, slack_rich_text_block);
89    }
90}
91
92fn find_slack_references_in_rich_text_block(
93    json_value: serde_json::Value,
94    slack_references: &mut SlackReferences,
95) {
96    let Some(serde_json::Value::Array(elements)) = json_value.get("elements") else {
97        return;
98    };
99
100    for element in elements {
101        match (
102            element.get("type").map(|t| t.as_str()),
103            element.get("style"),
104            element.get("elements"),
105        ) {
106            (Some(Some("rich_text_section")), None, Some(serde_json::Value::Array(elements))) => {
107                find_slack_references_in_rich_text_section_elements(elements, slack_references)
108            }
109            (Some(Some("rich_text_list")), _, Some(serde_json::Value::Array(elements))) => {
110                find_slack_references_in_rich_text_list_elements(elements, slack_references)
111            }
112            (
113                Some(Some("rich_text_preformatted")),
114                None,
115                Some(serde_json::Value::Array(elements)),
116            ) => {
117                find_slack_references_in_rich_text_preformatted_elements(elements, slack_references)
118            }
119            (Some(Some("rich_text_quote")), None, Some(serde_json::Value::Array(elements))) => {
120                find_slack_references_in_rich_text_quote_elements(elements, slack_references)
121            }
122            _ => {}
123        }
124    }
125}
126
127fn find_slack_references_in_rich_text_section_elements(
128    elements: &[serde_json::Value],
129    slack_references: &mut SlackReferences,
130) {
131    for element in elements {
132        find_slack_references_in_rich_text_section_element(element, slack_references);
133    }
134}
135
136fn find_slack_references_in_rich_text_list_elements(
137    elements: &[serde_json::Value],
138    slack_references: &mut SlackReferences,
139) {
140    for element in elements {
141        if let Some(serde_json::Value::Array(inner_elements)) = element.get("elements") {
142            find_slack_references_in_rich_text_section_elements(inner_elements, slack_references)
143        }
144    }
145}
146
147fn find_slack_references_in_rich_text_preformatted_elements(
148    elements: &[serde_json::Value],
149    slack_references: &mut SlackReferences,
150) {
151    find_slack_references_in_rich_text_section_elements(elements, slack_references);
152}
153
154fn find_slack_references_in_rich_text_quote_elements(
155    elements: &[serde_json::Value],
156    slack_references: &mut SlackReferences,
157) {
158    find_slack_references_in_rich_text_section_elements(elements, slack_references);
159}
160
161fn find_slack_references_in_rich_text_section_element(
162    element: &serde_json::Value,
163    slack_references: &mut SlackReferences,
164) {
165    match element.get("type").map(|t| t.as_str()) {
166        Some(Some("channel")) => {
167            let Some(serde_json::Value::String(channel_id)) = element.get("channel_id") else {
168                return;
169            };
170            slack_references
171                .channels
172                .insert(SlackChannelId(channel_id.to_string()), None);
173        }
174        Some(Some("user")) => {
175            let Some(serde_json::Value::String(user_id)) = element.get("user_id") else {
176                return;
177            };
178            slack_references
179                .users
180                .insert(SlackUserId(user_id.to_string()), None);
181        }
182        Some(Some("usergroup")) => {
183            let Some(serde_json::Value::String(usergroup_id)) = element.get("usergroup_id") else {
184                return;
185            };
186            slack_references
187                .usergroups
188                .insert(SlackUserGroupId(usergroup_id.to_string()), None);
189        }
190        Some(Some("emoji")) => {
191            let Some(serde_json::Value::String(name)) = element.get("name") else {
192                return;
193            };
194            let splitted = name.split("::skin-tone-").collect::<Vec<&str>>();
195            let Some(first) = splitted.first() else {
196                slack_references
197                    .emojis
198                    .insert(SlackEmojiName(name.to_string()), None);
199                return;
200            };
201            if emojis::get_by_shortcode(first).is_none() {
202                slack_references
203                    .emojis
204                    .insert(SlackEmojiName(name.to_string()), None);
205            };
206        }
207        _ => {}
208    }
209}
210
211#[cfg(test)]
212mod test {
213    use super::*;
214    use crate::test_utils::rich_text_block;
215
216    #[test]
217    fn test_find_slack_references_with_user_id() {
218        let blocks = vec![rich_text_block(serde_json::json!({
219            "type": "rich_text",
220            "elements": [
221                {
222                    "type": "rich_text_section",
223                    "elements": [
224                        {
225                            "type": "user",
226                            "user_id": "user1"
227                        }
228                    ]
229                }
230            ]
231        }))];
232        assert_eq!(
233            find_slack_references_in_blocks(&blocks),
234            SlackReferences {
235                users: HashMap::from([(SlackUserId("user1".to_string()), None)]),
236                ..SlackReferences::default()
237            }
238        );
239    }
240
241    #[test]
242    fn test_find_slack_references_with_usergroup_id() {
243        let blocks = vec![rich_text_block(serde_json::json!({
244            "type": "rich_text",
245            "elements": [
246                {
247                    "type": "rich_text_section",
248                    "elements": [
249                        {
250                            "type": "usergroup",
251                            "usergroup_id": "group1"
252                        }
253                    ]
254                }
255            ]
256        }))];
257        assert_eq!(
258            find_slack_references_in_blocks(&blocks),
259            SlackReferences {
260                usergroups: HashMap::from([(SlackUserGroupId("group1".to_string()), None)]),
261                ..SlackReferences::default()
262            }
263        );
264    }
265
266    #[test]
267    fn test_find_slack_references_with_channel_id() {
268        let blocks = vec![rich_text_block(serde_json::json!({
269            "type": "rich_text",
270            "elements": [
271                {
272                    "type": "rich_text_section",
273                    "elements": [
274                        {
275                            "type": "channel",
276                            "channel_id": "C0123456"
277                        }
278                    ]
279                }
280            ]
281        }))];
282        assert_eq!(
283            find_slack_references_in_blocks(&blocks),
284            SlackReferences {
285                channels: HashMap::from([(SlackChannelId("C0123456".to_string()), None)]),
286                ..SlackReferences::default()
287            }
288        );
289    }
290
291    #[test]
292    fn test_find_slack_references_with_multiple_references() {
293        let blocks = vec![rich_text_block(serde_json::json!({
294            "type": "rich_text",
295            "elements": [
296                {
297                    "type": "rich_text_section",
298                    "elements": [
299                        {
300                            "type": "user",
301                            "user_id": "user1"
302                        },
303                        {
304                            "type": "channel",
305                            "channel_id": "C1234567"
306                        },
307                        {
308                            "type": "usergroup",
309                            "usergroup_id": "group1"
310                        },
311                        {
312                            "type": "emoji",
313                            "name": "aaa"
314                        }
315                    ]
316                },
317                {
318                    "type": "rich_text_section",
319                    "elements": [
320                        {
321                            "type": "user",
322                            "user_id": "user2"
323                        },
324                        {
325                            "type": "channel",
326                            "channel_id": "C0123456"
327                        },
328                        {
329                            "type": "usergroup",
330                            "usergroup_id": "group2"
331                        },
332                        {
333                            "type": "emoji",
334                            "name": "bbb"
335                        }
336                    ]
337                },
338            ]
339        }))];
340        assert_eq!(
341            find_slack_references_in_blocks(&blocks),
342            SlackReferences {
343                channels: HashMap::from([
344                    (SlackChannelId("C0123456".to_string()), None),
345                    (SlackChannelId("C1234567".to_string()), None)
346                ]),
347                users: HashMap::from([
348                    (SlackUserId("user1".to_string()), None),
349                    (SlackUserId("user2".to_string()), None)
350                ]),
351                usergroups: HashMap::from([
352                    (SlackUserGroupId("group1".to_string()), None),
353                    (SlackUserGroupId("group2".to_string()), None)
354                ]),
355                emojis: HashMap::from([
356                    (SlackEmojiName("aaa".to_string()), None),
357                    (SlackEmojiName("bbb".to_string()), None)
358                ]),
359                ..SlackReferences::default()
360            }
361        );
362    }
363
364    #[test]
365    fn test_find_slack_references_with_known_emoji() {
366        let blocks = vec![rich_text_block(serde_json::json!({
367            "type": "rich_text",
368            "elements": [
369                {
370                    "type": "rich_text_section",
371                    "elements": [
372                        {
373                            "type": "emoji",
374                            "name": "wave"
375                        }
376                    ]
377                }
378            ]
379        }))];
380        assert_eq!(
381            find_slack_references_in_blocks(&blocks),
382            SlackReferences::default()
383        );
384    }
385
386    #[test]
387    fn test_find_slack_references_with_known_skinned_emoji() {
388        let blocks = vec![rich_text_block(serde_json::json!({
389            "type": "rich_text",
390            "elements": [
391                {
392                    "type": "rich_text_section",
393                    "elements": [
394                        {
395                            "type": "emoji",
396                            "name": "wave::skin-tone-2"
397                        }
398                    ]
399                }
400            ]
401        }))];
402        assert_eq!(
403            find_slack_references_in_blocks(&blocks),
404            SlackReferences::default()
405        );
406    }
407
408    #[test]
409    fn test_find_slack_references_with_unknown_emoji() {
410        let blocks = vec![rich_text_block(serde_json::json!({
411            "type": "rich_text",
412            "elements": [
413                {
414                    "type": "rich_text_section",
415                    "elements": [
416                        {
417                            "type": "emoji",
418                            "name": "bbb"
419                        }
420                    ]
421                }
422            ]
423        }))];
424        assert_eq!(
425            find_slack_references_in_blocks(&blocks),
426            SlackReferences {
427                emojis: HashMap::from([(SlackEmojiName("bbb".to_string()), None)]),
428                ..SlackReferences::default()
429            }
430        );
431    }
432}