rust_commit_tracker/core/
config.rs1use serde::{Deserialize, Serialize};
2use std::error::Error;
3use std::fs;
4use std::io::{self, Write};
5use std::path::Path;
6
7const CONFIG_FILE: &str = "config.toml";
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Config {
11 pub discord: DiscordConfig,
12 pub monitoring: MonitoringConfig,
13 pub appearance: AppearanceConfig,
14 pub database: DatabaseConfig,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct DiscordConfig {
19 pub webhook_url: String,
20 pub bot_name: String,
21 pub bot_avatar_url: String,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct MonitoringConfig {
26 pub commits_url: String,
27 pub check_interval_secs: u64,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct AppearanceConfig {
32 pub embed_color: String,
33 pub footer_icon_url: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct DatabaseConfig {
38 pub url: String,
39 pub cleanup_keep_last: i64,
40}
41
42impl Config {
43 pub fn load_or_create() -> Result<Self, Box<dyn Error>> {
44 if Path::new(CONFIG_FILE).exists() {
45 let config = Self::load_from_file()?;
46 config.validate()?;
47 Ok(config)
48 } else {
49 Self::create_default_and_prompt()
50 }
51 }
52
53 fn load_from_file() -> Result<Self, Box<dyn Error>> {
54 let content = fs::read_to_string(CONFIG_FILE)?;
55
56 match toml::from_str::<Config>(&content) {
58 Ok(config) => Ok(config),
59 Err(e) => {
60 if e.to_string().contains("missing field") {
62 println!("⚠️ Config file is missing new fields, updating...");
63
64 let mut existing: toml::Value = toml::from_str(&content)?;
66 let default_config = Self::default();
67 let default_value = toml::Value::try_from(&default_config)?;
68
69 if let (toml::Value::Table(existing_table), toml::Value::Table(default_table)) =
71 (&mut existing, default_value)
72 {
73 for (key, value) in default_table {
74 if !existing_table.contains_key(&key) {
75 existing_table.insert(key, value);
76 }
77 }
78 }
79
80 let updated_config: Config = existing.try_into()?;
82 let updated_content = toml::to_string_pretty(&updated_config)?;
83 fs::write(CONFIG_FILE, &updated_content)?;
84
85 println!("✅ Config file updated with new fields");
86 Ok(updated_config)
87 } else {
88 Err(e.into())
89 }
90 }
91 }
92 }
93
94 fn create_default_and_prompt() -> Result<Self, Box<dyn Error>> {
95 println!("🔧 First time setup - Creating configuration file...");
96
97 let default_config = Self::default();
98 let toml_content = toml::to_string_pretty(&default_config)?;
99
100 fs::write(CONFIG_FILE, &toml_content)?;
101
102 println!("✅ Created '{}'", CONFIG_FILE);
103 println!();
104 println!("📝 Please edit the configuration file with your settings:");
105 println!(" - Discord webhook URL (REQUIRED)");
106 println!(" - Bot name and avatar (optional)");
107 println!(" - Monitoring settings (optional)");
108 println!(" - Database path (optional)");
109 println!();
110 print!("Press Enter when you've finished editing the config file...");
111 io::stdout().flush()?;
112
113 let mut input = String::new();
114 io::stdin().read_line(&mut input)?;
115
116 let config = Self::load_from_file()?;
118 config.validate()?;
119 Ok(config)
120 }
121
122 pub fn validate(&self) -> Result<(), Box<dyn Error>> {
123 if self.discord.webhook_url == "REPLACE_WITH_YOUR_DISCORD_WEBHOOK_URL"
125 || self.discord.webhook_url.trim().is_empty() {
126 return Err(format!(
127 "❌ Discord webhook URL not configured!\n\
128 Please edit '{}' and set a valid Discord webhook URL.\n\
129 You can get one from your Discord server settings → Integrations → Webhooks",
130 CONFIG_FILE
131 ).into());
132 }
133
134 if !self.discord.webhook_url.starts_with("https://discord.com/api/webhooks/")
136 && !self.discord.webhook_url.starts_with("https://discordapp.com/api/webhooks/") {
137 return Err(format!(
138 "❌ Invalid Discord webhook URL format!\n\
139 Expected: https://discord.com/api/webhooks/...\n\
140 Got: {}",
141 self.discord.webhook_url
142 ).into());
143 }
144
145 Ok(())
146 }
147
148 pub fn rust_color(&self) -> u32 {
149 if self.appearance.embed_color.starts_with('#') {
151 u32::from_str_radix(&self.appearance.embed_color[1..], 16).unwrap_or(0xCD412B)
152 } else {
153 u32::from_str_radix(&self.appearance.embed_color, 16).unwrap_or(0xCD412B)
154 }
155 }
156}
157
158impl Default for Config {
159 fn default() -> Self {
160 Self {
161 discord: DiscordConfig {
162 webhook_url: "REPLACE_WITH_YOUR_DISCORD_WEBHOOK_URL".to_string(),
163 bot_name: "Rust Commit Tracker".to_string(),
164 bot_avatar_url: "https://i.imgur.com/on47Qk9.png".to_string(),
165 },
166 monitoring: MonitoringConfig {
167 commits_url: "https://commits.facepunch.com/?format=json".to_string(),
168 check_interval_secs: 50,
169 },
170 appearance: AppearanceConfig {
171 embed_color: "#CD412B".to_string(), footer_icon_url: "https://i.imgur.com/on47Qk9.png".to_string(),
173 },
174 database: DatabaseConfig {
175 url: "sqlite:commits.db".to_string(),
176 cleanup_keep_last: 1000,
177 },
178 }
179 }
180}