sfr_types/
slash_command.rs

1//! The type that represents a request of a slash command.
2//!
3//! <https://api.slack.com/interactivity/slash-commands#app_command_handling>
4
5use crate::{Block, Layouts, MessagePayloads};
6use serde::{Deserialize, Serialize};
7
8/// The type that represents a request of a slash command.
9///
10/// <https://api.slack.com/interactivity/slash-commands#app_command_handling>
11#[derive(Deserialize, Debug, Clone)]
12#[serde(rename_all = "snake_case")]
13pub struct SlashCommandBody {
14    /// (Deprecated) This is a verification token, a deprecated feature that you shouldn't use any more.
15    pub token: String,
16
17    /// The command that was entered to trigger this request.
18    pub command: String,
19
20    /// This is the part of the slash command after the command itself, and it can contain absolutely anything the user might decide to type.
21    pub text: String,
22
23    /// A temporary [webhook URL](https://api.slack.com/messaging/webhooks) that you can use to [generate message responses](https://api.slack.com/interactivity/handling#message_responses).
24    pub response_url: String,
25
26    /// A short-lived ID that will allow your app to open [a modal](https://api.slack.com/surfaces/modals).
27    pub trigger_id: String,
28
29    /// The ID of the user who triggered the command.
30    pub user_id: String,
31
32    /// (Deprecated) The plain text name of the user who triggered the command.
33    pub user_name: String,
34
35    /// These IDs provide context about where the user was in Slack when they triggered your app's command (e.g. the workspace, Enterprise Grid, or channel).
36    pub team_id: String,
37
38    /// (Deprecated)
39    pub team_domain: String,
40
41    /// These IDs provide context about where the user was in Slack when they triggered your app's command (e.g. the workspace, Enterprise Grid, or channel).
42    pub enterprise_id: Option<String>,
43
44    /// (Deprecated)
45    pub enterprise_name: Option<String>,
46
47    /// These IDs provide context about where the user was in Slack when they triggered your app's command (e.g. the workspace, Enterprise Grid, or channel).
48    pub channel_id: String,
49
50    /// (Deprecated)
51    pub channel_name: String,
52
53    /// Your Slack app's unique identifier.
54    pub api_app_id: String,
55
56    /// `is_enterprise_install`
57    pub is_enterprise_install: bool,
58}
59
60/// The response type for slash command. <https://api.slack.com/interactivity/slash-commands#responding_to_commands>
61#[derive(Debug, Clone)]
62pub enum SlashCommandResponse {
63    /// no response
64    Empty,
65
66    /// as plain text
67    String(String),
68
69    /// complex response
70    Layouts(Layouts),
71
72    /// complex response and in channel
73    LayoutsInChannel(Layouts),
74}
75
76impl SlashCommandResponse {
77    /// A short-hand to create [`SlashCommandResponse::Empty`].
78    pub fn empty() -> Self {
79        Self::Empty
80    }
81
82    /// A short-hand to create [`SlashCommandResponse::String`].
83    pub fn string(string: String) -> Self {
84        Self::String(string)
85    }
86}
87
88mod inner {
89    //! The inner module to define serializing and deserializing.
90
91    use super::*;
92    use serde::ser::{Error as SerError, Serialize, Serializer};
93
94    /// The response JSON type for slash command. <https://api.slack.com/interactivity/slash-commands#responding_to_commands>
95    #[derive(Serialize)]
96    struct TmpLayouts<'a> {
97        /// The `response_type` parameter in the JSON payload controls this visibility; by default it is set to `ephemeral`, but you can specify a value of `in_channel` to post the response into the channel.
98        ///
99        /// <https://api.slack.com/interactivity/slash-commands#app_command_handling:~:text=range%20of%20possibilities.-,Message%20Visibility,-There%27s%20one%20special>
100        #[serde(skip_serializing_if = "Option::is_none")]
101        response_type: Option<SlashCommandResponseType>,
102
103        /// Sending an immediate response.
104        ///
105        /// <https://api.slack.com/interactivity/slash-commands#responding_immediate_response>
106        #[serde(flatten)]
107        layouts: TmpLayoutsInner<'a>,
108    }
109
110    /// The inner type of [`TmpLayouts`].
111    #[derive(Serialize)]
112    #[serde(untagged)]
113    enum TmpLayoutsInner<'a> {
114        /// The type mapped to [`Layouts::SingleBlock`].
115        Block {
116            /// The `blocks` field.
117            blocks: [&'a Block; 1],
118        },
119
120        /// The type mapped to [`Layouts::MultipleBlocks`].
121        Blocks {
122            /// The `blocks` field.
123            blocks: &'a Vec<Block>,
124        },
125
126        /// The type mapped to [`Layouts::BlocksArray`].
127        BlocksArray(&'a MessagePayloads),
128    }
129
130    /// `Message Visibility`: <https://api.slack.com/interactivity/slash-commands#command_payload_descriptions:~:text=range%20of%20possibilities.-,Message%20Visibility,-There%27s%20one%20special>
131    #[derive(Serialize, Debug, Clone)]
132    #[serde(rename_all = "snake_case")]
133    enum SlashCommandResponseType {
134        // Ephemeral,
135        /// When the `response_type` is `in_channel`, both the response message and the initial slash command entered by the user will be shared in the channel.
136        InChannel,
137    }
138
139    impl Serialize for SlashCommandResponse {
140        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141        where
142            S: Serializer,
143        {
144            match self {
145                Self::Layouts(layouts) | Self::LayoutsInChannel(layouts) => {
146                    let response_type = matches!(self, Self::LayoutsInChannel(_))
147                        .then_some(SlashCommandResponseType::InChannel);
148
149                    into_tmp_layouts(response_type, layouts).serialize(serializer)
150                }
151
152                Self::Empty | Self::String(_) => {
153                    Err(S::Error::custom("Empty or String must not serialize"))
154                }
155            }
156        }
157    }
158
159    /// Converts into a valid [`TmpLayouts`].
160    fn into_tmp_layouts(
161        response_type: Option<SlashCommandResponseType>,
162        layouts: &Layouts,
163    ) -> TmpLayouts<'_> {
164        let layouts = match layouts {
165            Layouts::SingleBlock(block) => TmpLayoutsInner::Block { blocks: [block] },
166            Layouts::MultipleBlocks(blocks) => TmpLayoutsInner::Blocks { blocks },
167            Layouts::BlocksArray(payload) => TmpLayoutsInner::BlocksArray(payload),
168        };
169
170        TmpLayouts {
171            response_type,
172            layouts,
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    use crate::SectionBlock;
182    use crate::{PlainTextObject, TextObject};
183    use anyhow::Result;
184
185    #[test]
186    fn test_serialize_empty() -> Result<()> {
187        let testdate = SlashCommandResponse::empty();
188        assert!(serde_json::to_string(&testdate).is_err());
189        Ok(())
190    }
191
192    #[test]
193    fn test_serialize_text() -> Result<()> {
194        let testdate = SlashCommandResponse::String("test".into());
195        assert!(serde_json::to_string(&testdate).is_err());
196        Ok(())
197    }
198
199    fn only_text_layouts(text: String) -> Layouts {
200        Layouts::BlocksArray(MessagePayloads {
201            text,
202
203            blocks: Default::default(),
204            attachments: Default::default(),
205            thread_ts: Default::default(),
206            mrkdwn: Default::default(),
207        })
208    }
209
210    #[test]
211    fn test_serialize_layouts_to_ephemeral() -> Result<()> {
212        let testdate = SlashCommandResponse::Layouts(only_text_layouts("test".into()));
213        let expected = r#"{"text":"test"}"#;
214        assert_eq!(serde_json::to_string(&testdate)?, expected);
215
216        let testdate = SlashCommandResponse::Layouts(Layouts::MultipleBlocks(vec![]));
217        let expected = r#"{"blocks":[]}"#;
218        assert_eq!(serde_json::to_string(&testdate)?, expected);
219
220        let testdate = SlashCommandResponse::Layouts(Layouts::SingleBlock(Box::new(
221            Block::Section(SectionBlock {
222                text: Some(TextObject::PlainText(PlainTextObject {
223                    text: "dummy".into(),
224                    emoji: None,
225                })),
226                ..Default::default()
227            }),
228        )));
229        let expected =
230            r#"{"blocks":[{"type":"section","text":{"type":"plain_text","text":"dummy"}}]}"#;
231        assert_eq!(serde_json::to_string(&testdate)?, expected);
232
233        Ok(())
234    }
235
236    #[test]
237    fn test_serialize_layouts_to_in_channel() -> Result<()> {
238        let testdate = SlashCommandResponse::LayoutsInChannel(only_text_layouts("test".into()));
239        let expected = r#"{"response_type":"in_channel","text":"test"}"#;
240        assert_eq!(serde_json::to_string(&testdate)?, expected);
241        Ok(())
242    }
243}