Working database submission display

This commit is contained in:
Elnu 2023-06-30 15:51:14 -07:00
parent b20fa28198
commit e2a35a271b
9 changed files with 274 additions and 65 deletions

View file

@ -52,8 +52,7 @@ async fn main() {
return;
}
let http = http();
Database::new(false)
.expect("Failed to load database")
load_database()
.load_legacy(&http).await
.expect("Failed to load legacy submissions");
},
@ -64,6 +63,10 @@ async fn main() {
}
}
fn load_database() -> Database {
Database::new(false).expect("Failed to load database")
}
fn http() -> Http {
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
Http::new(&token)
@ -81,11 +84,13 @@ async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
rocket::custom(config)
.manage(Settings::new(&http).await.unwrap())
.manage(http)
.manage(load_database())
.mount(
"/",
routes![get_challenge, get_guilds, login, post_login, success, logout, testing],
)
.mount("/css", FileServer::from(relative!("styles/css")))
.mount("/", FileServer::from(relative!("assets")).rank(2))
.mount("/", FileServer::from(relative!("static")).rank(1))
.attach(Template::custom(move |engines| {
use tera::Value;

View file

@ -1,18 +1,32 @@
use std::collections::HashSet;
use std::{fs::File, io::Read, collections::HashMap, path::Path};
use poise::serenity_prelude::Http;
use rusqlite::{Connection, params};
use r2d2::{Pool, PooledConnection};
use r2d2_sqlite::SqliteConnectionManager;
use r2d2_sqlite::rusqlite::{self, params};
use derive_more::From;
use crate::{utils::get_challenge_number, models::User};
use super::{LegacySubmission, Submission};
pub struct Database {
conn: Connection,
// Must be Arc because Connection contains RefCell,
// which cannot be shared between threads safely
connection_pool: Pool<SqliteConnectionManager>,
}
const DATABASE_FILENAME: &str = "database.db";
#[derive(From, Debug)]
pub enum DatabaseError {
Rusqlite(rusqlite::Error),
Pool(r2d2::Error),
}
type Result<T> = std::result::Result<T, DatabaseError>;
impl Database {
pub fn file_exists() -> bool {
Path::new(DATABASE_FILENAME).exists()
@ -20,12 +34,14 @@ impl Database {
pub fn new(
testing: bool,
) -> rusqlite::Result<Self> {
let conn = if testing {
Connection::open_in_memory()
) -> Result<Self> {
let connection_manager = if testing {
SqliteConnectionManager::memory()
} else {
Connection::open(DATABASE_FILENAME)
}?;
SqliteConnectionManager::file(DATABASE_FILENAME)
};
let connection_pool = Pool::new(connection_manager)?;
let conn = connection_pool.get()?;
conn.execute(
"CREATE TABLE IF NOT EXISTS Submission (
id INTEGER PRIMARY KEY,
@ -46,21 +62,26 @@ impl Database {
)",
params![],
)?;
Ok(Self { conn })
Ok(Self { connection_pool })
}
pub fn has_submitted(&self, user_id: u64) -> rusqlite::Result<bool> {
Ok(self.conn
fn conn(&self) -> std::result::Result<PooledConnection<SqliteConnectionManager>, r2d2::Error> {
self.connection_pool.get()
}
pub fn has_submitted(&self, user_id: u64) -> 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<()> {
pub async fn load_legacy(&self, http: &Http) -> 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();
let conn = self.conn()?;
for n in 1..=latest_challenge {
println!("Loading legacy challenge {n}/{latest_challenge}...");
let mut file = File::open(format!("data/challenges/{n}.json")).unwrap();
@ -72,7 +93,7 @@ impl Database {
.map(|legacy| (legacy, legacy.parse().unwrap())) {
let mut already_updated = false;
for submission in submissions {
self.conn.execute(
conn.execute(
"INSERT INTO Submission(author_id, timestamp, image, challenge) VALUES (?1, ?2, ?3, ?4)",
params![
&submission.author_id,
@ -120,7 +141,7 @@ impl Database {
} else {
match User::fetch(http, submission.author_id).await {
Ok(user) => {
self.conn.execute(
conn.execute(
"INSERT INTO User(id, name, discriminator, avatar) VALUES (?1, ?2, ?3, ?4)",
params![user.id, user.name, user.discriminator, user.avatar]
)?;
@ -137,6 +158,51 @@ impl Database {
}
Ok(())
}
pub fn get_challenge_user_data(&self, challenge: u32) -> Result<(Vec<Submission>, HashMap<String, User>)> {
let submissions = self.get_submissions(challenge)?;
let users = self.get_users({
let mut user_ids = HashSet::new();
for submission in &submissions {
user_ids.insert(submission.author_id);
}
user_ids
})?;
Ok((submissions, users))
}
pub fn get_submissions(&self, challenge: u32) -> Result<Vec<Submission>> {
Ok(self.conn()?
.prepare("SELECT author_id, timestamp, image FROM Submission WHERE challenge = ?1")?
.query_map(params![challenge], |row| {
Ok(Submission {
author_id: row.get(0)?,
timestamp: row.get(1)?,
image: row.get(2)?,
})
})?
.collect::<std::result::Result<Vec<Submission>, rusqlite::Error>>()?)
}
fn get_users(&self, users: HashSet<u64>) -> Result<HashMap<String, User>> {
let conn = self.conn()?;
// Not sure why derive_more::From is unable to convert these errors
users
.iter()
// u64 must be converted to String for templates
.filter_map(|id| -> Option<Result<(String, User)>> {
match conn.prepare("SELECT name, discriminator, avatar FROM User WHERE id = ?1") {
Ok(mut statement) => Some(statement.query_row(params![id], |row| Ok((id.to_string(), User {
id: *id,
name: row.get(0)?,
discriminator: row.get(1)?,
avatar: row.get(2)?,
}))).map_err(|error| DatabaseError::Rusqlite(error))),
Err(error) => Some(Err(DatabaseError::Rusqlite(error))),
}
})
.collect()
}
#[allow(dead_code)]
pub fn refresh_users(&self) -> rusqlite::Result<()> {

View file

@ -1,8 +1,11 @@
use chrono::{Utc, DateTime};
use serde::Serialize;
// Challenge submission
// In the legacy site version, one submission held 1 or more images.
// Now, 1 submission = 1 image, and the leaderboard count will be labeled as "participations"
#[derive(Serialize, Debug)]
pub struct Submission {
pub author_id: u64,
// Some fields might be empty for legacy submissions

View file

@ -18,7 +18,7 @@ pub trait Username {
fn username(&self) -> String;
}
#[derive(Default, Deserialize)]
#[derive(Default, Deserialize, Debug)]
pub struct User {
#[serde(deserialize_with = "deserialize_id")]
pub id: u64,

View file

@ -6,7 +6,7 @@ use rocket_dyn_templates::{context, Template};
use crate::{
cookies::LANG_COOKIE,
i18n::DEFAULT as DEFAULT_LANG,
models::{Challenge, Settings, SessionUser},
models::{Challenge, Settings, SessionUser, Database},
utils::AcceptLanguage,
};
@ -15,12 +15,16 @@ pub async fn get_challenge(
challenge: u32,
cookies: &CookieJar<'_>,
settings: &State<Settings>,
database: &State<Database>,
accept_language: AcceptLanguage,
) -> Template {
let (submissions, users) = database.get_challenge_user_data(challenge).unwrap();
Template::render(
"index",
context! {
challenge,
submissions,
users,
settings: settings.deref(),
lang: cookies
.get(LANG_COOKIE)