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),
}