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