mod utils;
mod msg;
use serde_derive::{Deserialize, Serialize};
use msg::{Text, Card};
use std::thread;
use std::time::Duration;
use reqwest::blocking::Client;
#[derive(Debug, Serialize, Deserialize)]
pub struct WebHookInput {
timestamp: String,
sign: String,
msg_type: String,
content: Option<Text>,
card: Option<Card>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WebHookOutput {
#[serde(rename = "StatusCode")]
status_code: i64,
#[serde(rename = "StatusMessage")]
status_message: String,
}
#[allow(dead_code)]
pub fn send_text(msg: String) {
let timestamp = chrono::Local::now().timestamp();
let input = WebHookInput {
timestamp: timestamp.to_string(),
sign: utils::gen_sign(utils::get_fs_secret().as_str(), timestamp),
msg_type: "text".to_string(),
content: Some(Text::new(msg)),
card: None,
};
if let Err(e) = send(&input) {
log::error!("send text to feishu failed: {:?}", e)
};
}
#[allow(dead_code)]
pub fn send_card(card: Card) {
let disable_feishu = std::env::var("DISABLED_NOTIFIER");
if disable_feishu.is_ok() && disable_feishu.unwrap() == "true" {
return;
}
let timestamp = chrono::Local::now().timestamp();
let input = WebHookInput {
timestamp: timestamp.to_string(),
sign: utils::gen_sign(utils::get_fs_secret().as_str(), timestamp),
msg_type: "interactive".to_string(),
content: None,
card: Some(card),
};
if let Err(e) = send(&input) {
log::error!("send card to feishu failed: {:?}", e)
};
}
#[allow(dead_code)]
pub fn report_card(title: &str, msg: &str, at: &str) {
let now = chrono::Local::now()
.format("%Y/%m/%d %H:%M")
.to_string();
let message = std::format!("detail:\n{}", msg);
let is_err = msg.contains("错误") || msg.contains("err");
let user = if !at.is_empty() && is_err {
std::format!("<at id={}></at>\n", at)
} else {
"".to_string()
};
let time = std::format!("time:\n {}", now.as_str());
let mut card = Card::new(
title.to_string(), std::vec![message, user, time],
None, );
if is_err {
card.set_title_color("red");
}
send_card(card);
}
pub fn send(input: &WebHookInput) -> Result<WebHookOutput, String> {
log::debug!("send to feishu input: {:?}", input);
let client = Client::new();
let mut retries = 3;
let mut wait_time = Duration::from_secs(5);
while retries > 0 {
let response = client
.post(utils::get_fs_url())
.header("content-type", "application/json")
.json(&input)
.send();
match response {
Ok(resp) => {
let http_code = resp.status();
let body = match resp.text() {
Ok(text) => text,
Err(e) => return Err(format!("Failed to read response text: {:?}", e)),
};
log::debug!(
"send to feishu http status code: {:?}, body: {:?}",
http_code,
body
);
let output: WebHookOutput = match serde_json::from_str(&body) {
Ok(data) => data,
Err(e) => return Err(format!("Failed to parse response body: {:?}", e)),
};
if http_code == reqwest::StatusCode::OK && output.status_code == 0 {
return Ok(output);
} else {
log::error!(
"send to feishu failed with status code: {}, message: {}",
output.status_code,
output.status_message
);
if http_code.is_server_error() {
retries -= 1;
thread::sleep(wait_time);
wait_time *= 2;
} else {
return Err(format!(
"send to feishu failed with status code: {}, message: {}",
output.status_code, output.status_message
));
}
}
}
Err(e) => {
if retries == 1 {
return Err(format!("send to feishu failed: {:?}", e));
}
log::error!("send to feishu failed: {:?}", e);
retries -= 1;
thread::sleep(wait_time);
wait_time *= 2;
}
}
}
Err("send to feishu failed after retries".to_string())
}