solid-xmpp-alarm/src/main.rs

252 lines
7.1 KiB
Rust

use axum::{
extract::{Json, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::post,
Router,
};
use std::net::SocketAddr;
use std::str::FromStr;
#[derive(Clone)]
struct AppState {
listen: String,
receiver: String,
jid: String,
password: String,
}
impl AppState{
pub fn new() -> Self {
Self {
listen: String::from("[::]:9199"),
receiver: String::from(""),
jid: String::from(""),
password: String::from(""),
}
}
pub fn is_valid(self) -> bool {
let mut valid = true;
if self.receiver == String::from("") {
println!("--receiver <jid> is not specified");
valid = false;
}
if self.jid == String::from("") {
println!("--jid <jid> is not specified");
valid = false;
}
if self.password == String::from("") {
println!("password is not specified, can't be empty");
valid = false;
}
return valid;
}
}
#[tokio::main]
async fn main() {
let mut app_state = AppState::new();
let mut password_file = String::from("");
let mut args = std::env::args();
let name = args.next().unwrap();
loop {
let arg = if let Some(arg) = args.next() {
arg
} else {
break;
};
match arg.as_str() {
"--help" | "-h" => {
println!("Alertmanager webhook receiver for xmpp clients");
println!("Use --listen <addr:port> to bind the web service.");
println!("");
println!("--receiver <jid> Who to send messaged to");
println!("--jid <jid> Who to send from");
println!("--password-file <path> Path to file with the password for the sender account");
std::process::exit(0);
}
"--listen" => {
app_state.listen = args.next().unwrap();
}
"--receiver" => {
app_state.receiver = args.next().unwrap();
}
"--jid" => {
app_state.jid = args.next().unwrap();
}
"--password-file" => {
password_file = args.next().unwrap();
}
unknown => {
println!("unknown option: {}", unknown);
std::process::exit(1)
}
}
}
if password_file == String::from("") {
println!("--password-file not specified");
} else {
app_state.password = std::fs::read_to_string(password_file).unwrap().trim().to_string();
}
if !app_state.clone().is_valid() {
println!("Invalid config");
std::process::exit(1);
}
let mut app = Router::new();
app = app.route("/alert", post(alert));
let app = app.with_state(app_state.clone());
let addr = SocketAddr::from_str(&app_state.listen.clone()).unwrap();
println!("listening on http://{}", addr);
println!("Sending alerts as {} to {}", app_state.jid, app_state.receiver);
println!("Use http://{}/alert as webhook target", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
fn status_to_emoji(status: &str, severity: &str) -> String {
let emoji = match status {
"firing" => match severity {
"warning" => "⚠️",
_ => "🔥",
},
"resolved" => "✔️",
_ => "",
};
String::from(emoji)
}
async fn alert(State(app_state): State<AppState>, Json(payload): Json<serde_json::Value>) -> Response {
println!("Incoming alerts:");
println!("{}", payload);
let mut message = String::from("");
let alerts = match payload.get("alerts") {
Some(alerts) => alerts,
None => {
println!("WARNING: No alerts to process");
return (StatusCode::OK, "").into_response();
}
};
let alerts = match alerts.as_array() {
Some(alerts) => alerts,
None => {
println!("ERROR: Alerts not of type list");
return (StatusCode::BAD_REQUEST, "").into_response();
}
};
for alert in alerts {
let status = match alert.get("status") {
Some(status) => status,
_ => {
println!("No status given");
return (StatusCode::BAD_REQUEST, "").into_response();
}
};
let status = match status.as_str() {
Some(status) => status,
_ => {
println!("ERROR: Status not a string");
return (StatusCode::BAD_REQUEST, "").into_response();
}
};
let mut severity = "";
if let Some(labels) = alert.get("labels") {
if let Some(severity_) = labels.get("severity") {
severity = severity_.as_str().unwrap_or("");
};
};
message.push_str(status_to_emoji(&status, &severity).as_str());
let annotations = match alert.get("annotations") {
Some(annotations) => annotations,
_ => {
println!("ERROR: No annotations given");
return (StatusCode::BAD_REQUEST, "").into_response();
}
};
if let Some(summary) = annotations.get("summary") {
if let Some(summary) = summary.as_str() {
message.push_str(format!(" {}", summary).as_str());
} else {
println!("ERROR: Summary not a string");
return (StatusCode::BAD_REQUEST, "").into_response();
}
} else {
println!("ERROR: No summary given");
return (StatusCode::BAD_REQUEST, "").into_response();
}
if let Some(description) = annotations.get("description") {
if let Some(description) = description.as_str() {
message.push_str(format!("\n{}", description).as_str());
}
}
message.push_str("\n\n");
}
let message = message.trim();
println!("Send message:");
println!("{}", message);
let mut command = std::process::Command::new("xmppc");
command.arg("--jid").arg(app_state.jid)
.arg("--pwd").arg(app_state.password)
.arg("--mode").arg("message")
.arg("chat").arg(app_state.receiver).arg(message);
let mut child = match command.spawn() {
Ok(child) => child,
Err(_) => {
println!("ERROR: Error starting xmppc");
return (StatusCode::INTERNAL_SERVER_ERROR, "").into_response();
}
};
let status = match child.wait() {
Ok(status) => status,
Err(_) => {
println!("ERROR: Error executing xmppc");
return (StatusCode::INTERNAL_SERVER_ERROR, "").into_response();
}
};
if !status.success() {
println!("ERROR: xmppc failed sending message");
return (StatusCode::INTERNAL_SERVER_ERROR, "").into_response();
}
println!("Message sent");
return (
StatusCode::OK,
"",
).into_response();
}