1use crate::{Error, Payload, Result};
2use chrono::NaiveDateTime;
3use reqwest::{Client, Url};
4use serde::{Serialize, Serializer};
5use std::fmt;
6
7#[derive(Debug, Clone)]
9pub struct Slack {
10 hook: Url,
11 client: Client,
12}
13
14impl Slack {
15 pub fn new<T: reqwest::IntoUrl>(hook: T) -> Result<Slack> {
17 Self::new_with_client(hook, Client::new())
18 }
19
20 pub fn new_with_client<T: reqwest::IntoUrl>(hook: T, client: Client) -> Result<Self> {
24 let hook = hook.into_url()?;
25 Ok(Self { hook, client })
26 }
27
28 pub async fn send(&self, payload: &Payload) -> Result<()> {
30 let response = self
31 .client
32 .post(self.hook.clone())
33 .json(payload)
34 .send()
35 .await?;
36
37 if response.status().is_success() {
38 Ok(())
39 } else {
40 Err(Error::Slack(format!("HTTP error {}", response.status())))
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, PartialOrd)]
47pub struct SlackTime(NaiveDateTime);
48
49impl SlackTime {
50 pub fn new(time: &NaiveDateTime) -> SlackTime {
52 SlackTime(*time)
53 }
54}
55
56impl Serialize for SlackTime {
57 fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
58 where
59 S: Serializer,
60 {
61 serializer.serialize_i64(self.0.and_utc().timestamp())
62 }
63}
64
65#[derive(Serialize, Debug, Default, Clone, PartialEq)]
68pub struct SlackText(String);
69
70impl SlackText {
71 pub fn new<S: Into<String>>(text: S) -> SlackText {
75 let s = text.into().chars().fold(String::new(), |mut s, c| {
76 match c {
77 '&' => s.push_str("&"),
78 '<' => s.push_str("<"),
79 '>' => s.push_str(">"),
80 _ => s.push(c),
81 }
82 s
83 });
84 SlackText(s)
85 }
86
87 fn new_raw<S: Into<String>>(text: S) -> SlackText {
88 SlackText(text.into())
89 }
90}
91
92impl<'a> From<&'a str> for SlackText {
93 fn from(s: &'a str) -> SlackText {
94 SlackText::new(String::from(s))
95 }
96}
97
98impl From<String> for SlackText {
99 fn from(s: String) -> SlackText {
100 SlackText::new(s)
101 }
102}
103
104#[derive(Debug, Clone, PartialEq)]
108pub enum SlackTextContent {
109 Text(SlackText),
111 Link(SlackLink),
113 User(SlackUserLink),
115}
116
117impl From<&[SlackTextContent]> for SlackText {
118 fn from(v: &[SlackTextContent]) -> SlackText {
119 let st = v
120 .iter()
121 .map(|item| match item {
122 SlackTextContent::Text(s) => s.to_string(),
123 SlackTextContent::Link(link) => link.to_string(),
124 SlackTextContent::User(u) => u.to_string(),
125 })
126 .collect::<Vec<String>>()
127 .join(" ");
128 SlackText::new_raw(st)
129 }
130}
131
132impl fmt::Display for SlackText {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 self.0.fmt(f)
135 }
136}
137
138#[derive(Debug, Clone, PartialEq)]
140pub struct SlackLink {
141 pub url: String,
146 pub text: SlackText,
148}
149
150impl SlackLink {
151 pub fn new(url: &str, text: &str) -> SlackLink {
153 SlackLink {
154 url: url.to_owned(),
155 text: SlackText::new(text),
156 }
157 }
158}
159
160impl fmt::Display for SlackLink {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 write!(f, "<{}|{}>", self.url, self.text)
163 }
164}
165
166impl Serialize for SlackLink {
167 fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
168 where
169 S: Serializer,
170 {
171 serializer.serialize_str(&self.to_string())
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, PartialOrd)]
180pub struct SlackUserLink {
181 pub uid: String,
183}
184
185impl SlackUserLink {
186 pub fn new(uid: &str) -> SlackUserLink {
188 SlackUserLink {
189 uid: uid.to_owned(),
190 }
191 }
192}
193
194impl fmt::Display for SlackUserLink {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 write!(f, "<{}>", self.uid)
197 }
198}
199
200impl Serialize for SlackUserLink {
201 fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
202 where
203 S: Serializer,
204 {
205 serializer.serialize_str(&self.to_string())
206 }
207}
208
209#[cfg(test)]
210mod test {
211 use crate::slack::{Slack, SlackLink};
212 use crate::{AttachmentBuilder, Field, Parse, PayloadBuilder, SlackText};
213 use chrono::DateTime;
214 use insta::{assert_json_snapshot, assert_snapshot};
215
216 #[test]
217 fn slack_incoming_url() {
218 let s = Slack::new("https://hooks.slack.com/services/abc/123/45z").unwrap();
219 assert_snapshot!(s.hook, @"https://hooks.slack.com/services/abc/123/45z");
220 }
221
222 #[test]
223 fn slack_text() {
224 let s = SlackText::new("moo <&> moo");
225 assert_snapshot!(s, @"moo <&> moo");
226 }
227
228 #[test]
229 fn slack_link() {
230 let s = SlackLink {
231 text: SlackText::new("moo <&> moo"),
232 url: "http://google.com".to_owned(),
233 };
234 assert_snapshot!(s, @"<http://google.com|moo <&> moo>");
235 }
236
237 #[test]
238 fn json_slacklink() {
239 let s = SlackLink {
240 text: SlackText::new("moo <&> moo"),
241 url: "http://google.com".to_owned(),
242 };
243 assert_json_snapshot!(s, @r###""<http://google.com|moo <&> moo>""###)
244 }
245
246 #[test]
247 fn json_complete_payload() {
248 let a = vec![AttachmentBuilder::new("fallback <&>")
249 .text("text <&>")
250 .color("#6800e8")
251 .fields(vec![Field::new("title", "value", None)])
252 .title_link("https://title_link.com/")
253 .ts(&DateTime::from_timestamp(123_456_789, 0)
254 .unwrap()
255 .naive_utc())
256 .build()
257 .unwrap()];
258
259 let p = PayloadBuilder::new()
260 .text("test message")
261 .channel("#abc")
262 .username("Bot")
263 .icon_emoji(":chart_with_upwards_trend:")
264 .icon_url("https://example.com")
265 .attachments(a)
266 .unfurl_links(false)
267 .link_names(true)
268 .parse(Parse::Full)
269 .build()
270 .unwrap();
271
272 assert_json_snapshot!(
273 p,
274 @r###"
275 {
276 "text": "test message",
277 "channel": "#abc",
278 "username": "Bot",
279 "icon_url": "https://example.com/",
280 "icon_emoji": ":chart_with_upwards_trend:",
281 "attachments": [
282 {
283 "fallback": "fallback <&>",
284 "text": "text <&>",
285 "color": "#6800e8",
286 "fields": [
287 {
288 "title": "title",
289 "value": "value"
290 }
291 ],
292 "title_link": "https://title_link.com/",
293 "ts": 123456789
294 }
295 ],
296 "unfurl_links": false,
297 "link_names": 1,
298 "parse": "full"
299 }
300 "###
301 );
302 }
303
304 #[test]
305 fn json_message_payload() {
306 let p = PayloadBuilder::new().text("test message").build().unwrap();
307
308 assert_json_snapshot!(
309 p,
310 @r###"
311 {
312 "text": "test message"
313 }
314 "###,
315 );
316 }
317
318 #[test]
319 fn slack_text_content() {
320 use super::SlackTextContent;
321 let message = [
322 SlackTextContent::Text("moo <&> moo".into()),
323 SlackTextContent::Link(SlackLink::new("@USER", "M<E>")),
324 SlackTextContent::Text("wow.".into()),
325 ];
326 let st = SlackText::from(&message[..]);
327 assert_snapshot!(st, @"moo <&> moo <@USER|M<E>> wow.");
328 }
329}