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#[derive(Debug, Clone)]
11pub struct Slack {
12 hook: Url,
13 client: Client,
14}
15
16impl Slack {
17 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 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#[derive(Debug, Clone, PartialEq, PartialOrd)]
40pub struct SlackTime(NaiveDateTime);
41
42impl SlackTime {
43 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#[derive(Serialize, Debug, Default, Clone, PartialEq)]
61pub struct SlackText(String);
62
63impl SlackText {
64 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("&"),
71 '<' => s.push_str("<"),
72 '>' => s.push_str(">"),
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#[derive(Debug, Clone, PartialEq)]
101pub enum SlackTextContent {
102 Text(SlackText),
104 Link(SlackLink),
106 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#[derive(Debug, Clone, PartialEq)]
132pub struct SlackLink {
133 pub url: String,
138 pub text: SlackText,
140}
141
142impl SlackLink {
143 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#[derive(Debug, Clone, PartialEq, PartialOrd)]
172pub struct SlackUserLink {
173 pub uid: String,
175}
176
177impl SlackUserLink {
178 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 <&> 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 <&> 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 <&> 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 <&>","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())
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 <&> moo <@USER|M<E>> wow."
302 );
303 }
304}