Want to contribute? Fork me on Codeberg.org!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

153 lines
6.4 KiB

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<Self> {
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<bool> {
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::<Vec<LegacySubmission>>(&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!()
}
}