Skip to main content

pingap_webhook/
lib.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// Sends a notification via configured webhook
45    ///
46    /// Formats and sends the notification based on the webhook type (wecom, dingtalk, etc).
47    /// Will log success/failure and handle timeouts.
48    ///
49    /// # Arguments
50    /// * `params` - The notification parameters including category, level, message and optional remark
51    pub async fn send_notification(&self, params: NotificationData) {
52        let title = &params.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(&params.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        // TODO get app name from config
77        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
178/// Returns a list of non-loopback IP addresses (both IPv4 and IPv6) for the local machine
179///
180/// # Returns
181/// A vector of IP addresses as strings
182fn 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}