slack_hook/
attachment.rs

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