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