openzeppelin_monitor/services/notification/
discord.rsuse async_trait::async_trait;
use serde::Serialize;
use serde_json;
use std::collections::HashMap;
use crate::{
models::TriggerTypeConfig,
services::notification::{NotificationError, Notifier, WebhookConfig, WebhookNotifier},
};
pub struct DiscordNotifier {
inner: WebhookNotifier,
}
#[derive(Serialize)]
struct DiscordField {
name: String,
value: String,
inline: Option<bool>,
}
#[derive(Serialize)]
struct DiscordEmbed {
title: String,
description: Option<String>,
url: Option<String>,
color: Option<u32>,
fields: Option<Vec<DiscordField>>,
tts: Option<bool>,
thumbnail: Option<String>,
image: Option<String>,
footer: Option<String>,
author: Option<String>,
timestamp: Option<String>,
}
#[derive(Serialize)]
struct DiscordMessage {
content: String,
username: Option<String>,
avatar_url: Option<String>,
embeds: Option<Vec<DiscordEmbed>>,
}
impl DiscordNotifier {
pub fn new(
url: String,
title: String,
body_template: String,
) -> Result<Self, Box<NotificationError>> {
Ok(Self {
inner: WebhookNotifier::new(WebhookConfig {
url,
url_params: None,
title,
body_template,
method: Some("POST".to_string()),
secret: None,
headers: None,
payload_fields: None,
})?,
})
}
pub fn format_message(&self, variables: &HashMap<String, String>) -> String {
let message = self.inner.format_message(variables);
format!("*{}*\n\n{}", self.inner.title, message)
}
pub fn from_config(config: &TriggerTypeConfig) -> Option<Self> {
match config {
TriggerTypeConfig::Discord {
discord_url,
message,
} => WebhookNotifier::new(WebhookConfig {
url: discord_url.as_ref().to_string(),
url_params: None,
title: message.title.clone(),
body_template: message.body.clone(),
method: Some("POST".to_string()),
secret: None,
headers: None,
payload_fields: None,
})
.ok()
.map(|inner| Self { inner }),
_ => None,
}
}
}
#[async_trait]
impl Notifier for DiscordNotifier {
async fn notify(&self, message: &str) -> Result<(), anyhow::Error> {
let mut payload_fields = HashMap::new();
let discord_message = DiscordMessage {
content: message.to_string(),
username: None,
avatar_url: None,
embeds: None,
};
payload_fields.insert(
"content".to_string(),
serde_json::json!(discord_message.content),
);
self.inner
.notify_with_payload(message, payload_fields)
.await
}
}
#[cfg(test)]
mod tests {
use crate::models::{NotificationMessage, SecretString, SecretValue};
use super::*;
fn create_test_notifier(body_template: &str) -> DiscordNotifier {
DiscordNotifier::new(
"https://non-existent-url-discord-webhook.com".to_string(),
"Alert".to_string(),
body_template.to_string(),
)
.unwrap()
}
fn create_test_discord_config() -> TriggerTypeConfig {
TriggerTypeConfig::Discord {
discord_url: SecretValue::Plain(SecretString::new(
"https://discord.example.com".to_string(),
)),
message: NotificationMessage {
title: "Test Alert".to_string(),
body: "Test message ${value}".to_string(),
},
}
}
#[test]
fn test_format_message() {
let notifier = create_test_notifier("Value is ${value} and status is ${status}");
let mut variables = HashMap::new();
variables.insert("value".to_string(), "100".to_string());
variables.insert("status".to_string(), "critical".to_string());
let result = notifier.format_message(&variables);
assert_eq!(result, "*Alert*\n\nValue is 100 and status is critical");
}
#[test]
fn test_format_message_with_missing_variables() {
let notifier = create_test_notifier("Value is ${value} and status is ${status}");
let mut variables = HashMap::new();
variables.insert("value".to_string(), "100".to_string());
let result = notifier.format_message(&variables);
assert_eq!(result, "*Alert*\n\nValue is 100 and status is ${status}");
}
#[test]
fn test_format_message_with_empty_template() {
let notifier = create_test_notifier("");
let variables = HashMap::new();
let result = notifier.format_message(&variables);
assert_eq!(result, "*Alert*\n\n");
}
#[test]
fn test_from_config_with_discord_config() {
let config = create_test_discord_config();
let notifier = DiscordNotifier::from_config(&config);
assert!(notifier.is_some());
let notifier = notifier.unwrap();
assert_eq!(notifier.inner.url, "https://discord.example.com");
assert_eq!(notifier.inner.title, "Test Alert");
assert_eq!(notifier.inner.body_template, "Test message ${value}");
}
#[tokio::test]
async fn test_notify_failure() {
let notifier = create_test_notifier("Test message");
let result = notifier.notify("Test message").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_notify_with_payload_failure() {
let notifier = create_test_notifier("Test message");
let result = notifier
.notify_with_payload("Test message", HashMap::new())
.await;
assert!(result.is_err());
}
}