slack_hook2/
attachment.rs

1use chrono::NaiveDateTime;
2use crate::error::Result;
3use url::Url;
4use crate::{HexColor, SlackText, SlackTime};
5
6/// Slack allows for attachments to be added to messages. See
7/// https://api.slack.com/docs/attachments for more information.
8#[derive(Serialize, Debug, Default, Clone, PartialEq)]
9pub struct Attachment {
10    /// Required text for attachment.
11    /// Slack will use this text to display on devices that don't support markup.
12    pub fallback: SlackText,
13    /// Optional text for other devices, markup supported
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub text: Option<SlackText>,
16    /// Optional text that appears above attachment
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub pretext: Option<SlackText>,
19    /// Optional color of attachment
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub color: Option<HexColor>,
22    /// Actions as array
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub actions: Option<Vec<Action>>,
25    /// Fields are defined as an array, and hashes contained within it will be
26    /// displayed in a table inside the message attachment.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub fields: Option<Vec<Field>>,
29    /// Optional small text used to display the author's name.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub author_name: Option<SlackText>,
32    /// Optional URL that will hyperlink the `author_name` text mentioned above. Will only
33    /// work if `author_name` is present.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub author_link: Option<Url>,
36    /// Optional URL that displays a small 16x16px image to the left of
37    /// the `author_name` text. Will only work if `author_name` is present.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub author_icon: Option<Url>,
40    /// Optional larger, bolder text above the main body
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub title: Option<SlackText>,
43    /// Optional URL to link to from the title
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub title_link: Option<Url>,
46    /// Optional URL to an image that will be displayed in the body
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub image_url: Option<Url>,
49    /// Optional URL to an image that will be displayed as a thumbnail to the
50    /// right of the body
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub thumb_url: Option<Url>,
53    /// Optional text that will appear at the bottom of the attachment
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub footer: Option<SlackText>,
56    /// Optional URL to an image that will be displayed at the bottom of the
57    /// attachment
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub footer_icon: Option<Url>,
60    /// Optional timestamp to be displayed with the attachment
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub ts: Option<SlackTime>,
63    /// Optional sections formatted as markdown.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub mrkdwn_in: Option<Vec<Section>>,
66    /// Optional callback_id for actions
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub callback_id: Option<SlackText>,
69}
70
71/// Sections define parts of an attachment.
72#[derive(Eq, PartialEq, Copy, Clone, Serialize, Debug)]
73#[serde(rename_all = "lowercase")]
74pub enum Section {
75    /// The pretext section.
76    Pretext,
77    /// The text section.
78    Text,
79    /// The fields.
80    Fields,
81}
82/// Actions are defined as an array, and values contained within it will
83/// be displayed with the message.
84#[derive(Serialize, Debug, Clone, PartialEq)]
85pub struct Action {
86    /// Action type, renamed to 'type'
87    #[serde(rename = "type")]
88    pub action_type: String,
89    /// Text for action
90    pub text: String,
91    /// Name of action
92    pub name: String,
93    /// Action style, ie: primary, danger, etc
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub style: Option<String>,
96    /// Value of action
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub value: Option<String>
99}
100
101impl Action {
102    /// Construct a new field
103    pub fn new<S: Into<String>>(
104        action_type: S,
105        text: S,
106        name: S,
107        style: Option<String>,
108        value: Option<String>
109
110    ) -> Action {
111        Action {
112            action_type: action_type.into(),
113            text: text.into(),
114            name: name.into(),
115            style,
116            value
117        }
118    }
119}
120/// Fields are defined as an array, and hashes contained within it will
121/// be displayed in a table inside the message attachment.
122#[derive(Serialize, Debug, Clone, PartialEq)]
123pub struct Field {
124    /// Shown as a bold heading above the value text.
125    /// It cannot contain markup and will be escaped for you.
126    pub title: String,
127    /// The text value of the field. It may contain standard message markup
128    /// and must be escaped as normal. May be multi-line.
129    pub value: SlackText,
130    /// An optional flag indicating whether the value is short enough to be
131    /// displayed side-by-side with other values.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub short: Option<bool>,
134}
135
136impl Field {
137    /// Construct a new field
138    pub fn new<S: Into<String>, ST: Into<SlackText>>(
139        title: S,
140        value: ST,
141        short: Option<bool>,
142    ) -> Field {
143        Field {
144            title: title.into(),
145            value: value.into(),
146            short,
147        }
148    }
149}
150
151/// `AttachmentBuilder` is used to build a `Attachment`
152#[derive(Debug)]
153pub struct AttachmentBuilder {
154    inner: Result<Attachment>,
155}
156
157impl AttachmentBuilder {
158    /// Make a new `AttachmentBuilder`
159    ///
160    /// Fallback is the only required field which is a plain-text summary of the attachment.
161    pub fn new<S: Into<SlackText>>(fallback: S) -> AttachmentBuilder {
162        AttachmentBuilder {
163            inner: Ok(Attachment {
164                fallback: fallback.into(),
165                ..Default::default()
166            }),
167        }
168    }
169
170    /// Optional text that appears within the attachment
171    pub fn text<S: Into<SlackText>>(self, text: S) -> AttachmentBuilder {
172        match self.inner {
173            Ok(mut inner) => {
174                inner.text = Some(text.into());
175                AttachmentBuilder { inner: Ok(inner) }
176            }
177            _ => self,
178        }
179    }
180
181    /// Set the color of the attachment
182    ///
183    /// The color can be one of:
184    ///
185    /// 1. `String`s: `good`, `warning`, `danger`
186    /// 2. The built-in enums: `SlackColor::Good`, etc.
187    /// 3. Any valid hex color code: e.g. `#b13d41` or `#000`.
188    ///
189    /// hex color codes will be checked to ensure a valid hex number is provided
190    pub fn color<S: Into<String>>(self, color: S) -> AttachmentBuilder {
191        match self.inner {
192            Ok(mut inner) => match HexColor::new_checked(color) {
193                Ok(c) => {
194                    inner.color = Some(c);
195                    AttachmentBuilder { inner: Ok(inner) }
196                }
197                Err(e) => AttachmentBuilder { inner: Err(e) },
198            },
199            _ => self,
200        }
201    }
202
203    /// Optional text that appears above the attachment block
204    pub fn pretext<S: Into<SlackText>>(self, pretext: S) -> AttachmentBuilder {
205        match self.inner {
206            Ok(mut inner) => {
207                inner.pretext = Some(pretext.into());
208                AttachmentBuilder { inner: Ok(inner) }
209            }
210            _ => self,
211        }
212    }
213    /// Actions are defined as an array, and hashes contained within it will be
214    /// displayed in a table inside the message attachment.
215    pub fn actions(self, actions: Vec<Action>) -> AttachmentBuilder {
216        match self.inner {
217            Ok(mut inner) => {
218                inner.actions = Some(actions);
219                AttachmentBuilder { inner: Ok(inner) }
220            }
221            _ => self,
222        }
223    }
224    /// Fields are defined as an array, and hashes contained within it will be
225    /// displayed in a table inside the message attachment.
226    pub fn fields(self, fields: Vec<Field>) -> AttachmentBuilder {
227        match self.inner {
228            Ok(mut inner) => {
229                inner.fields = Some(fields);
230                AttachmentBuilder { inner: Ok(inner) }
231            }
232            _ => self,
233        }
234    }
235    /// Optional small text used to display the author's name.
236    pub fn author_name<S: Into<SlackText>>(self, author_name: S) -> AttachmentBuilder {
237        match self.inner {
238            Ok(mut inner) => {
239                inner.author_name = Some(author_name.into());
240                AttachmentBuilder { inner: Ok(inner) }
241            }
242            _ => self,
243        }
244    }
245
246    url_builder_fn! {
247        /// Optional URL that will hyperlink the `author_name`.
248        author_link, AttachmentBuilder
249    }
250
251    url_builder_fn! {
252        /// Optional URL that displays a small 16x16px image to the left of the `author_name` text.
253        author_icon, AttachmentBuilder
254    }
255
256    /// Optional larger, bolder text above the main body
257    pub fn title<S: Into<SlackText>>(self, title: S) -> AttachmentBuilder {
258        match self.inner {
259            Ok(mut inner) => {
260                inner.title = Some(title.into());
261                AttachmentBuilder { inner: Ok(inner) }
262            }
263            _ => self,
264        }
265    }
266
267    /// Optional larger, bolder text above the main body
268    pub fn callback_id<S: Into<SlackText>>(self, callback_id: S) -> AttachmentBuilder {
269        match self.inner {
270            Ok(mut inner) => {
271                inner.callback_id = Some(callback_id.into());
272                AttachmentBuilder { inner: Ok(inner) }
273            }
274            _ => self,
275        }
276    }
277
278    url_builder_fn! {
279        /// Optional URL to link to from the title
280        title_link, AttachmentBuilder
281    }
282
283    url_builder_fn! {
284        /// Optional URL to an image that will be displayed in the body
285        image_url, AttachmentBuilder
286    }
287
288    url_builder_fn! {
289        /// Optional URL to an image that will be displayed as a thumbnail to the right of the body
290        thumb_url, AttachmentBuilder
291    }
292
293    /// Optional text that will appear at the bottom of the attachment
294    pub fn footer<S: Into<SlackText>>(self, footer: S) -> AttachmentBuilder {
295        match self.inner {
296            Ok(mut inner) => {
297                inner.footer = Some(footer.into());
298                AttachmentBuilder { inner: Ok(inner) }
299            }
300            _ => self,
301        }
302    }
303
304    url_builder_fn! {
305        /// Optional URL to an image that will be displayed at the bottom of the attachment
306        footer_icon, AttachmentBuilder
307    }
308
309    /// Optional timestamp to be displayed with the attachment
310    pub fn ts(self, time: &NaiveDateTime) -> AttachmentBuilder {
311        match self.inner {
312            Ok(mut inner) => {
313                inner.ts = Some(SlackTime::new(time));
314                AttachmentBuilder { inner: Ok(inner) }
315            }
316            _ => self,
317        }
318    }
319
320    /// Optional sections formatted as markdown.
321    pub fn markdown_in<'a, I: IntoIterator<Item = &'a Section>>(
322        self,
323        sections: I,
324    ) -> AttachmentBuilder {
325        match self.inner {
326            Ok(mut inner) => {
327                inner.mrkdwn_in = Some(sections.into_iter().cloned().collect());
328                AttachmentBuilder { inner: Ok(inner) }
329            }
330            _ => self,
331        }
332    }
333
334    /// Attempt to build the `Attachment`
335    pub fn build(self) -> Result<Attachment> {
336        // set text to equal fallback if text wasn't specified
337        match self.inner {
338            Ok(mut inner) => {
339                if inner.text.is_none() {
340                    inner.text = Some(inner.fallback.clone())
341                }
342                Ok(inner)
343            }
344            _ => self.inner,
345        }
346    }
347}