pub mod challenges; pub mod day; use crate::advent::day::Day; use crate::assets::ChallengesMarkdown; use crate::models::db_models::ChallengeProgress; use async_trait::async_trait; use handlebars::{Handlebars, RenderError}; use markdown::{CompileOptions, Options}; use rand::distr::{Alphanumeric, SampleString}; use rand::seq::IndexedRandom; use rust_embed::EmbeddedFile; use serde_json::json; use sqlx::PgPool; use std::str::Utf8Error; use thiserror::Error; pub fn get_implemented_days() -> Vec { let days_from_env = std::env::var("IMPLEMENTED_DAYS").unwrap_or_else(|_| "1".to_string()); let max_day: u8 = days_from_env.parse().unwrap_or(1); (1..=max_day).collect() } #[derive(Debug, Error)] pub enum AdventError { #[error("Database error: {0}")] Database(#[from] sqlx::Error), #[error("Io error: {0}")] Io(#[from] std::io::Error), #[error("Invalid day: {0}. Day must be between 1 and 25")] InvalidDay(i32), #[error("This challenge only has a single challenge")] NoPartTwo, #[error("UTF-8 error: {0}")] Utf8Error(#[from] Utf8Error), #[error("Render error: {0}")] RenderError(#[from] RenderError), #[error("This was not designed to happen: {0}")] ShouldNotHappen(String), } pub enum AdventAction { //If Part one is done it shows both, if not it only shows part one // the Option here is the did of the user. If none only show part one no matter what ViewChallenge(Option), //The strings here are the users did's StartPartOne(String), StartPartTwo(String), //TODO should I have like SubmitPartOne(String) and it's the code? //These actions will be locked behind logged in SubmitPartOne, SubmitPartTwo, } pub enum AdventPart { One, Two, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum CompletionStatus { ///None of the day's challenges have been completed None, ///PartOne of the day's challenges has been completed PartOne, ///PartTwo of the day's challenges has been completed //i dont think this was needed // PartTwo, ///Both of the day's challenges have been completed Both, } pub enum ChallengeCheckResponse { Correct, ///Error message on why it was incorrect Incorrect(String), } pub enum AdventActionResult { ShowPartOne, // If partone is completed, this will be shown ShowPartTwo, CorrectSubmission(AdventPart), IncorrectSubmission, Completed, Error(String), } #[async_trait] pub trait AdventChallenge { /// The db pool in case the challenge needs extra access fn pool(&self) -> &PgPool; /// The day of the challenge 1-25 fn day(&self) -> Day; fn get_day_markdown_file(&self, part: AdventPart) -> Result, AdventError> { let day = self.day().to_string().to_ascii_lowercase(); let path = match part { AdventPart::One => format!("{day}/part_one.md"), AdventPart::Two => format!("{day}/part_two.md"), }; match ChallengesMarkdown::get(path.as_str()) { None => { log::error!("Missing the part one challenge file for day: {}", day); Ok(None) } Some(day_one_file) => Ok(Some(day_one_file)), } } /// Does the day have a part two challenge? fn has_part_two(&self) -> bool; //Commenting this out and just going leave it to who makes the impl. This is less code to write, // but really it's a faster response to just have the author put true or false // // { // match self.get_day_markdown_file(AdventPart::Two) { // Ok(_) => true, // Err(_) => false, // } // } /// Does part one require manual verification code input from the user? /// Default is false (verification happens automatically via backend) fn requires_manual_verification_part_one(&self) -> bool { false } /// Does part two require manual verification code input from the user? /// Default is false (verification happens automatically via backend) fn requires_manual_verification_part_two(&self) -> bool { false } /// Does this challenge's part two have its own submit mechanism? /// Default is false (render the normal submit button) fn custom_submit_part_two(&self) -> bool { false } /// The text Markdown for challenge 1 async fn markdown_text_part_one( &self, verification_code: Option, additional_context: Option<&serde_json::Value>, ) -> Result { let day = self.day(); //May acutally leave this unwrap or put a panic in case it doesn't exist since it is needed to have a part one match self.get_day_markdown_file(AdventPart::One)? { None => { log::error!("Missing the part one challenge file for day: {}", day); Ok("Someone let the admins know this page is missing. No, this is not an Easter egg, it's an actual bug".to_string()) } Some(day_one_file) => { //TODO probably should be a shared variable, but prototyping let reg = Handlebars::new(); let day_one_text = std::str::from_utf8(day_one_file.data.as_ref())?; let code = verification_code.unwrap_or_else(|| "Login to get a code".to_string()); let mut context = json!({"code": code}); if let Some(serde_json::Value::Object(map)) = additional_context { if let serde_json::Value::Object(ref mut ctx_map) = context { ctx_map.extend(map.iter().map(|(k, v)| (k.clone(), v.clone()))); } } let handlebar_rendered = reg.render_template(day_one_text, &context)?; Ok( markdown::to_html_with_options(&handlebar_rendered, &get_markdown_options()) .unwrap(), ) } } } /// The text Markdown for challenge 2, could be None async fn markdown_text_part_two( &self, verification_code: Option, additional_context: Option<&serde_json::Value>, ) -> Result, AdventError> { match self.get_day_markdown_file(AdventPart::Two)? { None => Ok(None), Some(day_two_file) => { //TODO probably should be a shared variable, but prototyping let reg = Handlebars::new(); let day_two_text = std::str::from_utf8(day_two_file.data.as_ref())?; let code = verification_code.unwrap_or_else(|| "Login to get a code".to_string()); let mut context = json!({"code": code}); if let Some(serde_json::Value::Object(map)) = additional_context { if let serde_json::Value::Object(ref mut ctx_map) = context { ctx_map.extend(map.iter().map(|(k, v)| (k.clone(), v.clone()))); } } let handlebar_rendered = reg.render_template(day_two_text, &context)?; Ok(Some( markdown::to_html_with_options(&handlebar_rendered, &get_markdown_options()) .unwrap(), )) } } } /// Checks to see if the day's challenge had been started for the user async fn day_started(&self, did: &str) -> Result { let exists = sqlx::query_scalar::<_, i64>( "SELECT id FROM challenges WHERE user_did = $1 AND day = $2 LIMIT 1", ) .bind(did) .bind(self.day() as i16) .fetch_optional(self.pool()) .await?; Ok(exists.is_some()) } async fn get_days_challenge( &self, did: &str, ) -> Result, AdventError> { Ok(sqlx::query_as::<_, ChallengeProgress>( "SELECT * FROM challenges WHERE user_did = $1 AND day = $2", ) .bind(did) .bind(self.day() as i16) .fetch_optional(self.pool()) .await?) } /// Hook for challenges to build additional context before the DB write. /// Override this to store extra data (e.g. an at_uri) in the additional_context column. async fn build_additional_context( &self, _did: &str, _part: &AdventPart, _code: &str, ) -> Result, AdventError> { Ok(None) } async fn start_challenge(&self, did: String, part: AdventPart) -> Result { let code = get_random_token(); let additional_context = self.build_additional_context(&did, &part, &code).await?; match part { AdventPart::One => { sqlx::query( "INSERT INTO challenges (user_did, day, time_started, verification_code_one, additional_context) VALUES ($1, $2, NOW(), $3, $4) ON CONFLICT (user_did, day) DO UPDATE SET verification_code_one = $3, additional_context = COALESCE($4, challenges.additional_context) WHERE challenges.user_did = $1 AND challenges.day = $2", ) .bind(&did) .bind(self.day() as i16) .bind(&code) .bind(&additional_context) .execute(self.pool()) .await?; } //TODO just going leave these as an update. It should never ideally be an insert AdventPart::Two => { sqlx::query( "UPDATE challenges SET verification_code_two = $3 WHERE challenges.user_did = $1 AND challenges.day = $2", ) .bind(&did) .bind(self.day() as i16) .bind(&code) .execute(self.pool()) .await?; } } Ok(code) } /// Marks the challenge as completed. async fn complete_part_one(&self, did: String) -> Result<(), AdventError> { sqlx::query( "UPDATE challenges SET time_challenge_one_completed = COALESCE(time_challenge_one_completed, NOW()) WHERE user_did = $1 AND day = $2", ) .bind(did) .bind(self.day() as i16) .execute(self.pool()) .await?; Ok(()) } /// Marks the challenge as completed. async fn complete_part_two(&self, did: String) -> Result<(), AdventError> { sqlx::query( "UPDATE challenges SET time_challenge_two_completed = COALESCE(time_challenge_two_completed, NOW()) WHERE user_did = $1 AND day = $2", ) .bind(did) .bind(self.day() as i16) .execute(self.pool()) .await?; Ok(()) } async fn get_completed_status( &self, did: Option, ) -> Result { match did { None => Ok(CompletionStatus::None), Some(did) => { let day = self.day() as i32; let result = sqlx::query!( "SELECT time_challenge_one_completed, time_challenge_two_completed FROM challenges WHERE user_did = $1 AND day = $2", did, day ) .fetch_optional(self.pool()) .await?; Ok(match result { None => CompletionStatus::None, Some(row) => match ( row.time_challenge_one_completed, row.time_challenge_two_completed, ) { (None, None) => CompletionStatus::None, (Some(_), None) => CompletionStatus::PartOne, (Some(_), Some(_)) => CompletionStatus::Both, _ => panic!( "This should never happen as in part one shouldn't be not done but 2 is" ), }, }) } } } ///This is where the magic happens, aka logic to check if the user got it. Verification code is optional cause sometiems you may need to find it somewhere, sometimes maybe the backend does async fn check_part_one( &self, did: String, verification_code: Option, ) -> Result; ///This is where the magic happens, aka logic to check if the user got it. Verification code is optional cause sometiems you may need to find it somewhere, sometimes maybe the backend does /// part two does have a hard error if its called and there is not a part 2 async fn check_part_two( &self, _did: String, _verification_code: Option, ) -> Result { unimplemented!("Second day challenges are optional") } } pub fn get_random_token() -> String { let mut rng = rand::rng(); let full_code = Alphanumeric.sample_string(&mut rng, 10); let slice_one = &full_code[0..5].to_ascii_uppercase(); let slice_two = &full_code[5..10].to_ascii_uppercase(); format!("{slice_one}-{slice_two}") } fn get_markdown_options() -> Options { Options { parse: Default::default(), compile: CompileOptions { //Setting this to allow HTML in the markdown. So please be careful what you put in there allow_dangerous_html: true, ..Default::default() }, } } /// Get the globally unlocked day from the settings table. /// Returns the day number that all users are unlocked up to. pub async fn get_global_unlock_day(pool: &PgPool) -> Result { let result = sqlx::query_scalar::<_, i32>("SELECT unlocked_up_to_day FROM settings LIMIT 1") .fetch_optional(pool) .await?; Ok(result.unwrap_or(1) as u8) } /// Get completion status for all 25 days at once /// Returns a Vec of tuples (day_number, CompletionStatus) for days 1-25 pub async fn get_all_days_completion_status( pool: &PgPool, did: Option<&String>, ) -> Result, AdventError> { // If no user is logged in, return None for all days let did = match did { Some(d) => d, None => { return Ok((1..=25).map(|day| (day, CompletionStatus::None)).collect()); } }; // Query all challenge progress for this user in a single query let results = sqlx::query!( "SELECT day, time_challenge_one_completed, time_challenge_two_completed FROM challenges WHERE user_did = $1 AND day BETWEEN 1 AND 25 ORDER BY day", did ) .fetch_all(pool) .await?; // Create a map of day -> completion status let mut status_map: std::collections::HashMap = std::collections::HashMap::new(); for row in results { let day = row.day as u8; let status = match ( row.time_challenge_one_completed, row.time_challenge_two_completed, ) { (None, None) => CompletionStatus::None, (Some(_), None) => CompletionStatus::PartOne, (Some(_), Some(_)) => CompletionStatus::Both, _ => CompletionStatus::None, }; status_map.insert(day, status); } // Build the result vec for all 25 days let result: Vec<(u8, CompletionStatus)> = (1..=25) .map(|day| { let status = status_map .get(&day) .copied() .unwrap_or(CompletionStatus::None); (day, status) }) .collect(); Ok(result) } const VERY_BAD_JOKE_SETUPS: &[&str] = &[ "what do you call a cow with two legs?", "what do bees do if they need a ride?", "what do you call a monkey that loves doritos?", "why did the can crusher quit her job?", "when’s the best time to go to the dentist?", "why do seagulls fly over the sea?", "what do you call a farm that makes bad jokes?", "why do fish live in saltwater?", "what kind of streets do ghosts haunt?", "what do you call it when one cow spies on another?", "what happens when a frog’s car breaks down?", "what does a zombie vegetarian eat?", "what do you call it when a snowman throws a tantrum?", "why did the scarecrow win an award?", "what did the buffalo say when his son left?", "wait, you don’t want to hear a joke about potassium?", "how do you organize a space party?", "what did one casket say to the other casket?", ]; /// Gets a random joke from the VERY_BAD_JOKE_SETUPS array pub fn get_a_joke() -> &'static str { VERY_BAD_JOKE_SETUPS .choose(&mut rand::rng()) .unwrap_or(&"no joke") }