diff --git a/src/cookies.rs b/src/cookies.rs index dd49781..573c4c3 100644 --- a/src/cookies.rs +++ b/src/cookies.rs @@ -9,4 +9,4 @@ pub mod user { pub const USER_AVATAR_COOKIE: &str = "user_avatar"; } pub const LANG_COOKIE: &str = "lang"; -pub const WELCOMED_COOKIE: &str = "welcomed"; \ No newline at end of file +pub const WELCOMED_COOKIE: &str = "welcomed"; diff --git a/src/main.rs b/src/main.rs index 2c62483..3c23adb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,17 @@ #[macro_use] extern crate rocket; +use clap::{Parser, Subcommand}; use poise::serenity_prelude::Http; -use rocket::{fs::{relative, FileServer}, Rocket, Ignite}; +use rocket::{ + fs::{relative, FileServer}, + Ignite, Rocket, +}; use rocket_dyn_templates::{tera, Template}; use std::{collections::HashMap, env}; -use clap::{Parser, Subcommand}; mod models; -use models::{Settings, Database}; +use models::{Database, Settings}; mod utils; @@ -52,13 +55,14 @@ async fn main() { } let http = http(); load_database() - .load_legacy(&http).await + .load_legacy(&http) + .await .expect("Failed to load legacy submissions"); - }, + } }, None => { rocket().await.expect("Failed to launch rocket"); - }, + } } } @@ -86,7 +90,16 @@ async fn rocket() -> Result, rocket::Error> { .manage(load_database()) .mount( "/", - routes![get_challenge, get_guilds, get_user, login, post_login, success, logout, testing], + routes![ + get_challenge, + get_guilds, + get_user, + login, + post_login, + success, + logout, + testing + ], ) .mount("/css", FileServer::from(relative!("styles/css"))) .mount("/", FileServer::from(relative!("assets")).rank(2)) @@ -108,12 +121,12 @@ async fn rocket() -> Result, rocket::Error> { ); engines.tera.register_filter( "furigana", - move |value: &Value, args: &HashMap| { - furigana_filter(value, args) - }, + move |value: &Value, args: &HashMap| furigana_filter(value, args), ) })) - .ignite().await + .ignite() + .await .unwrap() - .launch().await + .launch() + .await } diff --git a/src/models/challenge.rs b/src/models/challenge.rs index 758b435..079c5fe 100644 --- a/src/models/challenge.rs +++ b/src/models/challenge.rs @@ -35,7 +35,7 @@ impl Challenge { .expect("Couldn't find challenge file"); let root = parse_document( &arena, - &(challenge_text + &(challenge_text // comrak can't find frontmatter if there's only frontmatter and no newline at end // TODO: Open issue in comrak + "\n"), @@ -53,8 +53,9 @@ impl Challenge { let mut html = vec![]; format_html(root, &ComrakOptions::default(), &mut html) .expect("Failed to format HTML"); - challenge.text = Some(furigana_to_html(&gh_emoji::Replacer::new() - .replace_all(&String::from_utf8(html).unwrap()))); + challenge.text = Some(furigana_to_html( + &gh_emoji::Replacer::new().replace_all(&String::from_utf8(html).unwrap()), + )); challenge } else { panic!("No frontmatter!") diff --git a/src/models/database.rs b/src/models/database.rs index 2c3522c..93bbfb4 100644 --- a/src/models/database.rs +++ b/src/models/database.rs @@ -1,13 +1,13 @@ use std::collections::HashSet; -use std::{fs::File, io::Read, collections::HashMap, path::Path}; +use std::{collections::HashMap, fs::File, io::Read, path::Path}; -use poise::serenity_prelude::{SerenityError, Http}; +use derive_more::From; +use poise::serenity_prelude::{Http, SerenityError}; use r2d2::{Pool, PooledConnection}; -use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::rusqlite::{self, params}; -use derive_more::From; +use r2d2_sqlite::SqliteConnectionManager; -use crate::{utils::get_challenge_number, models::User}; +use crate::{models::User, utils::get_challenge_number}; use super::{LegacySubmission, Submission}; @@ -38,9 +38,7 @@ impl Database { Path::new(DATABASE_FILENAME).exists() } - pub fn new( - testing: bool, - ) -> Result { + pub fn new(testing: bool) -> Result { let connection_manager = if testing { SqliteConnectionManager::memory() } else { @@ -77,7 +75,8 @@ impl Database { } pub fn has_submitted(&self, user_id: u64) -> Result { - Ok(self.conn()? + Ok(self + .conn()? .prepare("SELECT 1 FROM User WHERE id = ?1 LIMIT 1")? .query_row(params![user_id], |_row| Ok(())) .is_ok()) @@ -103,38 +102,47 @@ impl Database { // their username/discriminator since their previous submission match archived_users.get(&id) { Some(User { deleted, .. }) => { - archived_users.insert(id, User { + archived_users.insert( id, - avatar: None, - deleted: *deleted, - ..User::from_username(&legacy.username) - }); - }, - None => match User::fetch(http, id).await { - Ok(User { deleted: true, .. }) => { - archived_users.insert(id, User { + User { id, avatar: None, - deleted: true, + deleted: *deleted, ..User::from_username(&legacy.username) - }); - }, + }, + ); + } + None => match User::fetch(http, id).await { + Ok(User { deleted: true, .. }) => { + archived_users.insert( + id, + User { + id, + avatar: None, + deleted: true, + ..User::from_username(&legacy.username) + }, + ); + } Ok(user) => { conn.execute( "INSERT INTO User(id, name, discriminator, avatar, deleted) VALUES (?1, ?2, ?3, ?4, ?5)", params![user.id, user.name, user.discriminator, user.avatar, user.deleted] ).map_err(DatabaseError::Rusqlite)?; - }, + } Err(error) if error.to_string().eq("Unknown User") => { // This will also be called in the case of an invalid user ID println!("Failed to fetch user {id}, adding to archive"); - archived_users.insert(id, User { + archived_users.insert( id, - avatar: None, - deleted: false, - ..User::from_username(&legacy.username) - }); - }, + User { + id, + avatar: None, + deleted: false, + ..User::from_username(&legacy.username) + }, + ); + } Err(error) => return Err(LoadLegacyError::Serenity(error)), }, }; @@ -160,8 +168,11 @@ impl Database { } Ok(()) } - - pub fn get_challenge_user_data(&self, challenge: u32) -> Result<(Vec, HashMap)> { + + pub fn get_challenge_user_data( + &self, + challenge: u32, + ) -> Result<(Vec, HashMap)> { let submissions = self.get_submissions(challenge)?; let users = self.get_users({ let mut user_ids = HashSet::new(); @@ -174,7 +185,8 @@ impl Database { } pub fn get_submissions(&self, challenge: u32) -> Result> { - Ok(self.conn()? + Ok(self + .conn()? .prepare("SELECT author_id, timestamp, image FROM Submission WHERE challenge = ?1")? .query_map(params![challenge], |row| { Ok(Submission { @@ -188,7 +200,8 @@ impl Database { } pub fn get_user_by_name(&self, name: &str) -> Result> { - Ok(self.conn()? + Ok(self + .conn()? .prepare("SELECT id, discriminator, avatar, deleted FROM User where name = ?1")? .query_row(params![name], |row| { Ok(Some(User { @@ -202,8 +215,10 @@ impl Database { } pub fn get_submissions_by_user_name(&self, name: &str) -> Result> { - Ok(self.conn()? - .prepare(" + Ok(self + .conn()? + .prepare( + " SELECT author_id, timestamp, image, challenge FROM Submission s JOIN User u ON s.author_id = u.id @@ -227,16 +242,23 @@ impl Database { .iter() // u64 must be converted to String for templates .map(|id| -> Result<(String, User)> { - match conn.prepare("SELECT name, discriminator, avatar, deleted FROM User WHERE id = ?1") { - Ok(mut statement) => 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)?, - deleted: row.get(3)?, - })) - }).map_err(DatabaseError::Rusqlite), + match conn + .prepare("SELECT name, discriminator, avatar, deleted FROM User WHERE id = ?1") + { + Ok(mut statement) => 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)?, + deleted: row.get(3)?, + }, + )) + }) + .map_err(DatabaseError::Rusqlite), Err(error) => Err(DatabaseError::Rusqlite(error)), } }) @@ -255,4 +277,4 @@ impl Database { // For new submissions only todo!() } -} \ No newline at end of file +} diff --git a/src/models/mod.rs b/src/models/mod.rs index a2aa09a..dcf6dc8 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,5 @@ mod user; -pub use user::{Username, User, SessionUser}; +pub use user::{SessionUser, User, Username}; mod challenge; pub use challenge::Challenge; @@ -11,4 +11,4 @@ mod submission; pub use submission::*; mod database; -pub use database::{Database, DatabaseError}; \ No newline at end of file +pub use database::{Database, DatabaseError}; diff --git a/src/models/settings.rs b/src/models/settings.rs index 0e3e36d..2bce1bc 100644 --- a/src/models/settings.rs +++ b/src/models/settings.rs @@ -51,11 +51,13 @@ 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_url().map(|icon| if icon.contains("/a_") { - // serenity only gives non-animated URL - icon.replace("webp", "gif") - } else { - 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 index 46795a5..6e14072 100644 --- a/src/models/submission/legacy_submission.rs +++ b/src/models/submission/legacy_submission.rs @@ -1,6 +1,6 @@ -use chrono::{Utc, TimeZone}; -use serde::Deserialize; +use chrono::{TimeZone, Utc}; use derive_more::From; +use serde::Deserialize; use super::Submission; @@ -28,7 +28,8 @@ pub enum LegacySubmissionParseError { impl LegacySubmission { pub fn parse(&self, challenge: u32) -> Result, LegacySubmissionParseError> { let author_id = self.id; - Ok(self.images + Ok(self + .images .iter() .map(|image| { Submission { @@ -37,20 +38,25 @@ impl LegacySubmission { // 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('.').next()? - // 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 + // Get filename without extension + .split('.') + .next()? + // 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(), challenge, @@ -58,4 +64,4 @@ impl LegacySubmission { }) .collect()) } -} \ No newline at end of file +} diff --git a/src/models/submission/mod.rs b/src/models/submission/mod.rs index 3263442..55732ba 100644 --- a/src/models/submission/mod.rs +++ b/src/models/submission/mod.rs @@ -2,4 +2,4 @@ mod submission; pub use submission::Submission; mod legacy_submission; -pub use legacy_submission::LegacySubmission; \ No newline at end of file +pub use legacy_submission::LegacySubmission; diff --git a/src/models/submission/submission.rs b/src/models/submission/submission.rs index df66e36..b8b4475 100644 --- a/src/models/submission/submission.rs +++ b/src/models/submission/submission.rs @@ -1,4 +1,4 @@ -use chrono::{Utc, DateTime}; +use chrono::{DateTime, Utc}; use serde::Serialize; // Challenge submission @@ -9,7 +9,7 @@ use serde::Serialize; 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. + // 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. @@ -18,4 +18,4 @@ pub struct Submission { // Not necessary for challenge pages, // but needed for user profile pages pub challenge: u32, -} \ No newline at end of file +} diff --git a/src/models/user/mod.rs b/src/models/user/mod.rs index 25432d4..76d5cd1 100644 --- a/src/models/user/mod.rs +++ b/src/models/user/mod.rs @@ -5,7 +5,7 @@ mod tests; use chrono::Utc; use derive_more::From; -use poise::serenity_prelude::{self, UserId, Http}; +use poise::serenity_prelude::{self, Http, UserId}; use regex::Regex; use reqwest::StatusCode; use rocket::http::{Cookie, CookieJar}; @@ -46,7 +46,9 @@ impl Username for User { } fn is_name_deleted(name: &str) -> bool { - Regex::new(r"Deleted User [a-f0-9]{8}").unwrap().is_match(name) + Regex::new(r"Deleted User [a-f0-9]{8}") + .unwrap() + .is_match(name) } impl User { @@ -54,10 +56,7 @@ impl User { let (name, discriminator) = { let mut iter = username.split('#'); let name = iter.next().unwrap().to_owned(); - let discriminator = iter - .next() - .map(|str| str.parse().unwrap()) - .unwrap_or(0); + let discriminator = iter.next().map(|str| str.parse().unwrap()).unwrap_or(0); (name, discriminator) }; Self { @@ -86,12 +85,19 @@ impl User { "https://cdn.discordapp.com/avatars/{}/{}.{}?size=1024", self.id, avatar, - if avatar.starts_with("a_") { "gif" } else { "webp" } + 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), + None => format!( + "https://cdn.discordapp.com/embed/avatars/{}.png", + self.discriminator % 5 + ), } } } diff --git a/src/models/user/tests.rs b/src/models/user/tests.rs index 8376850..d7f2412 100644 --- a/src/models/user/tests.rs +++ b/src/models/user/tests.rs @@ -27,4 +27,4 @@ fn is_name_deleted() { assert!(!is_name_deleted("Deleted User Ce34a7da")); // capital letter in hex assert!(!is_name_deleted("Deleted User ce34a7d")); // hex too short assert!(!is_name_deleted("Deleted User")); // no hex -} \ No newline at end of file +} diff --git a/src/routes/get_challenge.rs b/src/routes/get_challenge.rs index 4368c9b..6ba408e 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, SessionUser, Database}, + models::{Challenge, Database, SessionUser, Settings}, utils::AcceptLanguage, }; diff --git a/src/routes/get_guilds.rs b/src/routes/get_guilds.rs index a89216a..44852fc 100644 --- a/src/routes/get_guilds.rs +++ b/src/routes/get_guilds.rs @@ -1,6 +1,6 @@ use poise::serenity_prelude::Http; -use rocket::{http::CookieJar, State}; use rocket::response::stream::{Event, EventStream}; +use rocket::{http::CookieJar, State}; use crate::{cookies::user::USER_ID_COOKIE, models::Settings}; diff --git a/src/routes/get_user.rs b/src/routes/get_user.rs index e92d2a3..9856336 100644 --- a/src/routes/get_user.rs +++ b/src/routes/get_user.rs @@ -1,13 +1,15 @@ use std::ops::Deref; - -use rocket::{http::{CookieJar, Status}, State}; +use rocket::{ + http::{CookieJar, Status}, + State, +}; use rocket_dyn_templates::{context, Template}; use crate::{ cookies::LANG_COOKIE, i18n::DEFAULT as DEFAULT_LANG, - models::{Settings, SessionUser, Database, DatabaseError}, + models::{Database, DatabaseError, SessionUser, Settings}, utils::AcceptLanguage, }; @@ -21,11 +23,13 @@ pub async fn get_user( ) -> Result { let profile_user = match database.get_user_by_name(&user) { Ok(profile_user) => profile_user, - Err(DatabaseError::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => return Err(Status::NotFound), + Err(DatabaseError::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => { + return Err(Status::NotFound) + } Err(error) => { eprintln!("{:?}", error); return Err(Status::InternalServerError); - }, + } }; Ok(Template::render( "user", @@ -41,4 +45,4 @@ pub async fn get_user( user: SessionUser::get(cookies).await.unwrap(), }, )) -} \ No newline at end of file +} diff --git a/src/routes/post_login.rs b/src/routes/post_login.rs index 568a9e5..7b9cfdc 100644 --- a/src/routes/post_login.rs +++ b/src/routes/post_login.rs @@ -5,7 +5,10 @@ use rocket::{ response::Redirect, }; -use crate::{cookies::{token::TOKEN_EXPIRE_COOKIE, WELCOMED_COOKIE}, models::SessionUser}; +use crate::{ + cookies::{token::TOKEN_EXPIRE_COOKIE, WELCOMED_COOKIE}, + models::SessionUser, +}; #[derive(FromForm)] pub struct Login<'r> { diff --git a/src/routes/testing.rs b/src/routes/testing.rs index f84bcf5..9b5cd59 100644 --- a/src/routes/testing.rs +++ b/src/routes/testing.rs @@ -19,7 +19,8 @@ pub async fn testing(cookies: &CookieJar<'_>, http: &State) -> String { .await .expect("Failed to get logged in user data") .expect("No logged in user") - .0.id + .0 + .id ) .await .expect("Failed to fetch user in server") diff --git a/src/utils/challenge.rs b/src/utils/challenge.rs index d36ae66..02a76bd 100644 --- a/src/utils/challenge.rs +++ b/src/utils/challenge.rs @@ -18,4 +18,4 @@ pub fn get_challenge_number() -> i32 { } } max -} \ No newline at end of file +} diff --git a/src/utils/furigana.rs b/src/utils/furigana.rs index 749ca08..5000e30 100644 --- a/src/utils/furigana.rs +++ b/src/utils/furigana.rs @@ -12,14 +12,19 @@ pub fn furigana_to_html(text: &str) -> String { // Curly brace literals \{ need to have their backslash escaped as \\{ // TODO: Modify so only wraps continuous sections of furigana let re = Regex::new(r"\[([^\]]*)\]\{([^\\}]*)}").unwrap(); - format!("{}", re.replace_all(text, "$1($2)")) + format!( + "{}", + re.replace_all(text, "$1($2)") + ) } pub fn furigana_filter(value: &Value, _args: &HashMap) -> tera::Result { if value.is_null() { return Ok(Value::String("".to_string())); } - Ok(Value::String(furigana_to_html(value.as_str().expect("The furigana input must be a string")))) + Ok(Value::String(furigana_to_html( + value.as_str().expect("The furigana input must be a string"), + ))) } #[cfg(test)] @@ -36,4 +41,4 @@ mod tests { " ); } -} \ No newline at end of file +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5702a3b..7f141d4 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -8,4 +8,4 @@ mod challenge; pub use challenge::*; mod furigana; -pub use furigana::*; \ No newline at end of file +pub use furigana::*;