1use rsb_derive::Builder;
2use serde::{Deserialize, Serialize};
3use serde_with::{serde_as, skip_serializing_none};
4
5use crate::blocks::*;
6use crate::models::messages::*;
7use crate::*;
8use std::collections::HashMap;
9
10#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
11#[serde(tag = "type")]
12pub enum SlackInteractionEvent {
13 #[serde(rename = "block_actions")]
14 BlockActions(SlackInteractionBlockActionsEvent),
15 #[serde(rename = "block_suggestion")]
16 BlockSuggestion(SlackInteractionBlockSuggestionEvent),
17 #[serde(rename = "dialog_submission")]
18 DialogSubmission(SlackInteractionDialogueSubmissionEvent),
19 #[serde(rename = "message_action")]
20 MessageAction(SlackInteractionMessageActionEvent),
21 #[serde(rename = "shortcut")]
22 Shortcut(SlackInteractionShortcutEvent),
23 #[serde(rename = "view_submission")]
24 ViewSubmission(SlackInteractionViewSubmissionEvent),
25 #[serde(rename = "view_closed")]
26 ViewClosed(SlackInteractionViewClosedEvent),
27}
28
29#[skip_serializing_none]
30#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
31pub struct SlackInteractionBlockActionsEvent {
32 pub team: SlackBasicTeamInfo,
33 pub user: Option<SlackBasicUserInfo>,
34 pub api_app_id: SlackAppId,
35 pub container: SlackInteractionActionContainer,
36 pub trigger_id: SlackTriggerId,
37 pub channel: Option<SlackBasicChannelInfo>,
38 pub message: Option<SlackHistoryMessage>,
39 pub view: Option<SlackView>,
40 pub response_url: Option<SlackResponseUrl>,
41 pub actions: Option<Vec<SlackInteractionActionInfo>>,
42 pub state: Option<SlackActionState>,
43}
44
45#[skip_serializing_none]
46#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
47pub struct SlackInteractionBlockSuggestionEvent {
48 pub team: SlackBasicTeamInfo,
49 pub user: SlackBasicUserInfo,
50 pub api_app_id: SlackAppId,
51 pub block_id: SlackBlockId,
52 pub action_id: SlackActionId,
53 pub container: SlackInteractionActionContainer,
54 pub channel: Option<SlackBasicChannelInfo>,
55 pub view: Option<SlackView>,
56 pub value: String,
57 pub message: Option<SlackHistoryMessage>,
58}
59
60#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
61#[serde(tag = "type")]
62pub enum SlackInteractionActionContainer {
63 #[serde(rename = "message")]
64 Message(SlackInteractionActionMessageContainer),
65 #[serde(rename = "message_attachment")]
66 MessageAttachment(SlackInteractionActionMessageAttachmentContainer),
67 #[serde(rename = "view")]
68 View(SlackInteractionActionViewContainer),
69}
70
71#[skip_serializing_none]
72#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
73pub struct SlackInteractionActionMessageContainer {
74 pub message_ts: SlackTs,
75 pub channel_id: Option<SlackChannelId>,
76 pub is_ephemeral: Option<bool>,
77 pub is_app_unfurl: Option<bool>,
78}
79
80#[skip_serializing_none]
81#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
82pub struct SlackInteractionActionMessageAttachmentContainer {
83 pub message_ts: SlackTs,
84 pub attachment_id: SlackMessageAttachmentId,
85 pub channel_id: Option<SlackChannelId>,
86 pub is_ephemeral: Option<bool>,
87 pub is_app_unfurl: Option<bool>,
88}
89
90#[skip_serializing_none]
91#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
92pub struct SlackInteractionActionViewContainer {
93 pub view_id: SlackViewId,
94}
95
96#[skip_serializing_none]
97#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
98pub struct SlackInteractionActionInfo {
99 #[serde(rename = "type")]
100 pub action_type: SlackActionType,
101 pub action_id: SlackActionId,
102 pub block_id: Option<SlackBlockId>,
103 pub text: Option<SlackBlockText>,
104 pub value: Option<String>,
105 pub selected_option: Option<SlackBlockChoiceItem<SlackBlockText>>,
106 pub selected_options: Option<Vec<SlackBlockChoiceItem<SlackBlockText>>>,
107 pub action_ts: Option<SlackTs>,
108}
109
110#[serde_as]
111#[skip_serializing_none]
112#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
113pub struct SlackInteractionDialogueSubmissionEvent {
114 pub team: SlackBasicTeamInfo,
115 pub user: SlackBasicUserInfo,
116 pub channel: Option<SlackBasicChannelInfo>,
117 #[serde(default)]
118 #[serde_as(as = "serde_with::NoneAsEmptyString")]
119 pub callback_id: Option<SlackCallbackId>,
120 pub state: Option<String>,
121 pub submission: HashMap<String, String>,
122}
123
124#[skip_serializing_none]
125#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
126pub struct SlackInteractionMessageActionEvent {
127 pub team: SlackBasicTeamInfo,
128 pub user: SlackBasicUserInfo,
129 pub channel: Option<SlackBasicChannelInfo>,
130 pub message: Option<SlackHistoryMessage>,
131 pub callback_id: SlackCallbackId,
132 pub trigger_id: SlackTriggerId,
133 pub response_url: SlackResponseUrl,
134 pub actions: Option<Vec<SlackInteractionActionInfo>>,
135}
136
137#[skip_serializing_none]
138#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
139pub struct SlackInteractionShortcutEvent {
140 pub team: SlackBasicTeamInfo,
141 pub user: SlackBasicUserInfo,
142 pub callback_id: SlackCallbackId,
143 pub trigger_id: SlackTriggerId,
144 pub actions: Option<Vec<SlackInteractionActionInfo>>,
145}
146
147#[skip_serializing_none]
148#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
149pub struct SlackInteractionViewSubmissionEvent {
150 pub team: SlackBasicTeamInfo,
151 pub user: SlackBasicUserInfo,
152 pub view: SlackStatefulView,
153 pub trigger_id: Option<SlackTriggerId>,
154}
155
156#[skip_serializing_none]
157#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
158pub struct SlackInteractionViewClosedEvent {
159 pub team: SlackBasicTeamInfo,
160 pub user: SlackBasicUserInfo,
161 pub view: SlackStatefulView,
162 pub trigger_id: Option<SlackTriggerId>,
163}
164
165#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
166#[serde(untagged)]
167pub enum SlackBlockSuggestionResponse {
168 Options(SlackBlockSuggestionOptions),
169 OptionGroups(SlackBlockSuggestionOptionGroups),
170}
171
172#[skip_serializing_none]
173#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
174pub struct SlackBlockSuggestionOptions {
175 pub options: Vec<SlackBlockChoiceItem<SlackBlockPlainTextOnly>>,
176}
177
178#[skip_serializing_none]
179#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
180pub struct SlackBlockSuggestionOptionGroups {
181 pub option_groups: Vec<SlackBlockOptionGroup<SlackBlockPlainTextOnly>>,
182}
183
184#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
194#[serde(untagged)]
195pub enum SlackInteractionResponse {
196 #[serde(skip)]
197 Empty,
198 ViewSubmission(SlackViewSubmissionResponse),
199 BlockSuggestion(SlackBlockSuggestionResponse),
200}
201
202impl From<()> for SlackInteractionResponse {
203 fn from(_: ()) -> Self {
204 SlackInteractionResponse::Empty
205 }
206}
207
208impl From<SlackViewSubmissionResponse> for SlackInteractionResponse {
209 fn from(response: SlackViewSubmissionResponse) -> Self {
210 SlackInteractionResponse::ViewSubmission(response)
211 }
212}
213
214impl From<SlackBlockSuggestionResponse> for SlackInteractionResponse {
215 fn from(response: SlackBlockSuggestionResponse) -> Self {
216 SlackInteractionResponse::BlockSuggestion(response)
217 }
218}
219
220#[cfg(test)]
221mod test {
222 use super::*;
223
224 #[test]
225 fn test_block_suggestion_view_event_deserialization() {
226 let payload = include_str!("./fixtures/interaction_block_suggestion_view.json");
227 let event: SlackInteractionEvent = serde_json::from_str(payload).unwrap();
228
229 match event {
230 SlackInteractionEvent::BlockSuggestion(ev) => {
231 assert_eq!(ev.value, "sentien");
232 assert_eq!(ev.action_id, "my-external-select-action".into());
233 assert_eq!(ev.block_id, "my-external-select-block".into());
234 assert!(matches!(
235 ev.container,
236 SlackInteractionActionContainer::View(_)
237 ));
238 assert!(ev.view.is_some());
239 assert_eq!(ev.channel, None);
240 assert_eq!(ev.message, None);
241 }
242 other => panic!("Unexpected interaction event: {:?}", other),
243 }
244 }
245
246 #[test]
247 fn test_block_suggestion_message_event_deserialization() {
248 let payload = include_str!("./fixtures/interaction_block_suggestion_message.json");
249 let event: SlackInteractionEvent = serde_json::from_str(payload).unwrap();
250
251 match event {
252 SlackInteractionEvent::BlockSuggestion(ev) => {
253 assert_eq!(ev.value, "sentien");
254 assert_eq!(ev.action_id, "my-external-select-action".into());
255 assert_eq!(ev.block_id, "my-external-select-block".into());
256 assert!(matches!(
257 ev.container,
258 SlackInteractionActionContainer::Message(_)
259 ));
260 assert_eq!(
261 ev.channel.map(|channel| channel.id),
262 Some("CXXXXXXXXXX".into())
263 );
264 assert!(ev.message.is_some());
265 assert!(ev.view.is_none());
266 }
267 other => panic!("Unexpected interaction event: {:?}", other),
268 }
269 }
270
271 #[test]
272 fn test_block_suggestion_options_response_serialization() {
273 let response =
274 SlackBlockSuggestionResponse::Options(SlackBlockSuggestionOptions::new(vec![
275 SlackBlockChoiceItem::new(
276 SlackBlockPlainTextOnly::from("Unexpected sentience"),
277 "AI-2323".to_string(),
278 )
279 .with_description(SlackBlockPlainTextOnly::from("Reported by the night shift")),
280 ]));
281
282 assert_eq!(
283 serde_json::to_string(&response).unwrap(),
284 r#"{"options":[{"text":{"type":"plain_text","text":"Unexpected sentience"},"value":"AI-2323","description":{"type":"plain_text","text":"Reported by the night shift"}}]}"#
285 );
286 }
287
288 #[test]
289 fn test_block_suggestion_option_groups_response_serialization() {
290 let response = SlackBlockSuggestionResponse::OptionGroups(
291 SlackBlockSuggestionOptionGroups::new(vec![SlackBlockOptionGroup::new(
292 SlackBlockPlainTextOnly::from("Open"),
293 vec![SlackBlockChoiceItem::new(
294 SlackBlockPlainTextOnly::from("Unexpected sentience"),
295 "AI-2323".to_string(),
296 )],
297 )]),
298 );
299
300 assert_eq!(
301 serde_json::to_string(&response).unwrap(),
302 r#"{"option_groups":[{"label":{"type":"plain_text","text":"Open"},"options":[{"text":{"type":"plain_text","text":"Unexpected sentience"},"value":"AI-2323"}]}]}"#
303 );
304 }
305
306 #[test]
307 fn test_interaction_response_from_unit_is_empty() {
308 assert_eq!(
309 SlackInteractionResponse::from(()),
310 SlackInteractionResponse::Empty
311 );
312 }
313
314 #[test]
315 fn test_interaction_response_empty_is_not_serializable() {
316 assert!(serde_json::to_string(&SlackInteractionResponse::Empty).is_err());
317 }
318
319 #[test]
320 fn test_interaction_response_block_suggestion_serialization() {
321 let inner = SlackBlockSuggestionResponse::Options(SlackBlockSuggestionOptions::new(vec![
322 SlackBlockChoiceItem::new(
323 SlackBlockPlainTextOnly::from("Unexpected sentience"),
324 "AI-2323".to_string(),
325 ),
326 ]));
327
328 assert_eq!(
329 serde_json::to_string(&SlackInteractionResponse::from(inner.clone())).unwrap(),
330 serde_json::to_string(&inner).unwrap()
331 );
332 }
333
334 #[test]
335 fn test_interaction_response_view_submission_serialization() {
336 let inner = SlackViewSubmissionResponse::Clear(SlackViewSubmissionClearResponse::new());
337
338 assert_eq!(
339 serde_json::to_string(&SlackInteractionResponse::from(inner)).unwrap(),
340 r#"{"response_action":"clear"}"#
341 );
342 }
343}