diff --git a/Cargo.lock b/Cargo.lock index 105e85c..824d86e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2865,6 +2865,7 @@ dependencies = [ "atrium-repo", "atrium-xrpc-client", "axum", + "base64", "bb8", "bb8-redis", "handlebars", diff --git a/Cargo.toml b/Cargo.toml index 738ec1c..ddf36f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ atrium-identity = "0.1.8" atrium-xrpc-client = "0.5.15" atrium-oauth = "0.1.6" atrium-repo = "0.1.7" +base64 = "0.22" chrono = { version = "0.4", features = ["serde", "now"] } hickory-resolver = "0.24.1" dotenv = "0.15.0" diff --git a/shared/Cargo.toml b/shared/Cargo.toml index e585908..a0b3764 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -11,6 +11,7 @@ atrium-api.workspace = true atrium-identity.workspace = true atrium-oauth.workspace = true atrium-repo.workspace = true +base64.workspace = true bb8.workspace = true bb8-redis.workspace = true hickory-resolver.workspace = true diff --git a/shared/challenges_markdown/three/part_one.md b/shared/challenges_markdown/three/part_one.md index 1de2758..b519b15 100644 --- a/shared/challenges_markdown/three/part_one.md +++ b/shared/challenges_markdown/three/part_one.md @@ -1,3 +1,21 @@ This will be explaing the car format and will have a download link to a car file we create that they can download. We can create a new web handler like. /day/3/{did} and it downloads the car file. -Inside the car export is just one record with the verficiation code \ No newline at end of file +Inside the car export is just one record with the verficiation code + +TODO: detect not-logged-in and if not, don't render this button: + +Download your CAR file + + diff --git a/shared/examples/day_three_build_car.rs b/shared/examples/day_three_build_car.rs index abb1b45..a96d80e 100644 --- a/shared/examples/day_three_build_car.rs +++ b/shared/examples/day_three_build_car.rs @@ -1,16 +1,12 @@ -use shared::advent::challenges::day_three::repo::{ChallengeRecord, manufacture_car}; +use shared::advent::challenges::day_three::repo::manufacture_car; #[tokio::main] async fn main() { let car_bytes = manufacture_car( - "did:plc:somebodyyyy".parse().unwrap(), - "codes.advent.supersecret.verification", - "1337", - ChallengeRecord { - created_at: "2026-01-01T12:00:00Z".into(), - verification_code: "ABCDE-FGHIJ".to_string(), - message: "what did the muffin say to the other muffin?".into(), - }, + &("codes.advent.supersecret.verification".parse().unwrap()), + &("1337".parse().unwrap()), + "ABCDE-FGHIJ", + "what did the muffin say to the other muffin?", ) .await .expect("failed to build car"); diff --git a/shared/src/advent/challenges/day_three/day_three.rs b/shared/src/advent/challenges/day_three/day_three.rs new file mode 100644 index 0000000..d63410a --- /dev/null +++ b/shared/src/advent/challenges/day_three/day_three.rs @@ -0,0 +1,78 @@ +use handlebars::Handlebars; +use super::manufacture_car; +use crate::advent::AdventPart; +use serde_json::json; +use base64::{Engine, engine::general_purpose::STANDARD}; +use crate::OAuthAgentType; +use crate::advent::{day::Day, AdventChallenge, AdventError, ChallengeCheckResponse}; +use async_trait::async_trait; +use sqlx::PgPool; + +pub struct DayThree { + pub pool: PgPool, + pub oauth_client: Option, +} + +#[async_trait] +impl AdventChallenge for DayThree { + fn pool(&self) -> &PgPool { + &self.pool + } + + fn day(&self) -> Day { + Day::Three + } + + fn has_part_two(&self) -> bool { + false + } + + fn requires_manual_verification_part_one(&self) -> bool { + true + } + + /// inject the car right on the page :sicko: + async fn markdown_text_part_one( + &self, + verification_code: Option, + _additional_context: Option<&serde_json::Value>, + ) -> Result { + + let day_file = self.get_day_markdown_file(AdventPart::One)? + .ok_or_else(|| AdventError::ShouldNotHappen("Missing part one markdown".into()))?; + + let md_text = std::str::from_utf8(day_file.data.as_ref())?; + let code = verification_code.unwrap_or_else(|| "Login to get a code".to_string()); + + let car_base64 = if code != "Login to get a code" { + let car_bytes = manufacture_car( + &("codes.advent.supersecret.verification".parse().unwrap()), + &("1337".parse().unwrap()), + &code, + "hellooooooo", + ) + .await + .map_err(|e| AdventError::ShouldNotHappen(format!("failed to render car: {e}")))?; + STANDARD.encode(&car_bytes) + } else { + "".to_string() + }; + + let reg = Handlebars::new(); + let context = json!({ + "code": code, + "car_base64": car_base64, + }); + let rendered = reg.render_template(md_text, &context)?; + + Ok(markdown::to_html_with_options(&rendered, &crate::advent::get_markdown_options()).unwrap()) + } + + async fn check_part_one( + &self, + _did: String, + _verification_code: Option, + ) -> Result { + todo!() + } +} diff --git a/shared/src/advent/challenges/day_three/mod.rs b/shared/src/advent/challenges/day_three/mod.rs index ea590b0..afe43c6 100644 --- a/shared/src/advent/challenges/day_three/mod.rs +++ b/shared/src/advent/challenges/day_three/mod.rs @@ -1,41 +1,5 @@ +pub mod day_three; pub mod repo; +pub use day_three::DayThree; pub use repo::manufacture_car; - -use crate::OAuthAgentType; -use crate::advent::day::Day; -use crate::advent::{AdventChallenge, AdventError, ChallengeCheckResponse}; -use async_trait::async_trait; -use sqlx::PgPool; - -pub struct DayThree { - pub pool: PgPool, - pub oauth_client: Option, -} - -#[async_trait] -impl AdventChallenge for DayThree { - fn pool(&self) -> &PgPool { - &self.pool - } - - fn day(&self) -> Day { - Day::Three - } - - fn has_part_two(&self) -> bool { - false - } - - fn requires_manual_verification_part_one(&self) -> bool { - true - } - - async fn check_part_one( - &self, - _did: String, - _verification_code: Option, - ) -> Result { - todo!() - } -} diff --git a/shared/src/advent/challenges/day_three/repo.rs b/shared/src/advent/challenges/day_three/repo.rs index 0a1901e..ddeefeb 100644 --- a/shared/src/advent/challenges/day_three/repo.rs +++ b/shared/src/advent/challenges/day_three/repo.rs @@ -1,10 +1,12 @@ use std::io::Cursor; -use atrium_api::types::string::Did; +use atrium_api::types::string::{Nsid, RecordKey}; use atrium_crypto::keypair::P256Keypair; use atrium_repo::{Repository, blockstore::{CarStore, MemoryBlockStore}}; use serde::Serialize; +const CHALLENGE_DID: &str = "did:plc:3oktyyf7u4ecnvdwf3ogehpd"; + #[derive(Debug, thiserror::Error)] pub enum CarBuildError { #[error("Signing error: {0}")] @@ -18,32 +20,36 @@ pub enum CarBuildError { /// secret record type #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ChallengeRecord { - pub created_at: String, - pub verification_code: String, - pub message: String, +pub struct ChallengeRecord<'a> { + pub created_at: &'a str, + pub verification_code: &'a str, + pub message: &'a str, } /// make a single-record atproto car /// /// returns CAR bytes pub async fn manufacture_car( - did: Did, - collection: &str, - rkey: &str, - record: ChallengeRecord, + collection: &Nsid, + rkey: &RecordKey, + code: &str, + message: &str, ) -> Result, CarBuildError> { - // throwaway (fake key hello) - let keypair = P256Keypair::import(&[1u8; 32])?; - + let keypair = P256Keypair::import(&[0xAAu8; 32])?; // fake, it's ok + let did = CHALLENGE_DID.parse().unwrap(); let mut mem = MemoryBlockStore::new(); let builder = Repository::create(&mut mem, did).await?; let sig = keypair.sign(&builder.bytes())?; let mut repo = builder.finalize(sig).await?; // add record - let key = format!("{collection}/{rkey}"); + let key = format!("{}/{}", collection.as_str(), rkey.as_str()); + let record = ChallengeRecord { + created_at: "asdf", // TODO generate + verification_code: code, + message, + }; let (cb, _cid) = repo.add_raw(&key, record).await?; let sig = keypair.sign(&cb.bytes())?; cb.finalize(sig).await?; @@ -64,14 +70,10 @@ mod tests { #[tokio::test] async fn test_generate_car() { let car_bytes = manufacture_car( - "did:plc:somebodyyyy".parse().unwrap(), - "codes.advent.supersecret.verification", - "1337", - ChallengeRecord { - created_at: "2026-01-01T12:00:00Z".into(), - verification_code: "ABCDE-FGHIJ".to_string(), - message: "what did the muffin say to the other muffin?".into(), - }, + &("codes.advent.supersecret.verification".parse().unwrap()), + &("1337".parse().unwrap()), + "ABCDE-FGHIJ", + "what did the muffin say to the other muffin?", ) .await .expect("build a car"); diff --git a/web/Cargo.toml b/web/Cargo.toml index 7b96a13..5c3c8f2 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -9,9 +9,9 @@ atrium-api.workspace = true atrium-identity.workspace = true atrium-oauth.workspace = true axum.workspace = true -chrono.workspace = true bb8.workspace = true bb8-redis.workspace = true +chrono.workspace = true dotenv.workspace = true log.workspace = true redis.workspace = true diff --git a/web/src/handlers/day.rs b/web/src/handlers/day.rs index a3320a5..2583620 100644 --- a/web/src/handlers/day.rs +++ b/web/src/handlers/day.rs @@ -111,7 +111,10 @@ pub async fn view_day_handler( .markdown_text_part_one(None, None) .await .map(|s| s.to_string()) - .unwrap_or_else(|_| "Error loading part one".to_string()), + .unwrap_or_else(|e| { + log::warn!("failed to render part 1: {e}"); + "Error loading part one".to_string() + }), Some(ref users_did) => match challenge.get_days_challenge(&users_did).await { Ok(current_challenge) => match current_challenge { None => { @@ -125,7 +128,10 @@ pub async fn view_day_handler( .markdown_text_part_one(Some(new_code), ctx) .await .map(|s| s.to_string()) - .unwrap_or_else(|_| "Error loading part one".to_string()) + .unwrap_or_else(|e| { + log::warn!("failed to render part 1: {e}"); + "Error loading part one".to_string() + }) } Some(current_challenge) => match current_challenge.verification_code_one { None => { @@ -139,7 +145,10 @@ pub async fn view_day_handler( .markdown_text_part_one(Some(new_code), ctx) .await .map(|s| s.to_string()) - .unwrap_or_else(|_| "Error loading part one".to_string()) + .unwrap_or_else(|e| { + log::warn!("failed to render part 1: {e}"); + "Error loading part one".to_string() + }) } Some(code) => challenge .markdown_text_part_one( @@ -148,7 +157,10 @@ pub async fn view_day_handler( ) .await .map(|s| s.to_string()) - .unwrap_or_else(|_| "Error loading part one".to_string()), + .unwrap_or_else(|e| { + log::warn!("failed to render part 1: {e}"); + "Error loading part one".to_string() + }), }, }, @@ -237,6 +249,7 @@ async fn get_part_two_text( None => challenge .markdown_text_part_two(None, None) .await + .inspect_err(|e| log::warn!("failed to render part 2: {e}")) .map(|opt| opt.map(|s| s.to_string())) .unwrap_or(None), Some(users_did) => match challenge.get_days_challenge(&users_did).await { @@ -256,6 +269,7 @@ async fn get_part_two_text( challenge .markdown_text_part_two(Some(new_code), ctx) .await + .inspect_err(|e| log::warn!("failed to render part 2: {e}")) .map(|opt| opt.map(|s| s.to_string())) .unwrap_or(None) } else { @@ -281,6 +295,7 @@ async fn get_part_two_text( challenge .markdown_text_part_two(Some(new_code), ctx) .await + .inspect_err(|e| log::warn!("failed to render part 2: {e}")) .map(|opt| opt.map(|s| s.to_string())) .unwrap_or(None) } @@ -290,6 +305,7 @@ async fn get_part_two_text( current_challenge.additional_context.as_ref(), ) .await + .inspect_err(|e| log::warn!("failed to render part 2: {e}")) .map(|opt| opt.map(|s| s.to_string())) .unwrap_or(None), }