use crate::{Error, Payload, Result};
use chrono::NaiveDateTime;
use reqwest::{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 async fn send(&self, payload: &Payload) -> Result<()> {
let response = self
.client
.post(self.hook.clone())
.json(payload)
.send()
.await?;
if response.status().is_success() {
Ok(())
} else {
Err(Error::Slack(format!("HTTP error {}", response.status())))
}
}
}
#[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.and_utc().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 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 From<&[SlackTextContent]> for SlackText {
fn from(v: &[SlackTextContent]) -> SlackText {
let st = v
.iter()
.map(|item| match item {
SlackTextContent::Text(s) => s.to_string(),
SlackTextContent::Link(link) => link.to_string(),
SlackTextContent::User(u) => u.to_string(),
})
.collect::<Vec<String>>()
.join(" ");
SlackText::new_raw(st)
}
}
impl fmt::Display for SlackText {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[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(&self.to_string())
}
}
#[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(&self.to_string())
}
}
#[cfg(test)]
mod test {
use crate::slack::{Slack, SlackLink};
use crate::{AttachmentBuilder, Field, Parse, PayloadBuilder, SlackText};
use chrono::DateTime;
use insta::{assert_json_snapshot, assert_snapshot};
#[test]
fn slack_incoming_url() {
let s = Slack::new("https://hooks.slack.com/services/abc/123/45z").unwrap();
assert_snapshot!(s.hook, @"https://hooks.slack.com/services/abc/123/45z");
}
#[test]
fn slack_text() {
let s = SlackText::new("moo <&> moo");
assert_snapshot!(s, @"moo <&> moo");
}
#[test]
fn slack_link() {
let s = SlackLink {
text: SlackText::new("moo <&> moo"),
url: "http://google.com".to_owned(),
};
assert_snapshot!(s, @"<http://google.com|moo <&> moo>");
}
#[test]
fn json_slacklink() {
let s = SlackLink {
text: SlackText::new("moo <&> moo"),
url: "http://google.com".to_owned(),
};
assert_json_snapshot!(s, @r###""<http://google.com|moo <&> moo>""###)
}
#[test]
fn json_complete_payload() {
let a = vec![AttachmentBuilder::new("fallback <&>")
.text("text <&>")
.color("#6800e8")
.fields(vec![Field::new("title", "value", None)])
.title_link("https://title_link.com/")
.ts(&DateTime::from_timestamp(123_456_789, 0)
.unwrap()
.naive_utc())
.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_json_snapshot!(
p,
@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"
}
"###
);
}
#[test]
fn json_message_payload() {
let p = PayloadBuilder::new().text("test message").build().unwrap();
assert_json_snapshot!(
p,
@r###"
{
"text": "test message"
}
"###,
);
}
#[test]
fn slack_text_content() {
use super::SlackTextContent;
let message = [
SlackTextContent::Text("moo <&> moo".into()),
SlackTextContent::Link(SlackLink::new("@USER", "M<E>")),
SlackTextContent::Text("wow.".into()),
];
let st = SlackText::from(&message[..]);
assert_snapshot!(st, @"moo <&> moo <@USER|M<E>> wow.");
}
}