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