Something went wrong. Try again.
advent of atproto
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328pub 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 rust_embed::EmbeddedFile;use serde_json::json;use sqlx::PgPool;use std::str::Utf8Error;use thiserror::Error;
#[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<String> here is the did of the user. If none only show part one no matter what ViewChallenge(Option<String>),
//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,}
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<Option<EmbeddedFile>, 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, // } // }
/// The text Markdown for challenge 1 fn markdown_text_part_one( &self, verification_code: Option<String>, ) -> Result<String, AdventError> { 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 handlebar_rendered = reg.render_template(day_one_text, &json!({"code": code}))?;
Ok( markdown::to_html_with_options(&handlebar_rendered, &get_markdown_options()) .unwrap(), ) } } }
/// The text Markdown for challenge 2, could be None fn markdown_text_part_two( &self, verification_code: Option<String>, ) -> Result<Option<String>, 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 handlebar_rendered = reg.render_template(day_two_text, &json!({"code": code}))?;
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<bool, AdventError> { 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: String, ) -> Result<Option<ChallengeProgress>, AdventError> { let day = self.day(); Ok(sqlx::query_as::<_, ChallengeProgress>( "SELECT * FROM challenges WHERE user_did = $1 AND day = $2", ) .bind(did) .bind(day as i16) .fetch_optional(self.pool()) .await?) }
async fn start_challenge(&self, did: String, part: AdventPart) -> Result<String, AdventError> { let code = get_random_token(); match part { AdventPart::One => sqlx::query( "INSERT INTO challenges (user_did, day, time_started, verification_code_one) VALUES ($1, $2, NOW(), $3) ON CONFLICT (user_did, day) DO UPDATE SET verification_code_one = $3 WHERE challenges.user_did = $1 AND challenges.day = $2", ), //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.clone()) .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<String>, ) -> Result<CompletionStatus, AdventError> { 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<String>, ) -> Result<ChallengeCheckResponse, AdventError>;
///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<String>, ) -> Result<ChallengeCheckResponse, AdventError> { 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 pleas be careful what you put in there allow_dangerous_html: true, ..Default::default() }, }}