use std::{fs::File, io::Read, collections::HashMap, path::Path}; use poise::serenity_prelude::Http; use rusqlite::{Connection, params}; use crate::{utils::get_challenge_number, models::User}; use super::{LegacySubmission, Submission}; pub struct Database { conn: Connection, } const DATABASE_FILENAME: &str = "database.db"; impl Database { pub fn file_exists() -> bool { Path::new(DATABASE_FILENAME).exists() } pub fn new( testing: bool, ) -> rusqlite::Result { let conn = if testing { Connection::open_in_memory() } else { Connection::open(DATABASE_FILENAME) }?; conn.execute( "CREATE TABLE IF NOT EXISTS Submission ( id INTEGER PRIMARY KEY, author_id INTEGER NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, image TEXT NOT NULL, challenge INTEGER NOT NULL, FOREIGN KEY (author_id) REFERENCES User(id) )", params![], )?; conn.execute( "CREATE TABLE IF NOT EXISTS User ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, discriminator INTEGER NOT NULL, avatar TEXT )", params![], )?; Ok(Self { conn }) } pub fn has_submitted(&self, user_id: u64) -> rusqlite::Result { Ok(self.conn .prepare("SELECT 1 FROM User WHERE id = ?1 LIMIT 1")? .query_row(params![user_id], |_row| Ok(())) .is_ok()) } pub async fn load_legacy(&self, http: &Http) -> rusqlite::Result<()> { let latest_challenge = get_challenge_number(); // HashMap of archived users that are no longer sharing a server with 字ちゃん // Their historical usernames and discriminators will be used let mut archived_users = HashMap::new(); for n in 1..=latest_challenge { println!("Loading legacy challenge {n}/{latest_challenge}..."); let mut file = File::open(format!("data/challenges/{n}.json")).unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); for (legacy, submissions) in serde_json::from_str::>(&contents) .unwrap() .iter() .map(|legacy| (legacy, legacy.parse().unwrap())) { let mut already_updated = false; for submission in submissions { self.conn.execute( "INSERT INTO Submission(author_id, timestamp, image, challenge) VALUES (?1, ?2, ?3, ?4)", params![ &submission.author_id, &submission.timestamp, &submission.image, n, ] )?; let id = submission.author_id; if !self.has_submitted(id)? { println!("Fetching user {id}..."); let previously_archived = archived_users.contains_key(&id); // Parse archived user out of legacy and insert into HashMap let mut archive = || { if already_updated { return; } if previously_archived { println!("Updating archived data for user {id}"); } else { println!("Adding archived data for user {id}"); } let (name, discriminator) = { let mut iter = legacy.username.split('#'); let name = iter.next().unwrap().to_owned(); let discriminator = iter .next() .map(|str| str.parse().unwrap()) .unwrap_or(0); (name, discriminator) }; archived_users.insert(id, User { id, name, discriminator, avatar: None, }); already_updated = true; }; if previously_archived { // If it already contains the archived user, // overwrite write their data since they may have updated // their username/discriminator since their previous submission archive(); } else { match User::fetch(http, submission.author_id).await { Ok(user) => { self.conn.execute( "INSERT INTO User(id, name, discriminator, avatar) VALUES (?1, ?2, ?3, ?4)", params![user.id, user.name, user.discriminator, user.avatar] )?; }, Err(error) => { println!("Failed to fetch user {}, may update archived data: {error}", submission.author_id); archive(); }, }; } } } } } Ok(()) } #[allow(dead_code)] pub fn refresh_users(&self) -> rusqlite::Result<()> { // Periodically refresh all changable user data (name, discriminator, avatar) // Ideally this should run periodically. todo!() } #[allow(dead_code, unused_variables)] pub fn insert_submission(&self, submission: &Submission) -> rusqlite::Result<()> { // For new submissions only todo!() } }