slack_hook3/
slack.rs

1use chrono::NaiveDateTime;
2use crate::error::{SlackError, Result};
3use reqwest::Client;
4use url::Url;
5use serde::{Serialize, Serializer};
6use std::fmt;
7use crate::Payload;
8
9/// Handles sending messages to slack
10#[derive(Debug, Clone)]
11pub struct Slack {
12    hook: Url,
13    client: Client,
14}
15
16impl Slack {
17    /// Construct a new instance of slack for a specific incoming url endpoint.
18    pub fn new<U: reqwest::IntoUrl>(hook: U) -> Result<Slack> {
19        Ok(Slack {
20            hook: hook.into_url().map_err(|e| SlackError::Http(format!("failed to convert to reqwest::url: {}", e)))?,
21            client: Client::new(),
22        })
23    }
24
25    /// Send payload to slack service
26    pub async fn send(&self, payload: &Payload) -> Result<()> {
27        let response = self.client.post(self.hook.clone()).json(payload).send().await
28            .map_err(|e| SlackError::Http(format!("HTTP send error to {}: {}", self.hook, e)))?;
29
30        if response.status().is_success(){
31            Ok(())
32        } else {
33            Err(SlackError::Http(format!("HTTP error {}", response.status())))
34        }
35    }
36}
37
38/// Slack timestamp
39#[derive(Debug, Clone, PartialEq, PartialOrd)]
40pub struct SlackTime(NaiveDateTime);
41
42impl SlackTime {
43    /// Construct a new `SlackTime`
44    pub fn new(time: &NaiveDateTime) -> SlackTime {
45        SlackTime(*time)
46    }
47}
48
49impl Serialize for SlackTime {
50    fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
51    where
52        S: Serializer,
53    {
54        serializer.serialize_i64(self.0.timestamp())
55    }
56}
57
58/// Representation of any text sent through slack
59/// the text must be processed to escape specific characters
60#[derive(Serialize, Debug, Default, Clone, PartialEq)]
61pub struct SlackText(String);
62
63impl SlackText {
64    /// Construct slack text with escaping
65    /// Escape &, <, and > in any slack text
66    /// https://api.slack.com/docs/formatting
67    pub fn new<S: Into<String>>(text: S) -> SlackText {
68        let s = text.into().chars().fold(String::new(), |mut s, c| {
69            match c {
70                '&' => s.push_str("&amp;"),
71                '<' => s.push_str("&lt;"),
72                '>' => s.push_str("&gt;"),
73                _ => s.push(c),
74            }
75            s
76        });
77        SlackText(s)
78    }
79
80    fn new_raw<S: Into<String>>(text: S) -> SlackText {
81        SlackText(text.into())
82    }
83}
84
85impl<'a> From<&'a str> for SlackText {
86    fn from(s: &'a str) -> SlackText {
87        SlackText::new(String::from(s))
88    }
89}
90
91impl<'a> From<String> for SlackText {
92    fn from(s: String) -> SlackText {
93        SlackText::new(s)
94    }
95}
96
97/// Enum used for constructing a text field having both `SlackText`(s) and `SlackLink`(s). The
98/// variants should be used together in a `Vec` on any function having a `Into<SlackText>` trait
99/// bound. The combined text will be space-separated.
100#[derive(Debug, Clone, PartialEq)]
101pub enum SlackTextContent {
102    /// Text that will be escaped via slack api rules
103    Text(SlackText),
104    /// Link
105    Link(SlackLink),
106    /// User Link
107    User(SlackUserLink),
108}
109
110impl<'a> From<&'a [SlackTextContent]> for SlackText {
111    fn from(v: &[SlackTextContent]) -> SlackText {
112        let st = v
113            .iter()
114            .map(|item| match *item {
115                SlackTextContent::Text(ref s) => format!("{}", s),
116                SlackTextContent::Link(ref link) => format!("{}", link),
117                SlackTextContent::User(ref u) => format!("{}", u),
118            }).collect::<Vec<String>>()
119            .join(" ");
120        SlackText::new_raw(st)
121    }
122}
123
124impl fmt::Display for SlackText {
125    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126        write!(f, "{}", self.0)
127    }
128}
129
130/// Representation of a link sent in slack
131#[derive(Debug, Clone, PartialEq)]
132pub struct SlackLink {
133    /// URL for link.
134    ///
135    /// NOTE: this is NOT a `Url` type because some of the slack "urls", don't conform to standard
136    /// url parsing scheme, which are enforced by the `url` crate.
137    pub url: String,
138    /// Anchor text for link
139    pub text: SlackText,
140}
141
142impl SlackLink {
143    /// Construct new SlackLink with string slices
144    pub fn new(url: &str, text: &str) -> SlackLink {
145        SlackLink {
146            url: url.to_owned(),
147            text: SlackText::new(text),
148        }
149    }
150}
151
152impl fmt::Display for SlackLink {
153    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
154        write!(f, "<{}|{}>", self.url, self.text)
155    }
156}
157
158impl Serialize for SlackLink {
159    fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
160    where
161        S: Serializer,
162    {
163        serializer.serialize_str(&format!("{}", self)[..])
164    }
165}
166
167/// Representation of a user id link sent in slack
168///
169/// Cannot do @UGUID|handle links using SlackLink in the future due to
170/// https://api.slack.com/changelog/2017-09-the-one-about-usernames
171#[derive(Debug, Clone, PartialEq, PartialOrd)]
172pub struct SlackUserLink {
173    /// User ID (U1231232123) style
174    pub uid: String,
175}
176
177impl SlackUserLink {
178    /// Construct new `SlackUserLink` with a string slice
179    pub fn new(uid: &str) -> SlackUserLink {
180        SlackUserLink {
181            uid: uid.to_owned(),
182        }
183    }
184}
185
186impl fmt::Display for SlackUserLink {
187    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
188        write!(f, "<{}>", self.uid)
189    }
190}
191
192impl Serialize for SlackUserLink {
193    fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
194    where
195        S: Serializer,
196    {
197        serializer.serialize_str(&format!("{}", self)[..])
198    }
199}
200
201#[cfg(test)]
202mod test {
203    use chrono::NaiveDateTime;
204    use crate::slack::{Slack, SlackLink};
205    #[cfg(feature = "unstable")]
206    use test::Bencher;
207    use serde_json;
208    use crate::{AttachmentBuilder, Field, Parse, PayloadBuilder, SlackText};
209
210    #[test]
211    fn slack_incoming_url_test() {
212        let s = Slack::new("https://hooks.slack.com/services/abc/123/45z").unwrap();
213        assert_eq!(
214            s.hook.to_string(),
215            "https://hooks.slack.com/services/abc/123/45z".to_owned()
216        );
217    }
218
219    #[test]
220    fn slack_text_test() {
221        let s = SlackText::new("moo <&> moo");
222        assert_eq!(format!("{}", s), "moo &lt;&amp;&gt; moo".to_owned());
223    }
224
225    #[test]
226    fn slack_link_test() {
227        let s = SlackLink {
228            text: SlackText::new("moo <&> moo"),
229            url: "http://google.com".to_owned(),
230        };
231        assert_eq!(
232            format!("{}", s),
233            "<http://google.com|moo &lt;&amp;&gt; moo>".to_owned()
234        );
235    }
236
237    #[test]
238    fn json_slacklink_test() {
239        let s = SlackLink {
240            text: SlackText::new("moo <&> moo"),
241            url: "http://google.com".to_owned(),
242        };
243        assert_eq!(
244            serde_json::to_string(&s).unwrap().to_owned(),
245            "\"<http://google.com|moo &lt;&amp;&gt; moo>\"".to_owned()
246        )
247    }
248
249    #[test]
250    fn json_complete_payload_test() {
251        let a = vec![
252            AttachmentBuilder::new("fallback <&>")
253                .text("text <&>")
254                .color("#6800e8")
255                .fields(vec![Field::new("title", "value", None)])
256                .title_link("https://title_link.com/")
257                .ts(&NaiveDateTime::from_timestamp(123_456_789, 0))
258                .build()
259                .unwrap(),
260        ];
261
262        let p = PayloadBuilder::new()
263            .text("test message")
264            .channel("#abc")
265            .username("Bot")
266            .icon_emoji(":chart_with_upwards_trend:")
267            .icon_url("https://example.com")
268            .attachments(a)
269            .unfurl_links(false)
270            .link_names(true)
271            .parse(Parse::Full)
272            .build()
273            .unwrap();
274
275        assert_eq!(serde_json::to_string(&p).unwrap().to_owned(),
276            r##"{"text":"test message","channel":"#abc","username":"Bot","icon_url":"https://example.com/","icon_emoji":":chart_with_upwards_trend:","attachments":[{"fallback":"fallback &lt;&amp;&gt;","text":"text &lt;&amp;&gt;","color":"#6800e8","fields":[{"title":"title","value":"value"}],"title_link":"https://title_link.com/","ts":123456789}],"unfurl_links":false,"link_names":1,"parse":"full"}"##.to_owned())
277    }
278
279    #[test]
280    fn json_message_payload_test() {
281        let p = PayloadBuilder::new().text("test message").build().unwrap();
282
283        assert_eq!(
284            serde_json::to_string(&p).unwrap().to_owned(),
285            r##"{"text":"test message"}"##.to_owned()
286        )
287    }
288
289    #[test]
290    fn slack_text_content_test() {
291        use super::SlackTextContent;
292        use super::SlackTextContent::{Link, Text};
293        let message: Vec<SlackTextContent> = vec![
294            Text("moo <&> moo".into()),
295            Link(SlackLink::new("@USER", "M<E>")),
296            Text("wow.".into()),
297        ];
298        let st: SlackText = SlackText::from(&message[..]);
299        assert_eq!(
300            format!("{}", st),
301            "moo &lt;&amp;&gt; moo <@USER|M&lt;E&gt;> wow."
302        );
303    }
304}