use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub server_name: Option<String>,
pub ssh_host: Option<String>,
#[serde(default = "default_check_interval_secs")]
pub check_interval_secs: u64,
#[serde(default = "default_cpu_threshold")]
pub cpu_threshold: f32,
#[serde(default = "default_cpu_streak")]
pub cpu_streak: u32,
#[serde(default = "default_mem_threshold_mb")]
pub mem_threshold_mb: u64,
#[serde(default = "default_http_port")]
pub http_port: u16,
#[serde(default = "default_http_host")]
pub http_host: String,
#[serde(default = "default_auth_token")]
pub auth_token: String,
pub base_url: Option<String>,
pub slack_bot_token: Option<String>,
pub slack_channel: Option<String>,
pub discord_webhook_url: Option<String>,
pub telegram_bot_token: Option<String>,
pub telegram_chat_id: Option<String>,
#[serde(default)]
pub custom_whitelist: Vec<String>,
}
fn default_check_interval_secs() -> u64 {
10
}
fn default_cpu_threshold() -> f32 {
120.0
}
fn default_cpu_streak() -> u32 {
3
}
fn default_mem_threshold_mb() -> u64 {
4096
}
fn default_http_port() -> u16 {
19999
}
fn default_http_host() -> String {
"0.0.0.0".to_string()
}
fn default_auth_token() -> String {
use rand::Rng;
let token: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(24)
.map(char::from)
.collect();
format!("maniac-{}", token)
}
impl Default for Config {
fn default() -> Self {
Self {
server_name: None,
ssh_host: None,
check_interval_secs: default_check_interval_secs(),
cpu_threshold: default_cpu_threshold(),
cpu_streak: default_cpu_streak(),
mem_threshold_mb: default_mem_threshold_mb(),
http_port: default_http_port(),
http_host: default_http_host(),
auth_token: default_auth_token(),
base_url: None,
slack_bot_token: None,
slack_channel: None,
discord_webhook_url: None,
telegram_bot_token: None,
telegram_chat_id: None,
custom_whitelist: Vec::new(),
}
}
}
impl Config {
pub fn get_server_name(&self) -> String {
if let Some(name) = &self.server_name {
if !name.trim().is_empty() {
return name.trim().to_string();
}
}
let hostname = sysinfo::System::host_name().unwrap_or_else(|| {
std::env::var("HOSTNAME")
.or_else(|_| std::env::var("HOST"))
.unwrap_or_else(|_| "unknown-server".to_string())
});
let lower = hostname.to_lowercase();
if lower.contains("mac") || lower.contains("vodana") || lower.contains("bodanaui") {
"ep-mac".to_string()
} else if lower.contains("martian2") {
"martian2".to_string()
} else if lower.contains("livemixer") || lower.contains("cycorld-b650") {
"cycorld-b650".to_string()
} else {
hostname
}
}
pub fn get_ssh_host(&self) -> String {
if let Some(host) = &self.ssh_host {
if !host.trim().is_empty() {
return host.trim().to_string();
}
}
self.get_server_name()
}
pub fn get_base_url(&self) -> String {
if let Some(url) = &self.base_url {
if !url.trim().is_empty() {
return url.trim().trim_end_matches('/').to_string();
}
}
let s_name = self.get_server_name();
if s_name == "ep-mac" {
format!("http://ep-mac.tail1dcdac.ts.net:{}", self.http_port)
} else if s_name == "martian2" {
format!("http://martian2.tail1bb4bf.ts.net:{}", self.http_port)
} else {
format!("http://localhost:{}", self.http_port)
}
}
pub fn load_or_default(custom_path: Option<&Path>) -> Self {
let mut config = Self::default();
let possible_paths = match custom_path {
Some(p) => vec![p.to_path_buf()],
None => vec![
PathBuf::from("maniac-killer.toml"),
dirs_home().join(".config/maniac-killer/config.toml"),
dirs_home().join("Documents/maniac-killer/maniac-killer.toml"),
PathBuf::from("/etc/maniac-killer/config.toml"),
],
};
for path in &possible_paths {
if path.exists() {
if let Ok(content) = std::fs::read_to_string(path) {
if let Ok(parsed) = toml::from_str::<Config>(&content) {
config = parsed;
break;
}
}
}
}
if let Ok(val) = std::env::var("MANIAC_SERVER_NAME") {
config.server_name = Some(val);
}
if let Ok(val) = std::env::var("MANIAC_SSH_HOST") {
config.ssh_host = Some(val);
}
if let Ok(val) = std::env::var("MANIAC_CHECK_INTERVAL") {
if let Ok(parsed) = val.parse::<u64>() {
config.check_interval_secs = parsed;
}
}
if let Ok(val) = std::env::var("MANIAC_CPU_THRESHOLD") {
if let Ok(parsed) = val.parse::<f32>() {
config.cpu_threshold = parsed;
}
}
if let Ok(val) = std::env::var("MANIAC_CPU_STREAK") {
if let Ok(parsed) = val.parse::<u32>() {
config.cpu_streak = parsed;
}
}
if let Ok(val) = std::env::var("MANIAC_MEM_THRESHOLD_MB") {
if let Ok(parsed) = val.parse::<u64>() {
config.mem_threshold_mb = parsed;
}
}
if let Ok(val) = std::env::var("MANIAC_HTTP_PORT") {
if let Ok(parsed) = val.parse::<u16>() {
config.http_port = parsed;
}
}
if let Ok(val) = std::env::var("MANIAC_HTTP_HOST") {
config.http_host = val;
}
if let Ok(val) = std::env::var("MANIAC_AUTH_TOKEN") {
config.auth_token = val;
}
if let Ok(val) = std::env::var("MANIAC_BASE_URL") {
config.base_url = Some(val);
}
if config.slack_bot_token.is_none() {
if let Ok(tok) = std::env::var("MANIAC_SLACK_BOT_TOKEN")
.or_else(|_| std::env::var("SLACK_BOT_TOKEN"))
{
config.slack_bot_token = Some(tok);
} else {
let ep_env = dirs_home().join("Documents/ep-erp-prod/.env.local");
if let Some((tok, chan)) = parse_env_file(&ep_env) {
config.slack_bot_token = Some(tok);
if config.slack_channel.is_none() {
config.slack_channel = Some(chan);
}
}
}
}
if config.slack_channel.is_none() {
if let Ok(chan) = std::env::var("MANIAC_SLACK_CHANNEL")
.or_else(|_| std::env::var("SLACK_ALERT_CHANNEL"))
{
config.slack_channel = Some(chan);
}
}
if config.discord_webhook_url.is_none() {
if let Ok(url) = std::env::var("MANIAC_DISCORD_WEBHOOK_URL")
.or_else(|_| std::env::var("DISCORD_WEBHOOK_URL"))
{
config.discord_webhook_url = Some(url);
}
}
if config.telegram_bot_token.is_none() {
if let Ok(tok) = std::env::var("MANIAC_TELEGRAM_BOT_TOKEN")
.or_else(|_| std::env::var("TELEGRAM_BOT_TOKEN"))
{
config.telegram_bot_token = Some(tok);
}
}
if config.telegram_chat_id.is_none() {
if let Ok(cid) = std::env::var("MANIAC_TELEGRAM_CHAT_ID")
.or_else(|_| std::env::var("TELEGRAM_CHAT_ID"))
{
config.telegram_chat_id = Some(cid);
}
}
config
}
}
fn dirs_home() -> PathBuf {
std::env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."))
}
fn parse_env_file(path: &Path) -> Option<(String, String)> {
let content = std::fs::read_to_string(path).ok()?;
let mut token = None;
let mut channel = None;
for line in content.lines() {
let line = line.trim();
if let Some(val) = line.strip_prefix("SLACK_BOT_TOKEN=") {
let clean = val.trim_matches(|c| c == '"' || c == '\'').to_string();
token = Some(clean);
} else if let Some(val) = line.strip_prefix("SLACK_ALERT_CHANNEL=") {
let clean = val.trim_matches(|c| c == '"' || c == '\'').to_string();
channel = Some(clean);
}
}
if let (Some(t), Some(c)) = (token, channel) {
Some((t, c))
} else {
None
}
}