diff --git a/.gitignore b/.gitignore index 6ba22ed..20d5a08 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ # Added by cargo /target -.env \ No newline at end of file +.env +*.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index dbf40d8..743943a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2bc21ffc9b77e9c31e733bb7e937c11dcf6157bb74f80bf94734110aa9b9ebc" +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -49,6 +58,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check 0.9.4", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -67,6 +87,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -203,6 +229,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.6.2", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.9.3" @@ -378,9 +419,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.0" +version = "4.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +checksum = "bba77a07e4489fb41bd90e8d4201c3eb246b3c2c9ea2ba0bddd6c1d1df87db7d" dependencies = [ "clap_builder", "clap_derive", @@ -389,9 +430,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.0" +version = "4.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +checksum = "2c9b4a88bb4bc35d3d6f65a21b0f0bafe9c894fa00978de242c555ec28bea1c0" dependencies = [ "anstream", "anstyle", @@ -403,9 +444,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" dependencies = [ "heck", "proc-macro2 1.0.59", @@ -592,7 +633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ "cfg-if 1.0.0", - "hashbrown", + "hashbrown 0.12.3", "lock_api", "once_cell", "parking_lot_core", @@ -817,6 +858,18 @@ dependencies = [ "libc", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fancy-regex" version = "0.7.1" @@ -869,7 +922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.7.1", ] [[package]] @@ -1077,6 +1130,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + [[package]] name = "glob" version = "0.3.1" @@ -1132,6 +1191,25 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + [[package]] name = "heck" version = "0.4.1" @@ -1372,7 +1450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "serde", ] @@ -1534,9 +1612,19 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "pkg-config", + "vcpkg", +] [[package]] name = "line-wrap" @@ -1651,6 +1739,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1900,6 +1997,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.2" @@ -2643,6 +2749,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.3.1", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.4.0" @@ -3113,6 +3240,7 @@ version = "0.1.0" dependencies = [ "accept-language", "chrono", + "clap", "comrak", "derive_more", "dotenv", @@ -3122,10 +3250,12 @@ dependencies = [ "rocket 0.5.0-rc.3", "rocket_contrib", "rocket_dyn_templates", + "rusqlite", "sass-rocket-fairing", "serde", "serde_json", "serde_yaml", + "tokio", ] [[package]] @@ -3258,11 +3388,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "374442f06ee49c3a28a8fc9f01a2596fed7559c6b99b31279c3261778e77d84f" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", "mio 0.8.8", diff --git a/Cargo.toml b/Cargo.toml index 9f4d614..09a886a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] accept-language = "2.0.0" chrono = { version = "0.4.26", features = ["serde"] } +clap = "4.3.9" comrak = "0.18.0" derive_more = "0.99.17" dotenv = "0.15.0" @@ -17,7 +18,9 @@ reqwest = "0.11.18" rocket = { version = "=0.5.0-rc.3", features = ["secrets", "json"] } rocket_contrib = { version = "0.4.11", features = ["templates"] } rocket_dyn_templates = { version = "0.1.0-rc.3", features = ["tera"] } +rusqlite = { version = "0.29.0", features = ["chrono"] } sass-rocket-fairing = "0.2.0" serde = "1.0.163" serde_json = "1.0.96" -serde_yaml = "0.9.21" \ No newline at end of file +serde_yaml = "0.9.21" +tokio = { version = "1.29.0", features = ["macros", "rt-multi-thread"] } diff --git a/shell.nix b/shell.nix index 91c2b67..eee66fc 100644 --- a/shell.nix +++ b/shell.nix @@ -18,5 +18,6 @@ pkgs.mkShell { pkg-config openssl gettext + sqlite ]; } diff --git a/src/main.rs b/src/main.rs index ef96f4c..9125773 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,14 @@ extern crate rocket; use poise::serenity_prelude::Http; -use rocket::fs::{relative, FileServer}; +use rocket::{fs::{relative, FileServer}, Rocket, Ignite}; use rocket_dyn_templates::{tera, Template}; use sass_rocket_fairing::SassFairing; use std::{collections::HashMap, env}; +use clap::{Parser, Subcommand}; mod models; -use models::Settings; +use models::{Settings, Database}; mod utils; @@ -24,12 +25,52 @@ use crate::i18n::langs_filter; mod prelude; -#[launch] -async fn rocket() -> _ { +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Args { + #[command(subcommand)] + cmd: Option, +} + +#[derive(Subcommand)] +enum Command { + LoadLegacy, +} + +#[tokio::main] +async fn main() { dotenv::dotenv().expect("Failed to load .env file"); + let args = Args::parse(); + + match &args.cmd { + Some(cmd) => match cmd { + Command::LoadLegacy => { + if Database::file_exists() { + println!("Cannot load legacy submissions to an existing database"); + return; + } + let http = http(); + Database::new(false) + .expect("Failed to load database") + .load_legacy(&http).await + .expect("Failed to load legacy submissions"); + }, + }, + None => { + rocket().await.expect("Failed to launch rocket"); + }, + } +} + +fn http() -> Http { let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - let http = Http::new(&token); + Http::new(&token) +} + +async fn rocket() -> Result, rocket::Error> { + let http = http(); let config = rocket::Config::figment().merge(("port", 1313)).merge(( "secret_key", @@ -63,4 +104,7 @@ async fn rocket() -> _ { ) })) .attach(SassFairing::default()) + .ignite().await + .unwrap() + .launch().await } diff --git a/src/models/database.rs b/src/models/database.rs new file mode 100644 index 0000000..b0f3336 --- /dev/null +++ b/src/models/database.rs @@ -0,0 +1,153 @@ +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!() + } +} \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index 02844c2..470aeb2 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,8 +1,14 @@ mod user; -pub use user::User; +pub use user::{Username, User, SessionUser}; mod challenge; pub use challenge::Challenge; mod settings; pub use settings::Settings; + +mod submission; +pub use submission::*; + +mod database; +pub use database::Database; \ No newline at end of file diff --git a/src/models/settings.rs b/src/models/settings.rs index 2ac577b..a331404 100644 --- a/src/models/settings.rs +++ b/src/models/settings.rs @@ -50,7 +50,12 @@ pub struct Guild { impl Guild { pub async fn load(&mut self, http: &Http) -> poise::serenity_prelude::Result<()> { let server = http.get_guild(self.id).await?; - self.icon = Some(server.icon); + self.icon = Some(server.icon_url().map(|icon| if icon.contains("/a_") { + // serenity only gives non-animated URL + icon.replace("webp", "gif") + } else { + icon + })); self.name = Some(server.name); Ok(()) } diff --git a/src/models/submission/legacy_submission.rs b/src/models/submission/legacy_submission.rs new file mode 100644 index 0000000..4df96ca --- /dev/null +++ b/src/models/submission/legacy_submission.rs @@ -0,0 +1,52 @@ +use chrono::{Utc, TimeZone}; +use serde::Deserialize; +use derive_more::From; + +use super::Submission; + +#[derive(Deserialize)] +pub struct LegacySubmission { + pub id: String, + pub images: Vec, + pub username: String, +} + +#[derive(From, Debug)] +pub enum LegacySubmissionParseError { + BadAuthorId(std::num::ParseIntError), +} + +impl LegacySubmission { + pub fn parse(&self) -> Result, LegacySubmissionParseError> { + let author_id = self.id.parse()?; + Ok(self.images + .iter() + .map(|image| { + Submission { + author_id, + timestamp: { + // The last number of the filename is either a discriminator or a datestamp + // We can split apart the filename and check if it's >9999. In that case, + // it's a datestamp. + (|| image + // Get filename without extension + .split(".") + .nth(0)? + // Get last number + .split("-") + .last()? + .parse() + .ok() + // Check if discriminator or timestamp, then convert + .map(|number| if number > 9999 { + Utc.timestamp_millis_opt(number).single() + } else { + None + })?)() + }, + image: image.clone(), + } + }) + .collect()) + } +} \ No newline at end of file diff --git a/src/models/submission/mod.rs b/src/models/submission/mod.rs new file mode 100644 index 0000000..3263442 --- /dev/null +++ b/src/models/submission/mod.rs @@ -0,0 +1,5 @@ +mod submission; +pub use submission::Submission; + +mod legacy_submission; +pub use legacy_submission::LegacySubmission; \ No newline at end of file diff --git a/src/models/submission/submission.rs b/src/models/submission/submission.rs new file mode 100644 index 0000000..0ce5ccf --- /dev/null +++ b/src/models/submission/submission.rs @@ -0,0 +1,15 @@ +use chrono::{Utc, DateTime}; + +// 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" +pub struct Submission { + pub author_id: u64, + // Some fields might be empty for legacy submissions + // Starting during challenge #87, submissions have a datestamp appended to their filename. + // TODO: Determine whether this datestamp is local time or UTC + pub timestamp: Option>, + // Image path relative to submission folder. + // Thumbnail image path will be determined from this. + pub image: String, +} \ No newline at end of file diff --git a/src/models/user/mod.rs b/src/models/user/mod.rs index 37bca84..c0eeef4 100644 --- a/src/models/user/mod.rs +++ b/src/models/user/mod.rs @@ -5,14 +5,19 @@ mod tests; use chrono::Utc; use derive_more::From; +use poise::serenity_prelude::{self, UserId, Http}; use reqwest::StatusCode; use rocket::http::{Cookie, CookieJar}; use serial::*; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::cookies::{token::*, user::*}; +pub trait Username { + fn username(&self) -> String; +} + #[derive(Default, Deserialize)] pub struct User { #[serde(deserialize_with = "deserialize_id")] @@ -21,17 +26,56 @@ pub struct User { pub name: String, #[serde(deserialize_with = "deserialize_discriminator")] pub discriminator: u16, - pub avatar: String, + pub avatar: Option, } -impl User { - pub fn username(&self) -> String { +impl Username for User { + fn username(&self) -> String { if self.discriminator == 0 { return self.name.clone(); } format!("{}#{:0>4}", self.name, self.discriminator) } +} +impl User { + pub async fn fetch(http: &Http, id: u64) -> serenity_prelude::Result { + let user = UserId(id).to_user(http).await?; + Ok(Self { + id, + avatar: user.avatar, + name: user.name, + discriminator: user.discriminator, + }) + } + + fn avatar(&self) -> String { + match &self.avatar { + // https://cdn.discordapp.com/avatars/67795786229878784/e58524afe21b5058fc6f3cdc19aea8e1.webp?size=1024 + Some(avatar) => format!( + "https://cdn.discordapp.com/avatars/{}/{}.{}?size=1024", + self.id, + avatar, + if avatar.starts_with("a_") { "gif "} else { "webp" } + ), + // Archived user or user with no avatar, calculate default avatar + // https://www.reddit.com/r/discordapp/comments/au6v4e/comment/eh61dm6/ + // https://docs.rs/serenity/0.11.5/serenity/model/user/struct.User.html#method.default_avatar_url + None => format!("https://cdn.discordapp.com/embed/avatars/{}.png", self.discriminator % 5), + } + } +} + +#[derive(Default, Serialize)] +pub struct SessionUser(pub User); + +impl Username for SessionUser { + fn username(&self) -> String { + self.0.username() + } +} + +impl SessionUser { pub async fn init(token: &str, cookies: &CookieJar<'_>) -> Result { let (status, text) = { let response = reqwest::Client::new() @@ -47,7 +91,7 @@ impl User { message: text.ok(), }); } - let user: Self = serde_json::from_str(&text?)?; + let user: User = serde_json::from_str(&text?)?; cookies.add_private(Cookie::new(TOKEN_COOKIE, token.to_owned())); cookies.add_private(Cookie::new(USER_ID_COOKIE, user.id.to_string())); cookies.add_private(Cookie::new(USER_NAME_COOKIE, user.name.clone())); @@ -55,8 +99,8 @@ impl User { USER_DISCRIMINATOR_COOKIE, user.discriminator.to_string(), )); - cookies.add_private(Cookie::new(USER_AVATAR_COOKIE, user.avatar.clone())); - Ok(user) + cookies.add_private(Cookie::new(USER_AVATAR_COOKIE, user.avatar())); + Ok(Self(user)) } pub fn purge(cookies: &CookieJar<'_>) { @@ -69,12 +113,12 @@ impl User { } fn from_cookies(cookies: &CookieJar<'_>) -> Option { - Some(Self { + Some(Self(User { id: parse_cookie_value(cookies, USER_ID_COOKIE)?, name: parse_cookie_value(cookies, USER_NAME_COOKIE)?, discriminator: parse_cookie_value(cookies, USER_DISCRIMINATOR_COOKIE)?, - avatar: parse_cookie_value(cookies, USER_AVATAR_COOKIE)?, - }) + avatar: Some(parse_cookie_value(cookies, USER_AVATAR_COOKIE)?), + })) } pub async fn get(cookies: &CookieJar<'_>) -> Result, GetUserError> { diff --git a/src/models/user/serial.rs b/src/models/user/serial.rs index 466c6d9..c330e4d 100644 --- a/src/models/user/serial.rs +++ b/src/models/user/serial.rs @@ -1,6 +1,6 @@ use serde::{ser::SerializeStruct, Serialize, Serializer}; -use super::User; +use super::{User, Username}; pub fn deserialize_id<'de, D>(deserializer: D) -> Result where @@ -27,7 +27,7 @@ impl Serialize for User { state.serialize_field("id", &self.id)?; state.serialize_field("name", &self.name)?; state.serialize_field("discriminator", &self.discriminator)?; - state.serialize_field("avatar", &self.avatar)?; + state.serialize_field("avatar", &self.avatar())?; state.serialize_field("username", &self.username())?; state.end() } diff --git a/src/models/user/tests.rs b/src/models/user/tests.rs index 96f3db6..b60b5f7 100644 --- a/src/models/user/tests.rs +++ b/src/models/user/tests.rs @@ -1,4 +1,4 @@ -use super::User; +use super::{User, Username}; fn test_user(name: &str, discriminator: u16) -> User { User { diff --git a/src/routes/get_challenge.rs b/src/routes/get_challenge.rs index 8151f99..4d3dea2 100644 --- a/src/routes/get_challenge.rs +++ b/src/routes/get_challenge.rs @@ -6,7 +6,7 @@ use rocket_dyn_templates::{context, Template}; use crate::{ cookies::LANG_COOKIE, i18n::DEFAULT as DEFAULT_LANG, - models::{Challenge, Settings, User}, + models::{Challenge, Settings, SessionUser}, utils::AcceptLanguage, }; @@ -27,7 +27,7 @@ pub async fn get_challenge( .map(|cookie| vec![cookie.value().to_owned()]) .or(accept_language.0) .unwrap_or_else(|| vec![DEFAULT_LANG.to_owned()]), - user: User::get(cookies).await.unwrap(), + user: SessionUser::get(cookies).await.unwrap(), content: Challenge::get(challenge), }, ) diff --git a/src/routes/logout.rs b/src/routes/logout.rs index ef8c3c3..6680a2e 100644 --- a/src/routes/logout.rs +++ b/src/routes/logout.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, env}; use rocket::{http::CookieJar, response::Redirect}; -use crate::{cookies::token::TOKEN_COOKIE, models::User, utils::Referer}; +use crate::{cookies::token::TOKEN_COOKIE, models::SessionUser, utils::Referer}; #[get("/logout")] pub fn logout(cookies: &CookieJar<'_>, referer: Referer) -> Redirect { @@ -29,7 +29,7 @@ pub fn logout(cookies: &CookieJar<'_>, referer: Referer) -> Redirect { println!("Failed to revoke token: {:?}", error); } }); - User::purge(cookies); + SessionUser::purge(cookies); let redirect_url = referer.0.unwrap_or("/".to_owned()); Redirect::to(redirect_url) } diff --git a/src/routes/post_login.rs b/src/routes/post_login.rs index b5d389e..1dec00d 100644 --- a/src/routes/post_login.rs +++ b/src/routes/post_login.rs @@ -5,7 +5,7 @@ use rocket::{ response::Redirect, }; -use crate::{cookies::{token::TOKEN_EXPIRE_COOKIE, WELCOMED_COOKIE}, models::User}; +use crate::{cookies::{token::TOKEN_EXPIRE_COOKIE, WELCOMED_COOKIE}, models::SessionUser}; #[derive(FromForm)] pub struct Login<'r> { @@ -18,7 +18,7 @@ pub struct Login<'r> { #[post("/login", data = "")] pub async fn post_login(login: Form>, cookies: &CookieJar<'_>) -> Redirect { if (login.token_type != "Bearer" || login.scope.split("+").any(|scope| scope == "identify")) - && User::init(login.access_token, cookies).await.is_ok() + && SessionUser::init(login.access_token, cookies).await.is_ok() { cookies.add(Cookie::new( TOKEN_EXPIRE_COOKIE, diff --git a/src/routes/testing.rs b/src/routes/testing.rs index e62bbfc..f84bcf5 100644 --- a/src/routes/testing.rs +++ b/src/routes/testing.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use poise::serenity_prelude::Http; use rocket::{http::CookieJar, State}; -use crate::models::User; +use crate::models::SessionUser; #[get("/testing")] pub async fn testing(cookies: &CookieJar<'_>, http: &State) -> String { @@ -15,11 +15,11 @@ pub async fn testing(cookies: &CookieJar<'_>, http: &State) -> String { .expect("Failed to get testing guild") .member( http.deref(), - User::get(cookies) + SessionUser::get(cookies) .await .expect("Failed to get logged in user data") .expect("No logged in user") - .id + .0.id ) .await .expect("Failed to fetch user in server") diff --git a/src/utils/challenge.rs b/src/utils/challenge.rs new file mode 100644 index 0000000..d36ae66 --- /dev/null +++ b/src/utils/challenge.rs @@ -0,0 +1,21 @@ +use std::fs; + +pub fn get_challenge_number() -> i32 { + let paths = fs::read_dir("content/challenges").unwrap(); + let mut max = 0; + for path in paths { + let number = path + .unwrap() + .path() + .file_stem() + .unwrap() + .to_str() + .unwrap() + .parse::() + .unwrap(); + if number > max { + max = number; + } + } + max +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d39ec93..f8b78ae 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -3,3 +3,6 @@ pub use kyujitai::Kyujitai; mod headers; pub use headers::*; + +mod challenge; +pub use challenge::*; \ No newline at end of file diff --git a/templates/index.html.tera b/templates/index.html.tera index d59faee..ef3d19e 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -25,7 +25,7 @@ {% if user %}