Haruka041
fix: admin action tracking (broadcast/limit/lookup no longer confused) + db set_limit support
1fcc853 | use std::collections::HashMap; | |
| use std::sync::Mutex; | |
| use anyhow::Result; | |
| use super::models::{DailyStats, DownloadRecord, ForceChannel, User}; | |
| pub struct Database { | |
| config: Mutex<HashMap<String, String>>, | |
| users: Mutex<HashMap<i64, User>>, | |
| daily_counts: Mutex<HashMap<String, i64>>, | |
| records: Mutex<Vec<DownloadRecord>>, | |
| channels: Mutex<Vec<ForceChannel>>, | |
| next_record_id: Mutex<i64>, | |
| admin_actions: Mutex<HashMap<i64, String>>, | |
| } | |
| impl Database { | |
| pub fn open(_db_path: &std::path::Path) -> Result<Self> { | |
| let mut config = HashMap::new(); | |
| config.insert("default_daily_limit".into(), "3".into()); | |
| config.insert("welcome_message".into(), "".into()); | |
| config.insert("global_mute".into(), "false".into()); | |
| Ok(Self { | |
| config: Mutex::new(config), | |
| users: Mutex::new(HashMap::new()), | |
| daily_counts: Mutex::new(HashMap::new()), | |
| records: Mutex::new(Vec::new()), | |
| channels: Mutex::new(Vec::new()), | |
| next_record_id: Mutex::new(1), | |
| admin_actions: Mutex::new(HashMap::new()), | |
| }) | |
| } | |
| pub fn run_migrations(&self) -> Result<()> { | |
| Ok(()) | |
| } | |
| pub fn get_config(&self, key: &str) -> Option<String> { | |
| self.config.lock().unwrap().get(key).cloned() | |
| } | |
| pub fn set_config(&self, key: &str, value: &str) -> Result<()> { | |
| self.config.lock().unwrap().insert(key.to_string(), value.to_string()); | |
| Ok(()) | |
| } | |
| pub fn register_user(&self, user_id: i64, username: Option<&str>, first_name: Option<&str>, last_name: Option<&str>) -> Result<User> { | |
| let mut users = self.users.lock().unwrap(); | |
| if users.contains_key(&user_id) { | |
| return Ok(users[&user_id].clone()); | |
| } | |
| let default_limit: i64 = self.get_config("default_daily_limit").and_then(|v| v.parse().ok()).unwrap_or(3); | |
| let now = chrono::Utc::now().to_rfc3339(); | |
| let user = User { | |
| user_id, | |
| username: username.map(|s| s.to_string()), | |
| first_name: first_name.map(|s| s.to_string()), | |
| last_name: last_name.map(|s| s.to_string()), | |
| role: "user".to_string(), | |
| daily_limit: default_limit, | |
| format_preference: "epub".to_string(), | |
| created_at: now.clone(), | |
| last_active_at: now, | |
| }; | |
| users.insert(user_id, user.clone()); | |
| Ok(user) | |
| } | |
| pub fn get_user(&self, user_id: i64) -> Option<User> { | |
| self.users.lock().unwrap().get(&user_id).cloned() | |
| } | |
| pub fn update_last_active(&self, user_id: i64) { | |
| if let Ok(mut users) = self.users.lock() { | |
| if let Some(u) = users.get_mut(&user_id) { | |
| u.last_active_at = chrono::Utc::now().to_rfc3339(); | |
| } | |
| } | |
| } | |
| pub fn update_username(&self, user_id: i64, username: Option<&str>) { | |
| if let Ok(mut users) = self.users.lock() { | |
| if let Some(u) = users.get_mut(&user_id) { | |
| u.username = username.map(|s| s.to_string()); | |
| } | |
| } | |
| } | |
| pub fn set_user_role(&self, user_id: i64, role: &str) -> Result<()> { | |
| if let Ok(mut users) = self.users.lock() { | |
| if let Some(u) = users.get_mut(&user_id) { | |
| u.role = role.to_string(); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| pub fn set_user_daily_limit(&self, user_id: i64, limit: i64) -> Result<()> { | |
| if let Ok(mut users) = self.users.lock() { | |
| if let Some(u) = users.get_mut(&user_id) { | |
| u.daily_limit = limit; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| pub fn set_user_format(&self, user_id: i64, format: &str) -> Result<()> { | |
| if let Ok(mut users) = self.users.lock() { | |
| if let Some(u) = users.get_mut(&user_id) { | |
| u.format_preference = format.to_string(); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| pub fn count_users(&self) -> i64 { | |
| self.users.lock().unwrap().len() as i64 | |
| } | |
| pub fn get_today_download_count(&self, user_id: i64, date: &str) -> i64 { | |
| let key = format!("{}:{}", user_id, date); | |
| *self.daily_counts.lock().unwrap().get(&key).unwrap_or(&0) | |
| } | |
| pub fn increment_today_download(&self, user_id: i64, date: &str) -> Result<()> { | |
| let key = format!("{}:{}", user_id, date); | |
| *self.daily_counts.lock().unwrap().entry(key).or_insert(0) += 1; | |
| Ok(()) | |
| } | |
| pub fn count_today_downloads_total(&self, date: &str) -> i64 { | |
| self.daily_counts.lock().unwrap().iter().filter(|(k, _)| k.ends_with(&format!(":{}", date))).map(|(_, v)| *v).sum() | |
| } | |
| pub fn add_download_record(&self, user_id: i64, book_id: &str, book_name: Option<&str>, format: Option<&str>, status: &str) -> Result<i64> { | |
| let id = *self.next_record_id.lock().unwrap(); | |
| *self.next_record_id.lock().unwrap() += 1; | |
| let record = DownloadRecord { | |
| id, | |
| user_id, | |
| book_id: book_id.to_string(), | |
| book_name: book_name.map(|s| s.to_string()), | |
| format: format.map(|s| s.to_string()), | |
| status: status.to_string(), | |
| file_size: None, | |
| started_at: chrono::Utc::now().to_rfc3339(), | |
| finished_at: None, | |
| }; | |
| self.records.lock().unwrap().push(record); | |
| Ok(id) | |
| } | |
| pub fn finish_download_record(&self, record_id: i64, status: &str, file_size: Option<i64>) -> Result<()> { | |
| if let Ok(mut records) = self.records.lock() { | |
| if let Some(r) = records.iter_mut().find(|r| r.id == record_id) { | |
| r.status = status.to_string(); | |
| r.file_size = file_size; | |
| r.finished_at = Some(chrono::Utc::now().to_rfc3339()); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| pub fn count_user_downloads(&self, user_id: i64) -> i64 { | |
| self.records.lock().unwrap().iter().filter(|r| r.user_id == user_id && r.status == "completed").count() as i64 | |
| } | |
| pub fn get_force_channels(&self) -> Vec<ForceChannel> { | |
| self.channels.lock().unwrap().clone() | |
| } | |
| pub fn add_force_channel(&self, channel_id: &str, channel_name: Option<&str>, channel_type: &str, invite_link: Option<&str>, added_by: i64) -> Result<()> { | |
| let id = self.channels.lock().unwrap().len() as i64 + 1; | |
| self.channels.lock().unwrap().push(ForceChannel { | |
| id, | |
| channel_id: channel_id.to_string(), | |
| channel_name: channel_name.map(|s| s.to_string()), | |
| channel_type: channel_type.to_string(), | |
| invite_link: invite_link.map(|s| s.to_string()), | |
| added_at: chrono::Utc::now().to_rfc3339(), | |
| added_by, | |
| }); | |
| Ok(()) | |
| } | |
| pub fn remove_force_channel(&self, channel_id: &str) -> Result<()> { | |
| self.channels.lock().unwrap().retain(|c| c.channel_id != channel_id); | |
| Ok(()) | |
| } | |
| pub fn is_admin(&self, user_id: i64) -> bool { | |
| self.get_admin_ids().contains(&user_id) | |
| } | |
| pub fn get_admin_ids(&self) -> Vec<i64> { | |
| self.get_config("admin_ids").and_then(|v| serde_json::from_str(&v).ok()).unwrap_or_default() | |
| } | |
| pub fn set_admin_ids(&self, ids: &[i64]) -> Result<()> { | |
| let json = serde_json::to_string(ids).unwrap(); | |
| self.set_config("admin_ids", &json) | |
| } | |
| pub fn is_global_muted(&self) -> bool { | |
| self.get_config("global_mute").as_deref() == Some("true") | |
| } | |
| pub fn toggle_global_mute(&self) -> Result<bool> { | |
| let current = self.is_global_muted(); | |
| self.set_config("global_mute", if current { "false" } else { "true" })?; | |
| Ok(!current) | |
| } | |
| pub fn get_default_daily_limit(&self) -> i64 { | |
| self.get_config("default_daily_limit").and_then(|v| v.parse().ok()).unwrap_or(3) | |
| } | |
| pub fn set_default_daily_limit(&self, limit: i64) -> Result<()> { | |
| self.set_config("default_daily_limit", &limit.to_string()) | |
| } | |
| pub fn clean_old_stats(&self) -> Result<()> { | |
| Ok(()) | |
| } | |
| pub fn total_completed_downloads(&self) -> i64 { | |
| self.records.lock().unwrap().iter().filter(|r| r.status == "completed").count() as i64 | |
| } | |
| pub fn get_all_admin_users(&self) -> Vec<User> { | |
| let ids = self.get_admin_ids(); | |
| let users = self.users.lock().unwrap(); | |
| ids.iter().filter_map(|id| users.get(id).cloned()).collect() | |
| } | |
| pub fn set_action(&self, user_id: i64, action: &str) { | |
| self.admin_actions.lock().unwrap().insert(user_id, action.to_string()); | |
| } | |
| pub fn take_action(&self, user_id: i64) -> Option<String> { | |
| self.admin_actions.lock().unwrap().remove(&user_id) | |
| } | |
| } | |