this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

wire in the actual car download

ish

phil 87f9a7d0 0881985f

+151 -74
+1
Cargo.lock
··· 2865 2865 "atrium-repo", 2866 2866 "atrium-xrpc-client", 2867 2867 "axum", 2868 + "base64", 2868 2869 "bb8", 2869 2870 "bb8-redis", 2870 2871 "handlebars",
+1
Cargo.toml
··· 12 12 atrium-xrpc-client = "0.5.15" 13 13 atrium-oauth = "0.1.6" 14 14 atrium-repo = "0.1.7" 15 + base64 = "0.22" 15 16 chrono = { version = "0.4", features = ["serde", "now"] } 16 17 hickory-resolver = "0.24.1" 17 18 dotenv = "0.15.0"
+1
shared/Cargo.toml
··· 11 11 atrium-identity.workspace = true 12 12 atrium-oauth.workspace = true 13 13 atrium-repo.workspace = true 14 + base64.workspace = true 14 15 bb8.workspace = true 15 16 bb8-redis.workspace = true 16 17 hickory-resolver.workspace = true
+19 -1
shared/challenges_markdown/three/part_one.md
··· 1 1 This will be explaing the car format and will have a download link to a car file we create that they can download. 2 2 We can create a new web handler like. /day/3/{did} and it downloads the car file. 3 - Inside the car export is just one record with the verficiation code 3 + Inside the car export is just one record with the verficiation code 4 + 5 + TODO: detect not-logged-in and if not, don't render this button: 6 + 7 + <a id="download-car" href="#" class="btn btn-primary">Download your CAR file</a> 8 + 9 + <script> 10 + const carData = atob("{{car_base64}}"); 11 + const bytes = new Uint8Array(carData.length); 12 + for (let i = 0; i < carData.length; i++) bytes[i] = carData.charCodeAt(i); 13 + document.getElementById('download-car').addEventListener('click', (e) => { 14 + e.preventDefault(); 15 + const blob = new Blob([bytes], {type: 'application/vnd.ipld.car'}); 16 + const url = URL.createObjectURL(blob); 17 + const a = document.createElement('a'); 18 + a.href = url; a.download = 'day3.car'; a.click(); 19 + URL.revokeObjectURL(url); 20 + }); 21 + </script>
+5 -9
shared/examples/day_three_build_car.rs
··· 1 - use shared::advent::challenges::day_three::repo::{ChallengeRecord, manufacture_car}; 1 + use shared::advent::challenges::day_three::repo::manufacture_car; 2 2 3 3 #[tokio::main] 4 4 async fn main() { 5 5 let car_bytes = manufacture_car( 6 - "did:plc:somebodyyyy".parse().unwrap(), 7 - "codes.advent.supersecret.verification", 8 - "1337", 9 - ChallengeRecord { 10 - created_at: "2026-01-01T12:00:00Z".into(), 11 - verification_code: "ABCDE-FGHIJ".to_string(), 12 - message: "what did the muffin say to the other muffin?".into(), 13 - }, 6 + &("codes.advent.supersecret.verification".parse().unwrap()), 7 + &("1337".parse().unwrap()), 8 + "ABCDE-FGHIJ", 9 + "what did the muffin say to the other muffin?", 14 10 ) 15 11 .await 16 12 .expect("failed to build car");
+78
shared/src/advent/challenges/day_three/day_three.rs
··· 1 + use handlebars::Handlebars; 2 + use super::manufacture_car; 3 + use crate::advent::AdventPart; 4 + use serde_json::json; 5 + use base64::{Engine, engine::general_purpose::STANDARD}; 6 + use crate::OAuthAgentType; 7 + use crate::advent::{day::Day, AdventChallenge, AdventError, ChallengeCheckResponse}; 8 + use async_trait::async_trait; 9 + use sqlx::PgPool; 10 + 11 + pub struct DayThree { 12 + pub pool: PgPool, 13 + pub oauth_client: Option<OAuthAgentType>, 14 + } 15 + 16 + #[async_trait] 17 + impl AdventChallenge for DayThree { 18 + fn pool(&self) -> &PgPool { 19 + &self.pool 20 + } 21 + 22 + fn day(&self) -> Day { 23 + Day::Three 24 + } 25 + 26 + fn has_part_two(&self) -> bool { 27 + false 28 + } 29 + 30 + fn requires_manual_verification_part_one(&self) -> bool { 31 + true 32 + } 33 + 34 + /// inject the car right on the page :sicko: 35 + async fn markdown_text_part_one( 36 + &self, 37 + verification_code: Option<String>, 38 + _additional_context: Option<&serde_json::Value>, 39 + ) -> Result<String, AdventError> { 40 + 41 + let day_file = self.get_day_markdown_file(AdventPart::One)? 42 + .ok_or_else(|| AdventError::ShouldNotHappen("Missing part one markdown".into()))?; 43 + 44 + let md_text = std::str::from_utf8(day_file.data.as_ref())?; 45 + let code = verification_code.unwrap_or_else(|| "Login to get a code".to_string()); 46 + 47 + let car_base64 = if code != "Login to get a code" { 48 + let car_bytes = manufacture_car( 49 + &("codes.advent.supersecret.verification".parse().unwrap()), 50 + &("1337".parse().unwrap()), 51 + &code, 52 + "hellooooooo", 53 + ) 54 + .await 55 + .map_err(|e| AdventError::ShouldNotHappen(format!("failed to render car: {e}")))?; 56 + STANDARD.encode(&car_bytes) 57 + } else { 58 + "".to_string() 59 + }; 60 + 61 + let reg = Handlebars::new(); 62 + let context = json!({ 63 + "code": code, 64 + "car_base64": car_base64, 65 + }); 66 + let rendered = reg.render_template(md_text, &context)?; 67 + 68 + Ok(markdown::to_html_with_options(&rendered, &crate::advent::get_markdown_options()).unwrap()) 69 + } 70 + 71 + async fn check_part_one( 72 + &self, 73 + _did: String, 74 + _verification_code: Option<String>, 75 + ) -> Result<ChallengeCheckResponse, AdventError> { 76 + todo!() 77 + } 78 + }
+2 -38
shared/src/advent/challenges/day_three/mod.rs
··· 1 + pub mod day_three; 1 2 pub mod repo; 2 3 4 + pub use day_three::DayThree; 3 5 pub use repo::manufacture_car; 4 - 5 - use crate::OAuthAgentType; 6 - use crate::advent::day::Day; 7 - use crate::advent::{AdventChallenge, AdventError, ChallengeCheckResponse}; 8 - use async_trait::async_trait; 9 - use sqlx::PgPool; 10 - 11 - pub struct DayThree { 12 - pub pool: PgPool, 13 - pub oauth_client: Option<OAuthAgentType>, 14 - } 15 - 16 - #[async_trait] 17 - impl AdventChallenge for DayThree { 18 - fn pool(&self) -> &PgPool { 19 - &self.pool 20 - } 21 - 22 - fn day(&self) -> Day { 23 - Day::Three 24 - } 25 - 26 - fn has_part_two(&self) -> bool { 27 - false 28 - } 29 - 30 - fn requires_manual_verification_part_one(&self) -> bool { 31 - true 32 - } 33 - 34 - async fn check_part_one( 35 - &self, 36 - _did: String, 37 - _verification_code: Option<String>, 38 - ) -> Result<ChallengeCheckResponse, AdventError> { 39 - todo!() 40 - } 41 - }
+23 -21
shared/src/advent/challenges/day_three/repo.rs
··· 1 1 use std::io::Cursor; 2 2 3 - use atrium_api::types::string::Did; 3 + use atrium_api::types::string::{Nsid, RecordKey}; 4 4 use atrium_crypto::keypair::P256Keypair; 5 5 use atrium_repo::{Repository, blockstore::{CarStore, MemoryBlockStore}}; 6 6 use serde::Serialize; 7 + 8 + const CHALLENGE_DID: &str = "did:plc:3oktyyf7u4ecnvdwf3ogehpd"; 7 9 8 10 #[derive(Debug, thiserror::Error)] 9 11 pub enum CarBuildError { ··· 18 20 /// secret record type 19 21 #[derive(Debug, Serialize)] 20 22 #[serde(rename_all = "camelCase")] 21 - pub struct ChallengeRecord { 22 - pub created_at: String, 23 - pub verification_code: String, 24 - pub message: String, 23 + pub struct ChallengeRecord<'a> { 24 + pub created_at: &'a str, 25 + pub verification_code: &'a str, 26 + pub message: &'a str, 25 27 } 26 28 27 29 /// make a single-record atproto car 28 30 /// 29 31 /// returns CAR bytes 30 32 pub async fn manufacture_car( 31 - did: Did, 32 - collection: &str, 33 - rkey: &str, 34 - record: ChallengeRecord, 33 + collection: &Nsid, 34 + rkey: &RecordKey, 35 + code: &str, 36 + message: &str, 35 37 ) -> Result<Vec<u8>, CarBuildError> { 36 38 37 - // throwaway (fake key hello) 38 - let keypair = P256Keypair::import(&[1u8; 32])?; 39 - 39 + let keypair = P256Keypair::import(&[0xAAu8; 32])?; // fake, it's ok 40 + let did = CHALLENGE_DID.parse().unwrap(); 40 41 let mut mem = MemoryBlockStore::new(); 41 42 let builder = Repository::create(&mut mem, did).await?; 42 43 let sig = keypair.sign(&builder.bytes())?; 43 44 let mut repo = builder.finalize(sig).await?; 44 45 45 46 // add record 46 - let key = format!("{collection}/{rkey}"); 47 + let key = format!("{}/{}", collection.as_str(), rkey.as_str()); 48 + let record = ChallengeRecord { 49 + created_at: "asdf", // TODO generate 50 + verification_code: code, 51 + message, 52 + }; 47 53 let (cb, _cid) = repo.add_raw(&key, record).await?; 48 54 let sig = keypair.sign(&cb.bytes())?; 49 55 cb.finalize(sig).await?; ··· 64 70 #[tokio::test] 65 71 async fn test_generate_car() { 66 72 let car_bytes = manufacture_car( 67 - "did:plc:somebodyyyy".parse().unwrap(), 68 - "codes.advent.supersecret.verification", 69 - "1337", 70 - ChallengeRecord { 71 - created_at: "2026-01-01T12:00:00Z".into(), 72 - verification_code: "ABCDE-FGHIJ".to_string(), 73 - message: "what did the muffin say to the other muffin?".into(), 74 - }, 73 + &("codes.advent.supersecret.verification".parse().unwrap()), 74 + &("1337".parse().unwrap()), 75 + "ABCDE-FGHIJ", 76 + "what did the muffin say to the other muffin?", 75 77 ) 76 78 .await 77 79 .expect("build a car");
+1 -1
web/Cargo.toml
··· 9 9 atrium-identity.workspace = true 10 10 atrium-oauth.workspace = true 11 11 axum.workspace = true 12 - chrono.workspace = true 13 12 bb8.workspace = true 14 13 bb8-redis.workspace = true 14 + chrono.workspace = true 15 15 dotenv.workspace = true 16 16 log.workspace = true 17 17 redis.workspace = true
+20 -4
web/src/handlers/day.rs
··· 111 111 .markdown_text_part_one(None, None) 112 112 .await 113 113 .map(|s| s.to_string()) 114 - .unwrap_or_else(|_| "Error loading part one".to_string()), 114 + .unwrap_or_else(|e| { 115 + log::warn!("failed to render part 1: {e}"); 116 + "Error loading part one".to_string() 117 + }), 115 118 Some(ref users_did) => match challenge.get_days_challenge(&users_did).await { 116 119 Ok(current_challenge) => match current_challenge { 117 120 None => { ··· 125 128 .markdown_text_part_one(Some(new_code), ctx) 126 129 .await 127 130 .map(|s| s.to_string()) 128 - .unwrap_or_else(|_| "Error loading part one".to_string()) 131 + .unwrap_or_else(|e| { 132 + log::warn!("failed to render part 1: {e}"); 133 + "Error loading part one".to_string() 134 + }) 129 135 } 130 136 Some(current_challenge) => match current_challenge.verification_code_one { 131 137 None => { ··· 139 145 .markdown_text_part_one(Some(new_code), ctx) 140 146 .await 141 147 .map(|s| s.to_string()) 142 - .unwrap_or_else(|_| "Error loading part one".to_string()) 148 + .unwrap_or_else(|e| { 149 + log::warn!("failed to render part 1: {e}"); 150 + "Error loading part one".to_string() 151 + }) 143 152 } 144 153 Some(code) => challenge 145 154 .markdown_text_part_one( ··· 148 157 ) 149 158 .await 150 159 .map(|s| s.to_string()) 151 - .unwrap_or_else(|_| "Error loading part one".to_string()), 160 + .unwrap_or_else(|e| { 161 + log::warn!("failed to render part 1: {e}"); 162 + "Error loading part one".to_string() 163 + }), 152 164 }, 153 165 }, 154 166 ··· 237 249 None => challenge 238 250 .markdown_text_part_two(None, None) 239 251 .await 252 + .inspect_err(|e| log::warn!("failed to render part 2: {e}")) 240 253 .map(|opt| opt.map(|s| s.to_string())) 241 254 .unwrap_or(None), 242 255 Some(users_did) => match challenge.get_days_challenge(&users_did).await { ··· 256 269 challenge 257 270 .markdown_text_part_two(Some(new_code), ctx) 258 271 .await 272 + .inspect_err(|e| log::warn!("failed to render part 2: {e}")) 259 273 .map(|opt| opt.map(|s| s.to_string())) 260 274 .unwrap_or(None) 261 275 } else { ··· 281 295 challenge 282 296 .markdown_text_part_two(Some(new_code), ctx) 283 297 .await 298 + .inspect_err(|e| log::warn!("failed to render part 2: {e}")) 284 299 .map(|opt| opt.map(|s| s.to_string())) 285 300 .unwrap_or(None) 286 301 } ··· 290 305 current_challenge.additional_context.as_ref(), 291 306 ) 292 307 .await 308 + .inspect_err(|e| log::warn!("failed to render part 2: {e}")) 293 309 .map(|opt| opt.map(|s| s.to_string())) 294 310 .unwrap_or(None), 295 311 }