1use async_trait::async_trait;
16use pingap_core::{
17 get_hostname, Notification, NotificationData, NotificationLevel,
18};
19use serde_json::{Map, Value};
20use std::time::Duration;
21use tracing::{error, info};
22
23pub const LOG_CATEGORY: &str = "webhook";
24
25pub struct WebhookNotificationSender {
26 url: String,
27 category: String,
28 notifications: Vec<String>,
29}
30
31impl WebhookNotificationSender {
32 pub fn new(
33 url: String,
34 category: String,
35 notifications: Vec<String>,
36 ) -> Self {
37 Self {
38 url,
39 category,
40 notifications,
41 }
42 }
43
44 pub async fn send_notification(&self, params: NotificationData) {
52 info!(
53 category = LOG_CATEGORY,
54 notification = params.category,
55 title = params.title,
56 message = params.message,
57 "webhook notification"
58 );
59 let webhook_type = &self.category;
60 let url = &self.url;
61 if url.is_empty() {
62 return;
63 }
64 let found = self.notifications.contains(¶ms.category.to_string());
65 if !found {
66 return;
67 }
68 let category = params.category.to_string();
69 let level = params.level;
70 let ip = local_ip_list().join(";");
71
72 let client = reqwest::Client::new();
73 let mut data = serde_json::Map::new();
74 let hostname = get_hostname();
75 let name = "pingap".to_string();
77 let color_type = match level {
78 NotificationLevel::Error => "warning",
79 NotificationLevel::Warn => "warning",
80 _ => "comment",
81 };
82 let content = format!(
83 r###" <font color="{color_type}">{name}({level})</font>
84 >hostname: {hostname}
85 >ip: {ip}
86 >category: {category}
87 >message: {}"###,
88 params.message
89 );
90 match webhook_type.to_lowercase().as_str() {
91 "wecom" => {
92 let mut markdown_data = Map::new();
93 markdown_data
94 .insert("content".to_string(), Value::String(content));
95 data.insert(
96 "msgtype".to_string(),
97 Value::String("markdown".to_string()),
98 );
99 data.insert(
100 "markdown".to_string(),
101 Value::Object(markdown_data),
102 );
103 },
104 "dingtalk" => {
105 let mut markdown_data = serde_json::Map::new();
106 markdown_data.insert(
107 "title".to_string(),
108 Value::String(category.to_string()),
109 );
110 markdown_data
111 .insert("text".to_string(), Value::String(content));
112 data.insert(
113 "msgtype".to_string(),
114 Value::String("markdown".to_string()),
115 );
116 data.insert(
117 "markdown".to_string(),
118 Value::Object(markdown_data),
119 );
120 },
121 _ => {
122 data.insert("name".to_string(), Value::String(name));
123 data.insert(
124 "level".to_string(),
125 Value::String(level.to_string()),
126 );
127 data.insert(
128 "hostname".to_string(),
129 Value::String(hostname.to_string()),
130 );
131 data.insert("ip".to_string(), Value::String(ip));
132 data.insert("category".to_string(), Value::String(category));
133 data.insert(
134 "message".to_string(),
135 Value::String(params.message),
136 );
137 },
138 }
139
140 match client
141 .post(url)
142 .json(&data)
143 .timeout(Duration::from_secs(30))
144 .send()
145 .await
146 {
147 Ok(res) => {
148 if res.status().as_u16() < 400 {
149 info!(category = LOG_CATEGORY, "send webhook success");
150 } else {
151 error!(
152 category = LOG_CATEGORY,
153 status = res.status().to_string(),
154 "send webhook fail"
155 );
156 }
157 },
158 Err(e) => {
159 error!(
160 category = LOG_CATEGORY,
161 error = %e,
162 "send webhook fail"
163 );
164 },
165 };
166 }
167}
168
169#[async_trait]
170impl Notification for WebhookNotificationSender {
171 async fn notify(&self, data: NotificationData) {
172 self.send_notification(data).await;
173 }
174}
175
176fn local_ip_list() -> Vec<String> {
181 let mut ip_list = vec![];
182
183 if let Ok(value) = local_ip_address::local_ip() {
184 ip_list.push(value);
185 }
186 if let Ok(value) = local_ip_address::local_ipv6() {
187 ip_list.push(value);
188 }
189
190 ip_list
191 .iter()
192 .filter(|item| !item.is_loopback())
193 .map(|item| item.to_string())
194 .collect()
195}