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}