use crate::error::{ErrorKind, Result};
use crate::Payload;
use chrono::NaiveDateTime;
use reqwest::{blocking::Client, Url};
use serde::{Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone)]
pub struct Slack {
hook: Url,
client: Client,
}
impl Slack {
pub fn new<T: reqwest::IntoUrl>(hook: T) -> Result<Slack> {
Ok(Slack {
hook: hook.into_url()?,
client: Client::new(),
})
}
pub fn send(&self, payload: &Payload) -> Result<()> {
let response = self.client.post(self.hook.clone()).json(payload).send()?;
if response.status().is_success() {
Ok(())
} else {
Err(ErrorKind::Slack(format!("HTTP error {}", response.status())).into())
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct SlackTime(NaiveDateTime);
impl SlackTime {
pub fn new(time: &NaiveDateTime) -> SlackTime {
SlackTime(*time)
}
}
impl Serialize for SlackTime {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i64(self.0.timestamp())
}
}
#[derive(Serialize, Debug, Default, Clone, PartialEq)]
pub struct SlackText(String);
impl SlackText {
pub fn new<S: Into<String>>(text: S) -> SlackText {
let s = text.into().chars().fold(String::new(), |mut s, c| {
match c {
'&' => s.push_str("&"),
'<' => s.push_str("<"),
'>' => s.push_str(">"),
_ => s.push(c),
}
s
});
SlackText(s)
}
fn new_raw<S: Into<String>>(text: S) -> SlackText {
SlackText(text.into())
}
}
impl<'a> From<&'a str> for SlackText {
fn from(s: &'a str) -> SlackText {
SlackText::new(String::from(s))
}
}
impl<'a> From<String> for SlackText {
fn from(s: String) -> SlackText {
SlackText::new(s)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SlackTextContent {
Text(SlackText),
Link(SlackLink),
User(SlackUserLink),
}
impl<'a> From<&'a [SlackTextContent]> for SlackText {
fn from(v: &[SlackTextContent]) -> SlackText {
let st = v
.iter()
.map(|item| match *item {
SlackTextContent::Text(ref s) => format!("{}", s),
SlackTextContent::Link(ref link) => format!("{}", link),
SlackTextContent::User(ref u) => format!("{}", u),
})
.collect::<Vec<String>>()
.join(" ");
SlackText::new_raw(st)
}
}
impl fmt::Display for SlackText {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SlackLink {
pub url: String,
pub text: SlackText,
}
impl SlackLink {
pub fn new(url: &str, text: &str) -> SlackLink {
SlackLink {
url: url.to_owned(),
text: SlackText::new(text),
}
}
}
impl fmt::Display for SlackLink {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<{}|{}>", self.url, self.text)
}
}
impl Serialize for SlackLink {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{}", self)[..])
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct SlackUserLink {
pub uid: String,
}
impl SlackUserLink {
pub fn new(uid: &str) -> SlackUserLink {
SlackUserLink {
uid: uid.to_owned(),
}
}
}
impl fmt::Display for SlackUserLink {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<{}>", self.uid)
}
}
impl Serialize for SlackUserLink {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{}", self)[..])
}
}
#[cfg(test)]
mod test {
use crate::slack::{Slack, SlackLink};
use crate::{AttachmentBuilder, Field, Parse, PayloadBuilder, SlackText};
use chrono::NaiveDateTime;
#[cfg(feature = "unstable")]
use test::Bencher;
#[test]
fn slack_incoming_url_test() {
let s = Slack::new("https://hooks.slack.com/services/abc/123/45z").unwrap();
assert_eq!(
s.hook.to_string(),
"https://hooks.slack.com/services/abc/123/45z".to_owned()
);
}
#[test]
fn slack_text_test() {
let s = SlackText::new("moo <&> moo");
assert_eq!(format!("{}", s), "moo <&> moo".to_owned());
}
#[test]
fn slack_link_test() {
let s = SlackLink {
text: SlackText::new("moo <&> moo"),
url: "http://google.com".to_owned(),
};
assert_eq!(
format!("{}", s),
"<http://google.com|moo <&> moo>".to_owned()
);
}
#[test]
fn json_slacklink_test() {
let s = SlackLink {
text: SlackText::new("moo <&> moo"),
url: "http://google.com".to_owned(),
};
assert_eq!(
serde_json::to_string(&s).unwrap(),
"\"<http://google.com|moo <&> moo>\"".to_owned()
)
}
#[test]
fn json_complete_payload_test() {
let a = vec![AttachmentBuilder::new("fallback <&>")
.text("text <&>")
.color("#6800e8")
.fields(vec![Field::new("title", "value", None)])
.title_link("https://title_link.com/")
.ts(&NaiveDateTime::from_timestamp(123_456_789, 0))
.build()
.unwrap()];
let p = PayloadBuilder::new()
.text("test message")
.channel("#abc")
.username("Bot")
.icon_emoji(":chart_with_upwards_trend:")
.icon_url("https://example.com")
.attachments(a)
.unfurl_links(false)
.link_names(true)
.parse(Parse::Full)
.build()
.unwrap();
assert_eq!(serde_json::to_string(&p).unwrap(),
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())
}
#[test]
fn json_message_payload_test() {
let p = PayloadBuilder::new().text("test message").build().unwrap();
assert_eq!(
serde_json::to_string(&p).unwrap(),
r##"{"text":"test message"}"##.to_owned()
)
}
#[test]
fn slack_text_content_test() {
use super::SlackTextContent;
use super::SlackTextContent::{Link, Text};
let message: Vec<SlackTextContent> = vec![
Text("moo <&> moo".into()),
Link(SlackLink::new("@USER", "M<E>")),
Text("wow.".into()),
];
let st: SlackText = SlackText::from(&message[..]);
assert_eq!(
format!("{}", st),
"moo <&> moo <@USER|M<E>> wow."
);
}
}