rnotifylib/destination/kinds/
discord.rs

1use std::error::Error;
2use chrono::{SecondsFormat, TimeZone, Utc};
3use serde::{Serialize, Deserialize};
4use crate::util::http_util;
5use crate::destination::message_condition::MessageNotifyConditionConfigEntry;
6use crate::destination::{MessageDestination, SerializableDestination};
7use crate::message::formatted_detail::{FormattedMessageComponent, FormattedString, Style};
8use crate::message::{Level, Message, MessageDetail};
9
10#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
11pub struct DiscordDestination {
12    url: String,
13    username: Option<String>,
14    #[serde(default = "Vec::new")]
15    notify: Vec<MessageNotifyConditionConfigEntry<String>>,
16}
17
18impl DiscordDestination {
19    pub fn new(url: String) -> Self {
20        Self {
21            url,
22            username: None,
23            notify: vec![]
24        }
25    }
26
27    fn to_discord_message(&self, message: &Message) -> discord_webhook::models::Message {
28        let mut discord_msg = discord_webhook::models::Message::new();
29
30        let notify_receivers: Vec<String> = self.notify.iter().filter(|n| n.matches(message))
31            .map(|n| n.get_notify())
32            .map(|s| s.to_owned())
33            .collect();
34
35        if !notify_receivers.is_empty() {
36            let content = notify_receivers.join(" ");
37            discord_msg.content(&content);
38        }
39
40        discord_msg.embed(|embed| {
41            embed.title(message.get_title().as_deref().unwrap_or("Rnotify Notification"));
42
43            let timestamp = Utc::timestamp_millis(&Utc, message.get_unix_timestamp_millis());
44            let footer_str = format!("{} @ {}\n{} v{}",
45                                     timestamp.to_rfc3339_opts(SecondsFormat::Millis, true),
46                                     message.get_author(),
47                                     env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
48            embed.footer(&footer_str, None);
49            let color = get_color_from_level(message.get_level());
50            embed.color(&format!("{}", color));
51
52            if let Some(component) = message.get_component() {
53                embed.author(&format!("[{}]", component), None, None);
54            }
55
56            match message.get_message_detail() {
57                MessageDetail::Raw(raw) => { embed.description(raw); },
58                MessageDetail::Formatted(formatted) => {
59                    for component in formatted.components() {
60                        match component {
61                            FormattedMessageComponent::Section(title, contents) => {
62                                let string: String = contents.iter().map(|s| to_discord_format(s)).collect();
63                                embed.field(title, &string, false);
64                            }
65                            FormattedMessageComponent::Text(text) => {
66                                let string: String = text.iter().map(|s| to_discord_format(s)).collect();
67                                embed.description(&string);
68                            }
69                        }
70                    }
71                }
72            }
73
74            return embed;
75        });
76
77        discord_msg
78    }
79}
80
81fn to_discord_format(formatted_string: &FormattedString) -> String {
82    let mut result = String::from(formatted_string.get_string());
83    for style in formatted_string.get_styles() {
84        result = apply_style(&result, style);
85    }
86    result
87}
88
89fn apply_style(s: &str, style: &Style) -> String {
90    match style {
91        Style::Bold => format!("**{}**", s),
92        Style::Italics => format!("_{}_", s),
93        Style::Monospace => {
94            if s.is_empty() || s.contains('\n') {
95                format!("```\n{}\n```", s);
96            }
97            format!("`{}`", s)
98        },
99        Style::Code { lang} => format!("```{}\n{}```", lang, s),
100    }
101}
102
103impl MessageDestination for DiscordDestination {
104    fn send(&self, message: &Message) -> Result<(), Box<dyn Error>> {
105        let discord_msg = self.to_discord_message(message);
106        //let payload = serde_json::to_string(&discord_msg)?;
107        http_util::post_as_json_to(&self.url, &discord_msg)
108    }
109}
110
111#[typetag::serde(name = "Discord")]
112impl SerializableDestination for DiscordDestination {
113    fn as_message_destination(&self) -> &dyn MessageDestination {
114        self
115    }
116}
117
118fn get_color_from_level(level: &Level) -> u32 {
119    match level {
120        Level::Info => 0x00F4D0,
121        Level::Warn => 0xFFFF00,
122        Level::Error => 0xFF0000,
123        Level::SelfError => 0xB30000,
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use std::fs;
130    use crate::destination::kinds::discord::DiscordDestination;
131
132    #[test]
133    fn test_deserialize() {
134        let s = fs::read_to_string("test/discord_example.toml").expect("Should be able to read file");
135        let dest: DiscordDestination = toml::from_str(&s).expect("Should deserialize");
136
137        let expected = DiscordDestination::new("https://discord.com/api/webhooks/11111111111111/2aaaaaaaaaaaaaaaaa".to_string());
138
139        assert_eq!(dest, expected);
140    }
141}