slack_hooked/
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/// Actions are defined as an array, and values contained within it will
85/// be displayed with the message.
86#[derive(Serialize, Debug, Clone, PartialEq)]
87pub struct Action {
88    /// Action type, renamed to 'type'
89    #[serde(rename = "type")]
90    pub action_type: String,
91    /// Text for action
92    pub text: String,
93    /// Name of action
94    pub name: String,
95    /// Action style, ie: primary, danger, etc
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub style: Option<String>,
98    /// Value of action
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub value: Option<String>,
101}
102
103impl Action {
104    /// Construct a new field
105    pub fn new<S: Into<String>>(
106        action_type: S,
107        text: S,
108        name: S,
109        style: Option<String>,
110        value: Option<String>,
111    ) -> Action {
112        Action {
113            action_type: action_type.into(),
114            text: text.into(),
115            name: name.into(),
116            style,
117            value,
118        }
119    }
120}
121/// Fields are defined as an array, and hashes contained within it will
122/// be displayed in a table inside the message attachment.
123#[derive(Serialize, Debug, Clone, PartialEq)]
124pub struct Field {
125    /// Shown as a bold heading above the value text.
126    /// It cannot contain markup and will be escaped for you.
127    pub title: String,
128    /// The text value of the field. It may contain standard message markup
129    /// and must be escaped as normal. May be multi-line.
130    pub value: SlackText,
131    /// An optional flag indicating whether the value is short enough to be
132    /// displayed side-by-side with other values.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub short: Option<bool>,
135}
136
137impl Field {
138    /// Construct a new field
139    pub fn new<S: Into<String>, ST: Into<SlackText>>(
140        title: S,
141        value: ST,
142        short: Option<bool>,
143    ) -> Field {
144        Field {
145            title: title.into(),
146            value: value.into(),
147            short,
148        }
149    }
150}
151
152/// `AttachmentBuilder` is used to build a `Attachment`
153#[derive(Debug)]
154pub struct AttachmentBuilder {
155    inner: Result<Attachment>,
156}
157
158impl AttachmentBuilder {
159    /// Make a new `AttachmentBuilder`
160    ///
161    /// Fallback is the only required field which is a plain-text summary of the attachment.
162    pub fn new<S: Into<SlackText>>(fallback: S) -> AttachmentBuilder {
163        AttachmentBuilder {
164            inner: Ok(Attachment {
165                fallback: fallback.into(),
166                ..Default::default()
167            }),
168        }
169    }
170
171    /// Optional text that appears within the attachment
172    pub fn text<S: Into<SlackText>>(self, text: S) -> AttachmentBuilder {
173        match self.inner {
174            Ok(mut inner) => {
175                inner.text = Some(text.into());
176                AttachmentBuilder { inner: Ok(inner) }
177            }
178            _ => self,
179        }
180    }
181
182    /// Set the color of the attachment
183    ///
184    /// The color can be one of:
185    ///
186    /// 1. `String`s: `good`, `warning`, `danger`
187    /// 2. The built-in enums: `SlackColor::Good`, etc.
188    /// 3. Any valid hex color code: e.g. `#b13d41` or `#000`.
189    ///
190    /// hex color codes will be checked to ensure a valid hex number is provided
191    pub fn color<C: TryInto<HexColor, Error = Error>>(self, color: C) -> AttachmentBuilder {
192        match self.inner {
193            Ok(mut inner) => match color.try_into() {
194                Ok(c) => {
195                    inner.color = Some(c);
196                    AttachmentBuilder { inner: Ok(inner) }
197                }
198                Err(e) => AttachmentBuilder { inner: Err(e) },
199            },
200            _ => self,
201        }
202    }
203
204    /// Optional text that appears above the attachment block
205    pub fn pretext<S: Into<SlackText>>(self, pretext: S) -> AttachmentBuilder {
206        match self.inner {
207            Ok(mut inner) => {
208                inner.pretext = Some(pretext.into());
209                AttachmentBuilder { inner: Ok(inner) }
210            }
211            _ => self,
212        }
213    }
214    /// Actions are defined as an array, and hashes contained within it will be
215    /// displayed in a table inside the message attachment.
216    pub fn actions(self, actions: Vec<Action>) -> AttachmentBuilder {
217        match self.inner {
218            Ok(mut inner) => {
219                inner.actions = Some(actions);
220                AttachmentBuilder { inner: Ok(inner) }
221            }
222            _ => self,
223        }
224    }
225    /// Fields are defined as an array, and hashes contained within it will be
226    /// displayed in a table inside the message attachment.
227    pub fn fields(self, fields: Vec<Field>) -> AttachmentBuilder {
228        match self.inner {
229            Ok(mut inner) => {
230                inner.fields = Some(fields);
231                AttachmentBuilder { inner: Ok(inner) }
232            }
233            _ => self,
234        }
235    }
236    /// Optional small text used to display the author's name.
237    pub fn author_name<S: Into<SlackText>>(self, author_name: S) -> AttachmentBuilder {
238        match self.inner {
239            Ok(mut inner) => {
240                inner.author_name = Some(author_name.into());
241                AttachmentBuilder { inner: Ok(inner) }
242            }
243            _ => self,
244        }
245    }
246
247    url_builder_fn! {
248        /// Optional URL that will hyperlink the `author_name`.
249        author_link, AttachmentBuilder
250    }
251
252    url_builder_fn! {
253        /// Optional URL that displays a small 16x16px image to the left of the `author_name` text.
254        author_icon, AttachmentBuilder
255    }
256
257    /// Optional larger, bolder text above the main body
258    pub fn title<S: Into<SlackText>>(self, title: S) -> AttachmentBuilder {
259        match self.inner {
260            Ok(mut inner) => {
261                inner.title = Some(title.into());
262                AttachmentBuilder { inner: Ok(inner) }
263            }
264            _ => self,
265        }
266    }
267
268    /// Optional larger, bolder text above the main body
269    pub fn callback_id<S: Into<SlackText>>(self, callback_id: S) -> AttachmentBuilder {
270        match self.inner {
271            Ok(mut inner) => {
272                inner.callback_id = Some(callback_id.into());
273                AttachmentBuilder { inner: Ok(inner) }
274            }
275            _ => self,
276        }
277    }
278
279    url_builder_fn! {
280        /// Optional URL to link to from the title
281        title_link, AttachmentBuilder
282    }
283
284    url_builder_fn! {
285        /// Optional URL to an image that will be displayed in the body
286        image_url, AttachmentBuilder
287    }
288
289    url_builder_fn! {
290        /// Optional URL to an image that will be displayed as a thumbnail to the right of the body
291        thumb_url, AttachmentBuilder
292    }
293
294    /// Optional text that will appear at the bottom of the attachment
295    pub fn footer<S: Into<SlackText>>(self, footer: S) -> AttachmentBuilder {
296        match self.inner {
297            Ok(mut inner) => {
298                inner.footer = Some(footer.into());
299                AttachmentBuilder { inner: Ok(inner) }
300            }
301            _ => self,
302        }
303    }
304
305    url_builder_fn! {
306        /// Optional URL to an image that will be displayed at the bottom of the attachment
307        footer_icon, AttachmentBuilder
308    }
309
310    /// Optional timestamp to be displayed with the attachment
311    pub fn ts(self, time: &NaiveDateTime) -> AttachmentBuilder {
312        match self.inner {
313            Ok(mut inner) => {
314                inner.ts = Some(SlackTime::new(time));
315                AttachmentBuilder { inner: Ok(inner) }
316            }
317            _ => self,
318        }
319    }
320
321    /// Optional sections formatted as markdown.
322    pub fn markdown_in<'a, I: IntoIterator<Item = &'a Section>>(
323        self,
324        sections: I,
325    ) -> AttachmentBuilder {
326        match self.inner {
327            Ok(mut inner) => {
328                inner.mrkdwn_in = Some(sections.into_iter().cloned().collect());
329                AttachmentBuilder { inner: Ok(inner) }
330            }
331            _ => self,
332        }
333    }
334
335    /// Attempt to build the `Attachment`
336    pub fn build(self) -> Result<Attachment> {
337        // set text to equal fallback if text wasn't specified
338        match self.inner {
339            Ok(mut inner) => {
340                if inner.text.is_none() {
341                    inner.text = Some(inner.fallback.clone())
342                }
343                Ok(inner)
344            }
345            _ => self.inner,
346        }
347    }
348}