1use crate::error::Error;
2use crate::http_client::{get_slack_url, ResponseMetadata, SlackWebAPIClient};
3use crate::views::view::View;
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6
7#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
8pub struct PushRequest {
9 pub trigger_id: String,
10 pub view: View,
11}
12
13#[skip_serializing_none]
14#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
15pub struct PushResponse {
16 pub ok: bool,
17 pub error: Option<String>,
18 pub response_metadata: Option<ResponseMetadata>,
19 pub view: Option<View>,
20}
21
22pub async fn push<T>(
23 client: &T,
24 param: &PushRequest,
25 bot_token: &str,
26) -> Result<PushResponse, Error>
27where
28 T: SlackWebAPIClient,
29{
30 let url = get_slack_url("views.push");
31 let json = serde_json::to_string(¶m)?;
32
33 client
34 .post_json(&url, &json, bot_token)
35 .await
36 .and_then(|result| {
37 serde_json::from_str::<PushResponse>(&result).map_err(Error::SerdeJsonError)
38 })
39}
40
41#[cfg(test)]
42mod test {
43 use super::*;
44 use crate::block::block_actions::ActionBlock;
45 use crate::block::block_elements::{
46 BlockElement, ButtonElement, MultiSelectBlockElement, PlainTextInputBlockElement,
47 };
48 use crate::block::block_input::InputBlock;
49 use crate::block::block_object::{OptionBlockObject, TextBlockObject, TextBlockType};
50 use crate::block::blocks::Block;
51
52 use crate::http_client::MockSlackWebAPIClient;
53 use crate::views::view::ViewType;
54
55 #[test]
56 fn convert_request() {
57 let request = PushRequest {
58 trigger_id: "12345.98765.abcd2358fdea".to_string(),
59 view: View {
60 type_filed: Some(ViewType::Modal),
61 title: Some(TextBlockObject {
62 type_filed: TextBlockType::PlainText,
63 text: "Slack Rust Example Modal".to_string(),
64 ..Default::default()
65 }),
66 submit: Some(TextBlockObject {
67 type_filed: TextBlockType::PlainText,
68 text: "Submit".to_string(),
69 ..Default::default()
70 }),
71 blocks: Some(vec![
72 Block::InputBlock(InputBlock {
73 label: TextBlockObject {
74 type_filed: TextBlockType::PlainText,
75 text: "Title".to_string(),
76 ..Default::default()
77 },
78 element: BlockElement::PlainTextInputBlockElement(
79 PlainTextInputBlockElement {
80 action_id: "title".to_string(),
81 placeholder: Some(TextBlockObject {
82 type_filed: TextBlockType::PlainText,
83 text: "What do you want to ask of the world?".to_string(),
84 ..Default::default()
85 }),
86 ..Default::default()
87 },
88 ),
89 ..Default::default()
90 }),
91 Block::InputBlock(InputBlock {
92 label: TextBlockObject {
93 type_filed: TextBlockType::PlainText,
94 text: "Channel(s)".to_string(),
95 ..Default::default()
96 },
97 element: BlockElement::MultiSelectBlockElement(MultiSelectBlockElement {
98 action_id: "title".to_string(),
99 placeholder: TextBlockObject {
100 type_filed: TextBlockType::PlainText,
101 text: "Where should the poll be sent?".to_string(),
102 ..Default::default()
103 },
104 options: vec![OptionBlockObject {
105 text: TextBlockObject {
106 type_filed: TextBlockType::PlainText,
107 text: "*this is plain_text text*".to_string(),
108 ..Default::default()
109 },
110 value: Some("value-0".to_string()),
111 ..Default::default()
112 }],
113 ..Default::default()
114 }),
115 ..Default::default()
116 }),
117 Block::ActionBlock(ActionBlock {
118 elements: vec![BlockElement::ButtonElement(ButtonElement {
119 action_id: "add_option".to_string(),
120 text: TextBlockObject {
121 type_filed: TextBlockType::PlainText,
122 text: "Add another option".to_string(),
123 ..Default::default()
124 },
125 ..Default::default()
126 })],
127 ..Default::default()
128 }),
129 ]),
130 ..Default::default()
131 },
132 };
133 let json = r##"{
134 "trigger_id": "12345.98765.abcd2358fdea",
135 "view": {
136 "type": "modal",
137 "blocks": [
138 {
139 "type": "input",
140 "label": {
141 "type": "plain_text",
142 "text": "Title"
143 },
144 "element": {
145 "type": "plain_text_input",
146 "action_id": "title",
147 "placeholder": {
148 "type": "plain_text",
149 "text": "What do you want to ask of the world?"
150 }
151 }
152 },
153 {
154 "type": "input",
155 "label": {
156 "type": "plain_text",
157 "text": "Channel(s)"
158 },
159 "element": {
160 "type": "multi_static_select",
161 "placeholder": {
162 "type": "plain_text",
163 "text": "Where should the poll be sent?"
164 },
165 "action_id": "title",
166 "options": [
167 {
168 "text": {
169 "type": "plain_text",
170 "text": "*this is plain_text text*"
171 },
172 "value": "value-0"
173 }
174 ]
175 }
176 },
177 {
178 "type": "actions",
179 "elements": [
180 {
181 "type": "button",
182 "text": {
183 "type": "plain_text",
184 "text": "Add another option"
185 },
186 "action_id": "add_option"
187 }
188 ]
189 }
190 ],
191 "title": {
192 "type": "plain_text",
193 "text": "Slack Rust Example Modal"
194 },
195 "submit": {
196 "type": "plain_text",
197 "text": "Submit"
198 }
199 }
200}"##;
201
202 let j = serde_json::to_string_pretty(&request).unwrap();
203 assert_eq!(json, j);
204
205 let s = serde_json::from_str::<PushRequest>(json).unwrap();
206 assert_eq!(request, s);
207 }
208
209 #[test]
210 fn convert_response() {
211 let response = PushResponse {
212 ok: true,
213 view: Some(View {
214 type_filed: Some(ViewType::Modal),
215 title: Some(TextBlockObject {
216 type_filed: TextBlockType::PlainText,
217 text: "Slack Rust Example Modal".to_string(),
218 ..Default::default()
219 }),
220 submit: Some(TextBlockObject {
221 type_filed: TextBlockType::PlainText,
222 text: "Submit".to_string(),
223 ..Default::default()
224 }),
225 blocks: Some(vec![
226 Block::InputBlock(InputBlock {
227 label: TextBlockObject {
228 type_filed: TextBlockType::PlainText,
229 text: "Title".to_string(),
230 ..Default::default()
231 },
232 element: BlockElement::PlainTextInputBlockElement(
233 PlainTextInputBlockElement {
234 action_id: "title".to_string(),
235 placeholder: Some(TextBlockObject {
236 type_filed: TextBlockType::PlainText,
237 text: "What do you want to ask of the world?".to_string(),
238 ..Default::default()
239 }),
240 ..Default::default()
241 },
242 ),
243 ..Default::default()
244 }),
245 Block::InputBlock(InputBlock {
246 label: TextBlockObject {
247 type_filed: TextBlockType::PlainText,
248 text: "Channel(s)".to_string(),
249 ..Default::default()
250 },
251 element: BlockElement::MultiSelectBlockElement(MultiSelectBlockElement {
252 action_id: "title".to_string(),
253 placeholder: TextBlockObject {
254 type_filed: TextBlockType::PlainText,
255 text: "Where should the poll be sent?".to_string(),
256 ..Default::default()
257 },
258 options: vec![OptionBlockObject {
259 text: TextBlockObject {
260 type_filed: TextBlockType::PlainText,
261 text: "*this is plain_text text*".to_string(),
262 ..Default::default()
263 },
264 value: Some("value-0".to_string()),
265 ..Default::default()
266 }],
267 ..Default::default()
268 }),
269 ..Default::default()
270 }),
271 Block::ActionBlock(ActionBlock {
272 elements: vec![BlockElement::ButtonElement(ButtonElement {
273 action_id: "add_option".to_string(),
274 text: TextBlockObject {
275 type_filed: TextBlockType::PlainText,
276 text: "Add another option".to_string(),
277 ..Default::default()
278 },
279 ..Default::default()
280 })],
281 ..Default::default()
282 }),
283 ]),
284 ..Default::default()
285 }),
286 ..Default::default()
287 };
288 let json = r##"{
289 "ok": true,
290 "view": {
291 "type": "modal",
292 "blocks": [
293 {
294 "type": "input",
295 "label": {
296 "type": "plain_text",
297 "text": "Title"
298 },
299 "element": {
300 "type": "plain_text_input",
301 "action_id": "title",
302 "placeholder": {
303 "type": "plain_text",
304 "text": "What do you want to ask of the world?"
305 }
306 }
307 },
308 {
309 "type": "input",
310 "label": {
311 "type": "plain_text",
312 "text": "Channel(s)"
313 },
314 "element": {
315 "type": "multi_static_select",
316 "placeholder": {
317 "type": "plain_text",
318 "text": "Where should the poll be sent?"
319 },
320 "action_id": "title",
321 "options": [
322 {
323 "text": {
324 "type": "plain_text",
325 "text": "*this is plain_text text*"
326 },
327 "value": "value-0"
328 }
329 ]
330 }
331 },
332 {
333 "type": "actions",
334 "elements": [
335 {
336 "type": "button",
337 "text": {
338 "type": "plain_text",
339 "text": "Add another option"
340 },
341 "action_id": "add_option"
342 }
343 ]
344 }
345 ],
346 "title": {
347 "type": "plain_text",
348 "text": "Slack Rust Example Modal"
349 },
350 "submit": {
351 "type": "plain_text",
352 "text": "Submit"
353 }
354 }
355}"##;
356
357 let j = serde_json::to_string_pretty(&response).unwrap();
358 assert_eq!(json, j);
359
360 let s = serde_json::from_str::<PushResponse>(json).unwrap();
361 assert_eq!(response, s);
362 }
363
364 #[async_std::test]
365 async fn test_push() {
366 let param = PushRequest {
367 trigger_id: "12345.98765.abcd2358fdea".to_string(),
368 view: View {
369 type_filed: Some(ViewType::Modal),
370 title: Some(TextBlockObject {
371 type_filed: TextBlockType::PlainText,
372 text: "Slack Rust Example Modal".to_string(),
373 ..Default::default()
374 }),
375 submit: Some(TextBlockObject {
376 type_filed: TextBlockType::PlainText,
377 text: "Submit".to_string(),
378 ..Default::default()
379 }),
380 blocks: Some(vec![
381 Block::InputBlock(InputBlock {
382 label: TextBlockObject {
383 type_filed: TextBlockType::PlainText,
384 text: "Title".to_string(),
385 ..Default::default()
386 },
387 element: BlockElement::PlainTextInputBlockElement(
388 PlainTextInputBlockElement {
389 action_id: "title".to_string(),
390 placeholder: Some(TextBlockObject {
391 type_filed: TextBlockType::PlainText,
392 text: "What do you want to ask of the world?".to_string(),
393 ..Default::default()
394 }),
395 ..Default::default()
396 },
397 ),
398 ..Default::default()
399 }),
400 Block::InputBlock(InputBlock {
401 label: TextBlockObject {
402 type_filed: TextBlockType::PlainText,
403 text: "Channel(s)".to_string(),
404 ..Default::default()
405 },
406 element: BlockElement::MultiSelectBlockElement(MultiSelectBlockElement {
407 action_id: "title".to_string(),
408 placeholder: TextBlockObject {
409 type_filed: TextBlockType::PlainText,
410 text: "Where should the poll be sent?".to_string(),
411 ..Default::default()
412 },
413 options: vec![OptionBlockObject {
414 text: TextBlockObject {
415 type_filed: TextBlockType::PlainText,
416 text: "*this is plain_text text*".to_string(),
417 ..Default::default()
418 },
419 value: Some("value-0".to_string()),
420 ..Default::default()
421 }],
422 ..Default::default()
423 }),
424 ..Default::default()
425 }),
426 Block::ActionBlock(ActionBlock {
427 elements: vec![BlockElement::ButtonElement(ButtonElement {
428 action_id: "add_option".to_string(),
429 text: TextBlockObject {
430 type_filed: TextBlockType::PlainText,
431 text: "Add another option".to_string(),
432 ..Default::default()
433 },
434 ..Default::default()
435 })],
436 ..Default::default()
437 }),
438 ]),
439 ..Default::default()
440 },
441 };
442 let mut mock = MockSlackWebAPIClient::new();
443 mock.expect_post_json().returning(|_, _, _| {
444 Ok(r##"{
445 "ok": true,
446 "view": {
447 "type": "modal",
448 "blocks": [
449 {
450 "type": "input",
451 "label": {
452 "type": "plain_text",
453 "text": "Title"
454 },
455 "element": {
456 "type": "plain_text_input",
457 "action_id": "title",
458 "placeholder": {
459 "type": "plain_text",
460 "text": "What do you want to ask of the world?"
461 }
462 }
463 },
464 {
465 "type": "input",
466 "label": {
467 "type": "plain_text",
468 "text": "Channel(s)"
469 },
470 "element": {
471 "type": "multi_static_select",
472 "placeholder": {
473 "type": "plain_text",
474 "text": "Where should the poll be sent?"
475 },
476 "action_id": "title",
477 "options": [
478 {
479 "text": {
480 "type": "plain_text",
481 "text": "*this is plain_text text*"
482 },
483 "value": "value-0"
484 }
485 ]
486 }
487 },
488 {
489 "type": "actions",
490 "elements": [
491 {
492 "type": "button",
493 "text": {
494 "type": "plain_text",
495 "text": "Add another option"
496 },
497 "action_id": "add_option"
498 }
499 ]
500 }
501 ],
502 "title": {
503 "type": "plain_text",
504 "text": "Slack Rust Example Modal"
505 },
506 "submit": {
507 "type": "plain_text",
508 "text": "Submit"
509 }
510 }
511}"##
512 .to_string())
513 });
514
515 let response = push(&mock, ¶m, &"test_token".to_string())
516 .await
517 .unwrap();
518 let expect = PushResponse {
519 ok: true,
520 view: Some(View {
521 type_filed: Some(ViewType::Modal),
522 title: Some(TextBlockObject {
523 type_filed: TextBlockType::PlainText,
524 text: "Slack Rust Example Modal".to_string(),
525 ..Default::default()
526 }),
527 submit: Some(TextBlockObject {
528 type_filed: TextBlockType::PlainText,
529 text: "Submit".to_string(),
530 ..Default::default()
531 }),
532 blocks: Some(vec![
533 Block::InputBlock(InputBlock {
534 label: TextBlockObject {
535 type_filed: TextBlockType::PlainText,
536 text: "Title".to_string(),
537 ..Default::default()
538 },
539 element: BlockElement::PlainTextInputBlockElement(
540 PlainTextInputBlockElement {
541 action_id: "title".to_string(),
542 placeholder: Some(TextBlockObject {
543 type_filed: TextBlockType::PlainText,
544 text: "What do you want to ask of the world?".to_string(),
545 ..Default::default()
546 }),
547 ..Default::default()
548 },
549 ),
550 ..Default::default()
551 }),
552 Block::InputBlock(InputBlock {
553 label: TextBlockObject {
554 type_filed: TextBlockType::PlainText,
555 text: "Channel(s)".to_string(),
556 ..Default::default()
557 },
558 element: BlockElement::MultiSelectBlockElement(MultiSelectBlockElement {
559 action_id: "title".to_string(),
560 placeholder: TextBlockObject {
561 type_filed: TextBlockType::PlainText,
562 text: "Where should the poll be sent?".to_string(),
563 ..Default::default()
564 },
565 options: vec![OptionBlockObject {
566 text: TextBlockObject {
567 type_filed: TextBlockType::PlainText,
568 text: "*this is plain_text text*".to_string(),
569 ..Default::default()
570 },
571 value: Some("value-0".to_string()),
572 ..Default::default()
573 }],
574 ..Default::default()
575 }),
576 ..Default::default()
577 }),
578 Block::ActionBlock(ActionBlock {
579 elements: vec![BlockElement::ButtonElement(ButtonElement {
580 action_id: "add_option".to_string(),
581 text: TextBlockObject {
582 type_filed: TextBlockType::PlainText,
583 text: "Add another option".to_string(),
584 ..Default::default()
585 },
586 ..Default::default()
587 })],
588 ..Default::default()
589 }),
590 ]),
591 ..Default::default()
592 }),
593 ..Default::default()
594 };
595
596 assert_eq!(expect, response);
597 }
598}