rnotifylib/destination/kinds/
discord.rs1use 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 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}