1use crate::error::{ErrorKind, Result};
2use crate::Payload;
3use chrono::NaiveDateTime;
4use reqwest::{blocking::Client, Url};
5use serde::{Serialize, Serializer};
6use std::fmt;
7
8#[derive(Debug, Clone)]
10pub struct Slack {
11 hook: Url,
12 client: Client,
13}
14
15impl Slack {
16 pub fn new<T: reqwest::IntoUrl>(hook: T) -> Result<Slack> {
18 Ok(Slack {
19 hook: hook.into_url()?,
20 client: Client::new(),
21 })
22 }
23
24 pub fn send(&self, payload: &Payload) -> Result<()> {
26 let response = self.client.post(self.hook.clone()).json(payload).send()?;
27
28 if response.status().is_success() {
29 Ok(())
30 } else {
31 Err(ErrorKind::Slack(format!("HTTP error {}", response.status())).into())
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, PartialOrd)]
38pub struct SlackTime(NaiveDateTime);
39
40impl SlackTime {
41 pub fn new(time: &NaiveDateTime) -> SlackTime {
43 SlackTime(*time)
44 }
45}
46
47impl Serialize for SlackTime {
48 fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
49 where
50 S: Serializer,
51 {
52 serializer.serialize_i64(self.0.timestamp())
53 }
54}
55
56#[derive(Serialize, Debug, Default, Clone, PartialEq)]
59pub struct SlackText(String);
60
61impl SlackText {
62 pub fn new<S: Into<String>>(text: S) -> SlackText {
66 let s = text.into().chars().fold(String::new(), |mut s, c| {
67 match c {
68 '&' => s.push_str("&"),
69 '<' => s.push_str("<"),
70 '>' => s.push_str(">"),
71 _ => s.push(c),
72 }
73 s
74 });
75 SlackText(s)
76 }
77
78 fn new_raw<S: Into<String>>(text: S) -> SlackText {
79 SlackText(text.into())
80 }
81}
82
83impl<'a> From<&'a str> for SlackText {
84 fn from(s: &'a str) -> SlackText {
85 SlackText::new(String::from(s))
86 }
87}
88
89impl<'a> From<String> for SlackText {
90 fn from(s: String) -> SlackText {
91 SlackText::new(s)
92 }
93}
94
95#[derive(Debug, Clone, PartialEq)]
99pub enum SlackTextContent {
100 Text(SlackText),
102 Link(SlackLink),
104 User(SlackUserLink),
106}
107
108impl<'a> From<&'a [SlackTextContent]> for SlackText {
109 fn from(v: &[SlackTextContent]) -> SlackText {
110 let st = v
111 .iter()
112 .map(|item| match *item {
113 SlackTextContent::Text(ref s) => format!("{}", s),
114 SlackTextContent::Link(ref link) => format!("{}", link),
115 SlackTextContent::User(ref u) => format!("{}", u),
116 })
117 .collect::<Vec<String>>()
118 .join(" ");
119 SlackText::new_raw(st)
120 }
121}
122
123impl fmt::Display for SlackText {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 write!(f, "{}", self.0)
126 }
127}
128
129#[derive(Debug, Clone, PartialEq)]
131pub struct SlackLink {
132 pub url: String,
137 pub text: SlackText,
139}
140
141impl SlackLink {
142 pub fn new(url: &str, text: &str) -> SlackLink {
144 SlackLink {
145 url: url.to_owned(),
146 text: SlackText::new(text),
147 }
148 }
149}
150
151impl fmt::Display for SlackLink {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 write!(f, "<{}|{}>", self.url, self.text)
154 }
155}
156
157impl Serialize for SlackLink {
158 fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
159 where
160 S: Serializer,
161 {
162 serializer.serialize_str(&format!("{}", self)[..])
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, PartialOrd)]
171pub struct SlackUserLink {
172 pub uid: String,
174}
175
176impl SlackUserLink {
177 pub fn new(uid: &str) -> SlackUserLink {
179 SlackUserLink {
180 uid: uid.to_owned(),
181 }
182 }
183}
184
185impl fmt::Display for SlackUserLink {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 write!(f, "<{}>", self.uid)
188 }
189}
190
191impl Serialize for SlackUserLink {
192 fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
193 where
194 S: Serializer,
195 {
196 serializer.serialize_str(&format!("{}", self)[..])
197 }
198}
199
200#[cfg(test)]
201mod test {
202 use crate::slack::{Slack, SlackLink};
203 use crate::{AttachmentBuilder, Field, Parse, PayloadBuilder, SlackText};
204 use chrono::NaiveDateTime;
205 #[cfg(feature = "unstable")]
206 use test::Bencher;
207
208 #[test]
209 fn slack_incoming_url_test() {
210 let s = Slack::new("https://hooks.slack.com/services/abc/123/45z").unwrap();
211 assert_eq!(
212 s.hook.to_string(),
213 "https://hooks.slack.com/services/abc/123/45z".to_owned()
214 );
215 }
216
217 #[test]
218 fn slack_text_test() {
219 let s = SlackText::new("moo <&> moo");
220 assert_eq!(format!("{}", s), "moo <&> moo".to_owned());
221 }
222
223 #[test]
224 fn slack_link_test() {
225 let s = SlackLink {
226 text: SlackText::new("moo <&> moo"),
227 url: "http://google.com".to_owned(),
228 };
229 assert_eq!(
230 format!("{}", s),
231 "<http://google.com|moo <&> moo>".to_owned()
232 );
233 }
234
235 #[test]
236 fn json_slacklink_test() {
237 let s = SlackLink {
238 text: SlackText::new("moo <&> moo"),
239 url: "http://google.com".to_owned(),
240 };
241 assert_eq!(
242 serde_json::to_string(&s).unwrap(),
243 "\"<http://google.com|moo <&> moo>\"".to_owned()
244 )
245 }
246
247 #[test]
248 fn json_complete_payload_test() {
249 let a = vec![AttachmentBuilder::new("fallback <&>")
250 .text("text <&>")
251 .color("#6800e8")
252 .fields(vec![Field::new("title", "value", None)])
253 .title_link("https://title_link.com/")
254 .ts(&NaiveDateTime::from_timestamp(123_456_789, 0))
255 .build()
256 .unwrap()];
257
258 let p = PayloadBuilder::new()
259 .text("test message")
260 .channel("#abc")
261 .username("Bot")
262 .icon_emoji(":chart_with_upwards_trend:")
263 .icon_url("https://example.com")
264 .attachments(a)
265 .unfurl_links(false)
266 .link_names(true)
267 .parse(Parse::Full)
268 .build()
269 .unwrap();
270
271 assert_eq!(serde_json::to_string(&p).unwrap(),
272 r##"{"text":"test message","channel":"#abc","username":"Bot","icon_url":"https://example.com/","icon_emoji":":chart_with_upwards_trend:","attachments":[{"fallback":"fallback <&>","text":"text <&>","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())
273 }
274
275 #[test]
276 fn json_message_payload_test() {
277 let p = PayloadBuilder::new().text("test message").build().unwrap();
278
279 assert_eq!(
280 serde_json::to_string(&p).unwrap(),
281 r##"{"text":"test message"}"##.to_owned()
282 )
283 }
284
285 #[test]
286 fn slack_text_content_test() {
287 use super::SlackTextContent;
288 use super::SlackTextContent::{Link, Text};
289 let message: Vec<SlackTextContent> = vec![
290 Text("moo <&> moo".into()),
291 Link(SlackLink::new("@USER", "M<E>")),
292 Text("wow.".into()),
293 ];
294 let st: SlackText = SlackText::from(&message[..]);
295 assert_eq!(
296 format!("{}", st),
297 "moo <&> moo <@USER|M<E>> wow."
298 );
299 }
300}