slack_rust/chat/
post_message.rs

1//! Sends a message to a channel.  
2//! See: <https://api.slack.com/methods/chat.postMessage>
3
4use crate::attachment::attachment::Attachment;
5use crate::block::blocks::Block;
6use crate::chat::message::Message;
7use crate::error::Error;
8use crate::http_client::{get_slack_url, ResponseMetadata, SlackWebAPIClient};
9use serde::{Deserialize, Serialize};
10use serde_with::skip_serializing_none;
11
12#[skip_serializing_none]
13#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
14pub struct PostMessageRequest {
15    pub channel: String,
16    pub attachments: Option<Vec<Attachment>>,
17    pub blocks: Option<Vec<Block>>,
18    pub text: Option<String>,
19    pub icon_emoji: Option<String>,
20    pub icon_url: Option<String>,
21    pub link_names: Option<bool>,
22    pub mrkdwn: Option<bool>,
23    pub parse: Option<String>,
24    pub reply_broadcast: Option<bool>,
25    pub thread_ts: Option<String>,
26    pub unfurl_links: Option<bool>,
27    pub unfurl_media: Option<bool>,
28    pub username: Option<String>,
29}
30
31impl PostMessageRequest {
32    pub fn builder(channel: String) -> PostMessageRequestBuilder {
33        PostMessageRequestBuilder::new(channel)
34    }
35}
36
37#[derive(Debug, Default)]
38pub struct PostMessageRequestBuilder {
39    pub channel: String,
40    pub attachments: Option<Vec<Attachment>>,
41    pub blocks: Option<Vec<Block>>,
42    pub text: Option<String>,
43    pub icon_emoji: Option<String>,
44    pub icon_url: Option<String>,
45    pub link_names: Option<bool>,
46    pub mrkdwn: Option<bool>,
47    pub parse: Option<String>,
48    pub reply_broadcast: Option<bool>,
49    pub thread_ts: Option<String>,
50    pub unfurl_links: Option<bool>,
51    pub unfurl_media: Option<bool>,
52    pub username: Option<String>,
53}
54
55impl PostMessageRequestBuilder {
56    pub fn new(channel: String) -> PostMessageRequestBuilder {
57        PostMessageRequestBuilder {
58            channel,
59            ..Default::default()
60        }
61    }
62    pub fn attachments(mut self, attachments: Vec<Attachment>) -> PostMessageRequestBuilder {
63        self.attachments = Some(attachments);
64        self
65    }
66    pub fn blocks(mut self, blocks: Vec<Block>) -> PostMessageRequestBuilder {
67        self.blocks = Some(blocks);
68        self
69    }
70    pub fn text(mut self, text: String) -> PostMessageRequestBuilder {
71        self.text = Some(text);
72        self
73    }
74    pub fn icon_emoji(mut self, icon_emoji: String) -> PostMessageRequestBuilder {
75        self.icon_emoji = Some(icon_emoji);
76        self
77    }
78    pub fn icon_url(mut self, icon_url: String) -> PostMessageRequestBuilder {
79        self.icon_url = Some(icon_url);
80        self
81    }
82    pub fn link_names(mut self, link_names: bool) -> PostMessageRequestBuilder {
83        self.link_names = Some(link_names);
84        self
85    }
86    pub fn mrkdwn(mut self, mrkdwn: bool) -> PostMessageRequestBuilder {
87        self.mrkdwn = Some(mrkdwn);
88        self
89    }
90    pub fn parse(mut self, parse: String) -> PostMessageRequestBuilder {
91        self.parse = Some(parse);
92        self
93    }
94    pub fn reply_broadcast(mut self, reply_broadcast: bool) -> PostMessageRequestBuilder {
95        self.reply_broadcast = Some(reply_broadcast);
96        self
97    }
98    pub fn thread_ts(mut self, thread_ts: String) -> PostMessageRequestBuilder {
99        self.thread_ts = Some(thread_ts);
100        self
101    }
102    pub fn unfurl_links(mut self, unfurl_links: bool) -> PostMessageRequestBuilder {
103        self.unfurl_links = Some(unfurl_links);
104        self
105    }
106    pub fn unfurl_media(mut self, unfurl_media: bool) -> PostMessageRequestBuilder {
107        self.unfurl_media = Some(unfurl_media);
108        self
109    }
110    pub fn username(mut self, username: String) -> PostMessageRequestBuilder {
111        self.username = Some(username);
112        self
113    }
114    pub fn build(self) -> PostMessageRequest {
115        PostMessageRequest {
116            channel: self.channel,
117            attachments: self.attachments,
118            blocks: self.blocks,
119            text: self.text,
120            icon_emoji: self.icon_emoji,
121            icon_url: self.icon_url,
122            link_names: self.link_names,
123            mrkdwn: self.mrkdwn,
124            parse: self.parse,
125            reply_broadcast: self.reply_broadcast,
126            thread_ts: self.thread_ts,
127            unfurl_links: self.unfurl_links,
128            unfurl_media: self.unfurl_media,
129            username: self.username,
130        }
131    }
132}
133
134#[skip_serializing_none]
135#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
136pub struct PostMessageResponse {
137    pub ok: bool,
138    pub error: Option<String>,
139    pub response_metadata: Option<ResponseMetadata>,
140    pub channel: Option<String>,
141    pub ts: Option<String>,
142    pub message: Option<Message>,
143}
144
145/// Sends a message to a channel.  
146/// See: <https://api.slack.com/methods/chat.postMessage>
147pub async fn post_message<T>(
148    client: &T,
149    param: &PostMessageRequest,
150    bot_token: &str,
151) -> Result<PostMessageResponse, Error>
152where
153    T: SlackWebAPIClient,
154{
155    let url = get_slack_url("chat.postMessage");
156    let json = serde_json::to_string(&param)?;
157
158    client
159        .post_json(&url, &json, bot_token)
160        .await
161        .and_then(|result| {
162            serde_json::from_str::<PostMessageResponse>(&result).map_err(Error::SerdeJsonError)
163        })
164}
165
166#[cfg(test)]
167mod test {
168    use super::*;
169    use crate::attachment::attachment::AttachmentField;
170    use crate::block::block_actions::ActionBlock;
171    use crate::block::block_elements::{BlockElement, ButtonElement, SelectBlockElement};
172    use crate::block::block_object::{OptionBlockObject, TextBlockObject, TextBlockType};
173    use crate::chat::post_message::PostMessageRequest;
174    use crate::http_client::MockSlackWebAPIClient;
175
176    #[test]
177    fn convert_request() {
178        let request = PostMessageRequest {
179            channel: "test".to_string(),
180            text: Some("Hello world".to_string()),
181            attachments: Some(vec![Attachment {
182                color: Some("#36a64f".to_string()),
183                author_name: Some("slack-rust".to_string()),
184                author_link: Some("https://www.irasutoya.com/".to_string()),
185                author_icon: Some("https://2.bp.blogspot.com/-3o7K8_p8NNM/WGCRsl8GiCI/AAAAAAABAoc/XKnspjvc0YIoOiSRK9HW6wXhtlnZvHQ9QCLcB/s800/pyoko_hashiru.png".to_string()),
186                title: Some("title".to_string()),
187                title_link: Some("https://www.irasutoya.com/".to_string()),
188                pretext: Some("Optional pre-text that appears above the attachment block".to_string()),
189                text: Some("Optional `text` that appears within the attachment".to_string()),
190                thumb_url: Some("https://2.bp.blogspot.com/-3o7K8_p8NNM/WGCRsl8GiCI/AAAAAAABAoc/XKnspjvc0YIoOiSRK9HW6wXhtlnZvHQ9QCLcB/s800/pyoko_hashiru.png".to_string()),
191                fields: Some(vec![
192                    AttachmentField {
193                        title: Some("A field's title".to_string()),
194                        value: Some("This field's value".to_string()),
195                        short: Some(false),
196                    },
197                ]),
198                mrkdwn_in: Some(vec!["text".to_string()]),
199                footer: Some("footer".to_string()),
200                footer_icon: Some("https://1.bp.blogspot.com/-46AF2TCkb-o/VW6ORNeQ3UI/AAAAAAAAt_4/TA4RrGVcw_U/s800/pyoko05_cycling.png".to_string(), ),
201                ts: Some(123456789),
202                ..Default::default()
203            }]),
204            blocks: Some(vec![
205                Block::ActionBlock(ActionBlock {
206                    elements: vec![
207                        BlockElement::SelectBlockElement(SelectBlockElement{
208                            placeholder: TextBlockObject {
209                                type_filed: TextBlockType::PlainText,
210                                text: "select".to_string(),
211                                ..Default::default()
212                            },
213                            action_id: "select".to_string(),
214                            options: vec![
215                                OptionBlockObject{
216                                    text: TextBlockObject {
217                                        type_filed: TextBlockType::PlainText,
218                                        text: "Select1".to_string(),
219                                        ..Default::default()
220                                    },
221                                    ..Default::default()
222                                },
223                                OptionBlockObject{
224                                    text: TextBlockObject {
225                                        type_filed: TextBlockType::PlainText,
226                                        text: "Select2".to_string(),
227                                        ..Default::default()
228                                    },
229                                    ..Default::default()
230                                },
231                            ],
232                            ..Default::default()
233                        }),
234                        BlockElement::ButtonElement(ButtonElement{
235                            text: TextBlockObject {
236                                type_filed: TextBlockType::PlainText,
237                                text: "Submit".to_string(),
238                                ..Default::default()
239                            },
240                            action_id: "button".to_string(),
241                            ..Default::default()
242                        }),
243                    ],
244                    ..Default::default()
245                }),
246            ]),
247            ..Default::default()
248        };
249        let json = r##"{
250  "channel": "test",
251  "attachments": [
252    {
253      "color": "#36a64f",
254      "author_name": "slack-rust",
255      "author_link": "https://www.irasutoya.com/",
256      "author_icon": "https://2.bp.blogspot.com/-3o7K8_p8NNM/WGCRsl8GiCI/AAAAAAABAoc/XKnspjvc0YIoOiSRK9HW6wXhtlnZvHQ9QCLcB/s800/pyoko_hashiru.png",
257      "title": "title",
258      "title_link": "https://www.irasutoya.com/",
259      "pretext": "Optional pre-text that appears above the attachment block",
260      "text": "Optional `text` that appears within the attachment",
261      "thumb_url": "https://2.bp.blogspot.com/-3o7K8_p8NNM/WGCRsl8GiCI/AAAAAAABAoc/XKnspjvc0YIoOiSRK9HW6wXhtlnZvHQ9QCLcB/s800/pyoko_hashiru.png",
262      "fields": [
263        {
264          "title": "A field's title",
265          "value": "This field's value",
266          "short": false
267        }
268      ],
269      "mrkdwn_in": [
270        "text"
271      ],
272      "footer": "footer",
273      "footer_icon": "https://1.bp.blogspot.com/-46AF2TCkb-o/VW6ORNeQ3UI/AAAAAAAAt_4/TA4RrGVcw_U/s800/pyoko05_cycling.png",
274      "ts": 123456789
275    }
276  ],
277  "blocks": [
278    {
279      "type": "actions",
280      "elements": [
281        {
282          "type": "static_select",
283          "placeholder": {
284            "type": "plain_text",
285            "text": "select"
286          },
287          "action_id": "select",
288          "options": [
289            {
290              "text": {
291                "type": "plain_text",
292                "text": "Select1"
293              }
294            },
295            {
296              "text": {
297                "type": "plain_text",
298                "text": "Select2"
299              }
300            }
301          ]
302        },
303        {
304          "type": "button",
305          "text": {
306            "type": "plain_text",
307            "text": "Submit"
308          },
309          "action_id": "button"
310        }
311      ]
312    }
313  ],
314  "text": "Hello world"
315}"##;
316
317        let j = serde_json::to_string_pretty(&request).unwrap();
318        assert_eq!(json, j);
319
320        let s = serde_json::from_str::<PostMessageRequest>(json).unwrap();
321        assert_eq!(request, s);
322    }
323
324    #[test]
325    fn convert_response() {
326        let response = PostMessageResponse {
327            ok: true,
328            channel: Some("C02H7UK23GB".to_string()),
329            ts: Some("1640258472.000200".to_string()),
330            message: Some(Message {
331                bot_id: Some("B02H2MCBRL6".to_string()),
332                type_file: Some("message".to_string()),
333                text: Some("Hello world".to_string()),
334                user: Some("U02GUNSESDD".to_string()),
335                ts: Some("1640258472.000200".to_string()),
336                team: Some("T02H7RHQNL9".to_string()),
337                blocks: Some(vec![Block::ActionBlock(ActionBlock {
338                    block_id: Some("Zf2/".to_string()),
339                    elements: vec![
340                        BlockElement::SelectBlockElement(SelectBlockElement {
341                            action_id: "select".to_string(),
342                            placeholder: TextBlockObject {
343                                type_filed: TextBlockType::PlainText,
344                                text: "select".to_string(),
345                                ..Default::default()
346                            },
347                            options: vec![OptionBlockObject {
348                                text: TextBlockObject {
349                                    type_filed: TextBlockType::PlainText,
350                                    text: "Select1".to_string(),
351                                    ..Default::default()
352                                },
353                                ..Default::default()
354                            }],
355                            ..Default::default()
356                        }),
357                        BlockElement::ButtonElement(ButtonElement {
358                            text: TextBlockObject {
359                                type_filed: TextBlockType::PlainText,
360                                text: "Submit".to_string(),
361                                ..Default::default()
362                            },
363                            action_id: "button".to_string(),
364                            ..Default::default()
365                        }),
366                    ],
367                })]),
368                ..Default::default()
369            }),
370            ..Default::default()
371        };
372        let json = r##"{
373  "ok": true,
374  "channel": "C02H7UK23GB",
375  "ts": "1640258472.000200",
376  "message": {
377    "bot_id": "B02H2MCBRL6",
378    "type": "message",
379    "text": "Hello world",
380    "user": "U02GUNSESDD",
381    "ts": "1640258472.000200",
382    "team": "T02H7RHQNL9",
383    "blocks": [
384      {
385        "type": "actions",
386        "elements": [
387          {
388            "type": "static_select",
389            "placeholder": {
390              "type": "plain_text",
391              "text": "select"
392            },
393            "action_id": "select",
394            "options": [
395              {
396                "text": {
397                  "type": "plain_text",
398                  "text": "Select1"
399                }
400              }
401            ]
402          },
403          {
404            "type": "button",
405            "text": {
406              "type": "plain_text",
407              "text": "Submit"
408            },
409            "action_id": "button"
410          }
411        ],
412        "block_id": "Zf2/"
413      }
414    ]
415  }
416}"##;
417
418        let j = serde_json::to_string_pretty(&response).unwrap();
419        assert_eq!(json, j);
420
421        let s = serde_json::from_str::<PostMessageResponse>(json).unwrap();
422        assert_eq!(response, s);
423    }
424
425    #[async_std::test]
426    async fn test_post_message() {
427        let param = PostMessageRequest {
428            channel: "test".to_string(),
429            text: Some("test".to_string()),
430            ..Default::default()
431        };
432
433        let mut mock = MockSlackWebAPIClient::new();
434        mock.expect_post_json().returning(|_, _, _| {
435            Ok(r##"{
436  "ok": true,
437  "channel": "test",
438  "message": {
439    "text": "test"
440  }
441}"##
442            .to_string())
443        });
444
445        let response = post_message(&mock, &param, &"test_token".to_string())
446            .await
447            .unwrap();
448        let expect = PostMessageResponse {
449            ok: true,
450            channel: Some("test".to_string()),
451            message: Some(Message {
452                text: Some("test".to_string()),
453                ..Default::default()
454            }),
455            ..Default::default()
456        };
457
458        assert_eq!(expect, response);
459    }
460}