diff --git a/Cargo.toml b/Cargo.toml index 80fabc5..ec00f5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ log = "0.4.24" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.141" shared = { path = "./shared" } -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "macros"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "macros", "json"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } bb8 = "0.9.0" diff --git a/migrations/20260317224144_add_additional_context.sql b/migrations/20260317224144_add_additional_context.sql new file mode 100644 index 0000000..624d885 --- /dev/null +++ b/migrations/20260317224144_add_additional_context.sql @@ -0,0 +1 @@ +ALTER TABLE challenges ADD COLUMN additional_context JSONB NULL; diff --git a/shared/challenges_markdown/two/part_one.md b/shared/challenges_markdown/two/part_one.md index 72438fd..851d156 100644 --- a/shared/challenges_markdown/two/part_one.md +++ b/shared/challenges_markdown/two/part_one.md @@ -26,7 +26,7 @@ This uri can be broken down into 3 parts: - `app.bsky.feed.like` is the collection - `3mhbs2cnrl22r` is the record key -Using what you learned from day 1 find the following record {generate the at://uri here} and enter the verification code +Using what you learned from day 1 find the following record `{{at_uri}}` and enter the verification code found in the record for it below diff --git a/shared/src/advent/challenges/day_two.rs b/shared/src/advent/challenges/day_two.rs index 5edbe7e..86114f1 100644 --- a/shared/src/advent/challenges/day_two.rs +++ b/shared/src/advent/challenges/day_two.rs @@ -4,9 +4,11 @@ use crate::advent::{ }; use crate::atrium::safe_check_unknown_record_parse; use crate::lexicons::codes::advent; +use crate::lexicons::record::KnownRecord; use crate::{OAuthAgentType, PasswordAgent}; use async_trait::async_trait; use atrium_api::types::Collection; +use serde_json::json; use sqlx::PgPool; pub struct DayTwo { @@ -32,28 +34,99 @@ impl AdventChallenge for DayTwo { /// We are overriding the start challenge and using extra code async fn start_challenge(&self, did: String, part: AdventPart) -> Result { let code = get_random_token(); + + // For part one, create a record via the challenge agent and store the at_uri + let additional_context: Option = match part { + AdventPart::One => { + match &self.challenge_agent { + Some(agent) => { + let agent_did = agent.did().await.ok_or_else(|| { + AdventError::ShouldNotHappen( + "Challenge agent has no DID".to_string(), + ) + })?; + + let record_data = advent::challenge::day::RecordData { + part_one: code.clone(), + part_two: None, + created_at: None, + }; + let known_record: KnownRecord = record_data.into(); + let record_value: atrium_api::types::Unknown = known_record.into(); + + // Use a unique rkey per user based on the did to avoid collisions + let rkey = did.replace(":", "_").replace(".", "_"); + + let put_result = agent + .api + .com + .atproto + .repo + .put_record( + atrium_api::com::atproto::repo::put_record::InputData { + collection: advent::challenge::Day::NSID.parse().unwrap(), + repo: agent_did.as_ref().parse().unwrap(), + rkey: rkey.parse().unwrap(), + record: record_value, + swap_commit: None, + swap_record: None, + validate: Some(false), + } + .into(), + ) + .await; + + match put_result { + Ok(output) => Some(json!({"at_uri": output.uri})), + Err(e) => { + log::error!("Failed to create challenge record via agent: {e}"); + None + } + } + } + None => { + log::warn!("No challenge agent configured, skipping record creation for day two"); + None + } + } + } + AdventPart::Two => None, + }; + 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", - ), + 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 = $4 + 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", - ), + 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?; + } } - .bind(did) - .bind(self.day() as i16) - .bind(code.clone()) - .execute(self.pool()) - .await?; + Ok(code) } + async fn check_part_one( &self, did: String, diff --git a/shared/src/advent/mod.rs b/shared/src/advent/mod.rs index 4e7475d..8814613 100644 --- a/shared/src/advent/mod.rs +++ b/shared/src/advent/mod.rs @@ -135,6 +135,7 @@ pub trait AdventChallenge { fn markdown_text_part_one( &self, verification_code: Option, + additional_context: Option<&serde_json::Value>, ) -> Result { let day = self.day(); @@ -150,8 +151,14 @@ pub trait AdventChallenge { 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, &json!({"code": code}))?; + reg.render_template(day_one_text, &context)?; Ok( markdown::to_html_with_options(&handlebar_rendered, &get_markdown_options()) @@ -165,6 +172,7 @@ pub trait AdventChallenge { 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), @@ -174,8 +182,14 @@ pub trait AdventChallenge { 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, &json!({"code": code}))?; + reg.render_template(day_two_text, &context)?; Ok(Some( markdown::to_html_with_options(&handlebar_rendered, &get_markdown_options()) diff --git a/shared/src/models/db_models.rs b/shared/src/models/db_models.rs index 42f0bc3..27830b8 100644 --- a/shared/src/models/db_models.rs +++ b/shared/src/models/db_models.rs @@ -18,4 +18,5 @@ pub struct ChallengeProgress { pub time_challenge_two_completed: Option>, pub verification_code_one: Option, pub verification_code_two: Option, + pub additional_context: Option, } diff --git a/web/src/handlers/day.rs b/web/src/handlers/day.rs index c0f63d3..c4b3845 100644 --- a/web/src/handlers/day.rs +++ b/web/src/handlers/day.rs @@ -93,7 +93,7 @@ pub async fn view_day_handler( let title = format!("at://advent - Day {}", day as u8); let part_one_text = match did_clone { None => challenge - .markdown_text_part_one(None) + .markdown_text_part_one(None, None) .map(|s| s.to_string()) .unwrap_or_else(|_| "Error loading part one".to_string()), Some(ref users_did) => match challenge.get_days_challenge(&users_did).await { @@ -103,8 +103,10 @@ pub async fn view_day_handler( .start_challenge(users_did.to_string(), AdventPart::One) .await .unwrap(); + let started = challenge.get_days_challenge(users_did).await.ok().flatten(); + let ctx = started.as_ref().and_then(|c| c.additional_context.as_ref()); challenge - .markdown_text_part_one(Some(new_code)) + .markdown_text_part_one(Some(new_code), ctx) .map(|s| s.to_string()) .unwrap_or_else(|_| "Error loading part one".to_string()) } @@ -114,13 +116,15 @@ pub async fn view_day_handler( .start_challenge(users_did.to_string(), AdventPart::One) .await .unwrap(); + let started = challenge.get_days_challenge(users_did).await.ok().flatten(); + let ctx = started.as_ref().and_then(|c| c.additional_context.as_ref()); challenge - .markdown_text_part_one(Some(new_code)) + .markdown_text_part_one(Some(new_code), ctx) .map(|s| s.to_string()) .unwrap_or_else(|_| "Error loading part one".to_string()) } Some(code) => challenge - .markdown_text_part_one(Some(code)) + .markdown_text_part_one(Some(code), current_challenge.additional_context.as_ref()) .map(|s| s.to_string()) .unwrap_or_else(|_| "Error loading part one".to_string()), }, @@ -209,7 +213,7 @@ async fn get_part_two_text( ) -> Option { let part_two_text: Option = match did_clone { None => challenge - .markdown_text_part_two(None) + .markdown_text_part_two(None, None) .map(|opt| opt.map(|s| s.to_string())) .unwrap_or(None), Some(users_did) => match challenge.get_days_challenge(&users_did).await { @@ -220,8 +224,10 @@ async fn get_part_two_text( .start_challenge(users_did.to_string(), AdventPart::Two) .await .unwrap(); + let started = challenge.get_days_challenge(&users_did).await.ok().flatten(); + let ctx = started.as_ref().and_then(|c| c.additional_context.as_ref()); challenge - .markdown_text_part_two(Some(new_code)) + .markdown_text_part_two(Some(new_code), ctx) .map(|opt| opt.map(|s| s.to_string())) .unwrap_or(None) } else { @@ -237,13 +243,15 @@ async fn get_part_two_text( .start_challenge(users_did.to_string(), AdventPart::Two) .await .unwrap(); + let started = challenge.get_days_challenge(&users_did).await.ok().flatten(); + let ctx = started.as_ref().and_then(|c| c.additional_context.as_ref()); challenge - .markdown_text_part_two(Some(new_code)) + .markdown_text_part_two(Some(new_code), ctx) .map(|opt| opt.map(|s| s.to_string())) .unwrap_or(None) } Some(code) => challenge - .markdown_text_part_two(Some(code)) + .markdown_text_part_two(Some(code), current_challenge.additional_context.as_ref()) .map(|opt| opt.map(|s| s.to_string())) .unwrap_or(None), }