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}