1use 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
145pub 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(¶m)?;
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, ¶m, &"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}