fqnd / src /ui /telegram /database.rs
Haruka041
fix: admin action tracking (broadcast/limit/lookup no longer confused) + db set_limit support
1fcc853
Raw
History Blame Contribute Delete
8.7 kB
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)
}
}