1use serde::{Deserialize, Serialize};
2
3/// https://api.slack.com/reference/messaging/payload
4#[derive(Default, Clone, Serialize, Deserialize)]
5pub struct WebhookBody {
6 text: String,
7 #[serde(skip_serializing_if = "Vec::is_empty")]
8 blocks: Vec<Block>,
9 #[serde(skip_serializing_if = "Option::is_none")]
10 thread_ts: Option<String>,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 mrkdwn: Option<bool>,
13}
14
15impl WebhookBody {
16 pub fn new(f: impl FnOnce(Self) -> Self) -> Self {
17 f(Self::default())
18 }
19
20 pub fn add_section(mut self, build: impl FnOnce(Section) -> Section) -> Self {
21 self.blocks.push(Block::Section(build(Section::default())));
22 self
23 }
24
25 pub fn add_rich_text(mut self, build: impl FnOnce(RichText) -> RichText) -> Self {
26 self.blocks
27 .push(Block::RichText(build(RichText::default())));
28 self
29 }
30}
31
32#[derive(Clone, Serialize, Deserialize)]
33#[serde(tag = "type")]
34/// https://api.slack.com/reference/block-kit/blocks
35pub enum Block {
36 #[serde(rename = "section")]
37 Section(Section),
38 #[serde(rename = "rich_text")]
39 RichText(RichText),
40 // .... etc.
41}
42
43/// https://api.slack.com/reference/block-kit/blocks#section
44#[derive(Default, Clone, Serialize, Deserialize)]
45pub struct Section {
46 #[serde(skip_serializing_if = "Option::is_none")]
47 text: Option<Text>,
48 #[serde(skip_serializing_if = "Vec::is_empty")]
49 fields: Vec<Text>,
50 // fields, accessories...
51}
52
53impl Section {
54 pub fn text(mut self, text: Text) -> Self {
55 self.text = Some(text);
56 self
57 }
58
59 pub fn add_field(mut self, field: Text) -> Self {
60 self.fields.push(field);
61 self
62 }
63}
64
65/// https://api.slack.com/reference/block-kit/composition-objects#text
66#[derive(Clone, Serialize, Deserialize)]
67#[serde(tag = "type")]
68pub enum Text {
69 #[serde(rename = "plain_text")]
70 PlainText { text: String, emoji: bool },
71 #[serde(rename = "mrkdwn")]
72 Markdown { text: String, verbatim: bool },
73}
74
75impl Text {
76 pub fn plain(s: String) -> Self {
77 Self::PlainText {
78 text: s,
79 emoji: true,
80 }
81 }
82
83 pub fn markdown(s: String) -> Self {
84 Self::Markdown {
85 text: s,
86 verbatim: false,
87 }
88 }
89}
90
91#[derive(Default, Clone, Serialize, Deserialize)]
92pub struct RichText {
93 elements: Vec<RichTextObject>,
94}
95
96impl RichText {
97 pub fn new(f: impl FnOnce(Self) -> Self) -> Self {
98 f(Self::default())
99 }
100
101 pub fn add_preformatted(
102 mut self,
103 build: impl FnOnce(RichTextPreformatted) -> RichTextPreformatted,
104 ) -> Self {
105 self.elements.push(RichTextObject::Preformatted(build(
106 RichTextPreformatted::default(),
107 )));
108 self
109 }
110}
111
112/// https://api.slack.com/reference/block-kit/blocks#rich_text
113#[derive(Clone, Serialize, Deserialize)]
114#[serde(tag = "type")]
115pub enum RichTextObject {
116 #[serde(rename = "rich_text_preformatted")]
117 Preformatted(RichTextPreformatted),
118 // etc.
119}
120
121/// https://api.slack.com/reference/block-kit/blocks#rich_text_preformatted
122#[derive(Clone, Default, Serialize, Deserialize)]
123pub struct RichTextPreformatted {
124 #[serde(skip_serializing_if = "Vec::is_empty")]
125 elements: Vec<RichTextElement>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 border: Option<u8>,
128}
129
130impl RichTextPreformatted {
131 pub fn add_text(mut self, text: String) -> Self {
132 self.elements.push(RichTextElement::Text { text });
133 self
134 }
135}
136
137/// https://api.slack.com/reference/block-kit/blocks#element-types
138#[derive(Clone, Serialize, Deserialize)]
139#[serde(tag = "type")]
140pub enum RichTextElement {
141 #[serde(rename = "text")]
142 Text { text: String },
143 // etc.
144}