ngdp_client/commands/
keys.rs1use crate::OutputFormat;
2use clap::Subcommand;
3use serde_json;
4use std::fs;
5use std::path::PathBuf;
6use tracing::{info, warn};
7
8#[derive(Debug, Subcommand)]
9pub enum KeysCommands {
10 Update {
12 #[arg(short, long)]
14 output: Option<PathBuf>,
15
16 #[arg(short, long)]
18 force: bool,
19 },
20
21 Status,
23}
24
25pub async fn handle_keys_command(
26 command: KeysCommands,
27 format: OutputFormat,
28) -> Result<(), Box<dyn std::error::Error>> {
29 match command {
30 KeysCommands::Update { output, force } => update_keys(output, force, format).await,
31 KeysCommands::Status => show_key_status(format),
32 }
33}
34
35async fn update_keys(
36 output: Option<PathBuf>,
37 force: bool,
38 format: OutputFormat,
39) -> Result<(), Box<dyn std::error::Error>> {
40 let output_path = output.unwrap_or_else(|| {
42 let config_dir = dirs::config_dir()
43 .unwrap_or_else(|| PathBuf::from("."))
44 .join("cascette");
45 config_dir.join("WoW.txt")
46 });
47
48 if !force && output_path.exists() {
50 let metadata = fs::metadata(&output_path)?;
51 if let Ok(modified) = metadata.modified() {
52 let age = std::time::SystemTime::now()
53 .duration_since(modified)
54 .unwrap_or_default();
55
56 if age.as_secs() < 86400 {
58 info!(
59 "Key file is recent ({}h old), skipping update. Use --force to override.",
60 age.as_secs() / 3600
61 );
62 return show_key_status(format);
63 }
64 }
65 }
66
67 info!("Downloading latest TACTKeys from GitHub...");
68
69 let url = "https://raw.githubusercontent.com/wowdev/TACTKeys/master/WoW.txt";
71 let client = reqwest::Client::new();
72 let response = client.get(url).send().await?;
73
74 if !response.status().is_success() {
75 return Err(format!("Failed to download keys: HTTP {}", response.status()).into());
76 }
77
78 let content = response.text().await?;
79
80 info!("Processing key file...");
81
82 let mut key_count = 0;
84 let mut new_keys = 0;
85 let existing_keys = if output_path.exists() {
86 fs::read_to_string(&output_path).ok()
87 } else {
88 None
89 };
90
91 let mut valid_lines = Vec::new();
93 for line in content.lines() {
94 let line = line.trim();
95
96 if line.is_empty() || line.starts_with('#') {
98 valid_lines.push(line.to_string());
99 continue;
100 }
101
102 let parts: Vec<&str> = line.split_whitespace().collect();
104 if parts.len() >= 2 {
105 if parts[0].len() == 16 && parts[0].chars().all(|c| c.is_ascii_hexdigit()) {
107 if parts[1].len() == 32 && parts[1].chars().all(|c| c.is_ascii_hexdigit()) {
109 key_count += 1;
110
111 if let Some(ref existing) = existing_keys {
113 if !existing.contains(parts[0]) {
114 new_keys += 1;
115 }
116 } else {
117 new_keys += 1;
118 }
119
120 valid_lines.push(line.to_string());
121 } else {
122 warn!("Invalid key value for {}: {}", parts[0], parts[1]);
123 }
124 } else {
125 warn!("Invalid key name: {}", parts[0]);
126 }
127 }
128 }
129
130 info!("Writing key file...");
131
132 if let Some(parent) = output_path.parent() {
134 fs::create_dir_all(parent)?;
135 }
136
137 fs::write(&output_path, valid_lines.join("\n"))?;
139
140 if new_keys > 0 {
142 info!(
143 "✅ Updated key database: {} total keys ({} new)",
144 key_count, new_keys
145 );
146 } else {
147 info!("✅ Key database is up to date: {} total keys", key_count);
148 }
149 info!("📁 Key file saved to: {}", output_path.display());
150
151 Ok(())
152}
153
154fn show_key_status(format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
155 let config_dir = dirs::config_dir()
156 .unwrap_or_else(|| PathBuf::from("."))
157 .join("cascette");
158 let key_file = config_dir.join("WoW.txt");
159
160 if !key_file.exists() {
161 warn!("No key file found at {}", key_file.display());
162 info!("Run 'ngdp keys update' to download the latest keys");
163 return Ok(());
164 }
165
166 let content = fs::read_to_string(&key_file)?;
167
168 let mut key_count = 0;
169 let mut key_names = Vec::new();
170
171 for line in content.lines() {
172 let line = line.trim();
173 if line.is_empty() || line.starts_with('#') {
174 continue;
175 }
176
177 let parts: Vec<&str> = line.split_whitespace().collect();
178 if parts.len() >= 2
179 && parts[0].len() == 16
180 && parts[0].chars().all(|c| c.is_ascii_hexdigit())
181 && parts[1].len() == 32
182 && parts[1].chars().all(|c| c.is_ascii_hexdigit())
183 {
184 key_count += 1;
185 if key_count <= 5 {
186 key_names.push(parts[0].to_string());
187 }
188 }
189 }
190
191 let metadata = fs::metadata(&key_file)?;
192 let file_size = metadata.len();
193 let modified = metadata
194 .modified()
195 .ok()
196 .and_then(|m| std::time::SystemTime::now().duration_since(m).ok())
197 .map(|d| format!("{}h ago", d.as_secs() / 3600))
198 .unwrap_or_else(|| "unknown".to_string());
199
200 match format {
201 OutputFormat::Json | OutputFormat::JsonPretty => {
202 let status = serde_json::json!({
203 "location": key_file.display().to_string(),
204 "total_keys": key_count,
205 "file_size_bytes": file_size,
206 "file_size_kb": file_size / 1024,
207 "last_updated": modified,
208 "sample_keys": key_names,
209 "status": "loaded"
210 });
211 let output = if matches!(format, OutputFormat::JsonPretty) {
212 serde_json::to_string_pretty(&status)?
213 } else {
214 serde_json::to_string(&status)?
215 };
216 println!("{output}");
217 }
218 _ => {
219 info!("📊 Key Database Status");
220 info!(" Location: {}", key_file.display());
221 info!(" Total keys: {}", key_count);
222 info!(" File size: {} KB", file_size / 1024);
223 info!(" Last updated: {}", modified);
224
225 if !key_names.is_empty() {
226 info!(" Sample keys:");
227 for name in key_names {
228 info!(" - {}", name);
229 }
230 if key_count > 5 {
231 info!(" ... and {} more", key_count - 5);
232 }
233 }
234 }
235 }
236
237 Ok(())
238}