this repo has no description

day 5 part 1

+274 -9
+15 -2
shared/challenges_markdown/five/part_one.md
··· 1 - Day five is a only one part one. We talk about jetstream/firehose. A secret account creates a record and they have to 2 - watch create/update to capture the verification code. We hav an added button when clicked it creates the record. 1 + All events on the atmosphere are broadcast as events on 2 + the [Event Stream](https://atproto.com/specs/event-stream). Every lexicon you create is sent from a websocket 3 + on your PDS, picked up by a relay and then distributed to the rest of the network. This is usually referred to as 4 + the [firehose](https://docs.bsky.app/docs/advanced-guides/firehose). There is also 5 + the [jetstream](https://atproto.com/blog/jetstream) that simplifies the firehose by broadcasting the events in json and 6 + strips the authenticated portion to keep things simple. The jetstream also has the advantage of allowing you to filter 7 + by `collection` so you only get the events you are interested in. 8 + 9 + Today's challenge will be to find a record that is created by a secret account. When you click the link below our secret 10 + account will create a `codes.advent.challenge.shhh` record with your did as the `subject` field and will hold today's 11 + verificaiton code to enter below. This will come across as a create or update event and you can click the button below 12 + to create the record as many times as you need. 13 + 14 + <a href="/day/5/{{did}}" class="button">Create the record (will create as many as you need)</a> 15 +
+31
shared/lexicons/codes/advent/shhh.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "codes.advent.challenge.shhh", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "required": [ 11 + "secretPartOne", 12 + "subject", 13 + "createdAt" 14 + ], 15 + "properties": { 16 + "secretPartOne": { 17 + "type": "string" 18 + }, 19 + "subject": { 20 + "type": "string", 21 + "format": "did" 22 + }, 23 + "createdAt": { 24 + "type": "string", 25 + "format": "datetime" 26 + } 27 + } 28 + } 29 + } 30 + } 31 + }
+119 -5
shared/src/advent/challenges/day_five.rs
··· 1 - use crate::OAuthAgentType; 2 1 use crate::advent::day::Day; 3 - use crate::advent::{AdventChallenge, AdventError, ChallengeCheckResponse}; 2 + use crate::advent::{AdventChallenge, AdventError, AdventPart, ChallengeCheckResponse}; 3 + use crate::lexicons::codes::advent; 4 + use crate::lexicons::record::KnownRecord; 5 + use crate::{OAuthAgentType, PasswordAgent}; 4 6 use async_trait::async_trait; 7 + use atrium_api::types::Collection; 8 + use atrium_api::types::string::Tid; 9 + use serde_json::json; 5 10 use sqlx::PgPool; 6 11 7 12 pub struct DayFive { 8 13 pub pool: PgPool, 9 14 pub oauth_client: Option<OAuthAgentType>, 15 + pub secret_agent: Option<PasswordAgent>, 16 + } 17 + 18 + impl DayFive { 19 + /// Creates the challenge record on the secret agent's repo with the user's verification code. 20 + /// This is called from the `/day/5/{did}` endpoint. 21 + pub async fn create_secret_record(&self, did: &str) -> Result<(), AdventError> { 22 + let Some(agent) = &self.secret_agent else { 23 + log::warn!("No secret agent configured, skipping record creation for day five"); 24 + return Err(AdventError::ShouldNotHappen( 25 + "No secret agent configured".to_string(), 26 + )); 27 + }; 28 + 29 + // Get the user's challenge to find their verification code 30 + let challenge = self.get_days_challenge(did).await?.ok_or_else(|| { 31 + AdventError::ShouldNotHappen("Could not find challenge record for day 5".to_string()) 32 + })?; 33 + 34 + let code = challenge.verification_code_one.ok_or_else(|| { 35 + AdventError::ShouldNotHappen( 36 + "No verification code found for day 5 challenge".to_string(), 37 + ) 38 + })?; 39 + 40 + let agent_did = agent 41 + .did() 42 + .await 43 + .ok_or_else(|| AdventError::ShouldNotHappen("Secret agent has no DID".to_string()))?; 44 + 45 + let record_data = advent::challenge::shhh::RecordData { 46 + secret_part_one: code, 47 + created_at: atrium_api::types::string::Datetime::now(), 48 + subject: did.parse().unwrap(), 49 + }; 50 + let known_record: KnownRecord = record_data.into(); 51 + let record_value: atrium_api::types::Unknown = known_record.into(); 52 + 53 + let tid = Tid::from_datetime(23.try_into().unwrap(), challenge.time_started); 54 + let result = agent 55 + .api 56 + .com 57 + .atproto 58 + .repo 59 + .put_record( 60 + atrium_api::com::atproto::repo::put_record::InputData { 61 + collection: advent::challenge::Shhh::NSID.parse().unwrap(), 62 + repo: agent_did.as_ref().parse().unwrap(), 63 + rkey: tid.as_ref().parse().unwrap(), 64 + swap_record: None, 65 + record: record_value, 66 + swap_commit: None, 67 + validate: Some(false), 68 + } 69 + .into(), 70 + ) 71 + .await; 72 + 73 + match result { 74 + Ok(_) => { 75 + log::info!("Created secret record for day 5 for user: {did}"); 76 + Ok(()) 77 + } 78 + Err(e) => { 79 + log::error!("Failed to create secret record for day 5: {e}"); 80 + Err(AdventError::ShouldNotHappen(format!( 81 + "Failed to create secret record: {e}" 82 + ))) 83 + } 84 + } 85 + } 10 86 } 11 87 12 88 #[async_trait] ··· 27 103 true 28 104 } 29 105 106 + async fn build_additional_context( 107 + &self, 108 + did: &str, 109 + part: &AdventPart, 110 + _code: &str, 111 + ) -> Result<Option<serde_json::Value>, AdventError> { 112 + match part { 113 + AdventPart::One => Ok(Some(json!({ "did": did }))), 114 + AdventPart::Two => Ok(None), 115 + } 116 + } 117 + 30 118 async fn check_part_one( 31 119 &self, 32 - _did: String, 33 - _verification_code: Option<String>, 120 + did: String, 121 + verification_code: Option<String>, 34 122 ) -> Result<ChallengeCheckResponse, AdventError> { 35 - todo!() 123 + let submitted_code = match verification_code { 124 + Some(code) if !code.is_empty() => code, 125 + _ => { 126 + return Ok(ChallengeCheckResponse::Incorrect( 127 + "Please enter a verification code".to_string(), 128 + )); 129 + } 130 + }; 131 + 132 + let Some(challenge) = self.get_days_challenge(&did).await? else { 133 + log::error!("Could not find a challenge record for day: 5 for the user: {did:?}"); 134 + return Err(AdventError::ShouldNotHappen( 135 + "Could not find challenge record".to_string(), 136 + )); 137 + }; 138 + 139 + let expected_code = challenge 140 + .verification_code_one 141 + .ok_or(AdventError::ShouldNotHappen( 142 + "no verification code for day 5 challenge".to_string(), 143 + ))?; 144 + 145 + Ok(if submitted_code == expected_code { 146 + ChallengeCheckResponse::Correct 147 + } else { 148 + ChallengeCheckResponse::Incorrect(format!("The code {} is incorrect", submitted_code)) 149 + }) 36 150 } 37 151 }
+7
shared/src/lexicons/codes/advent/challenge.rs
··· 1 1 // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 2 //!Definitions for the `codes.advent.challenge` namespace. 3 3 pub mod day; 4 + pub mod shhh; 4 5 #[derive(Debug)] 5 6 pub struct Day; 6 7 impl atrium_api::types::Collection for Day { 7 8 const NSID: &'static str = "codes.advent.challenge.day"; 8 9 type Record = day::Record; 9 10 } 11 + #[derive(Debug)] 12 + pub struct Shhh; 13 + impl atrium_api::types::Collection for Shhh { 14 + const NSID: &'static str = "codes.advent.challenge.shhh"; 15 + type Record = shhh::Record; 16 + }
+16
shared/src/lexicons/codes/advent/challenge/shhh.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `codes.advent.challenge.shhh` namespace. 3 + use atrium_api::types::TryFromUnknown; 4 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 + #[serde(rename_all = "camelCase")] 6 + pub struct RecordData { 7 + pub created_at: atrium_api::types::string::Datetime, 8 + pub secret_part_one: String, 9 + pub subject: atrium_api::types::string::Did, 10 + } 11 + pub type Record = atrium_api::types::Object<RecordData>; 12 + impl From<atrium_api::types::Unknown> for RecordData { 13 + fn from(value: atrium_api::types::Unknown) -> Self { 14 + Self::try_from_unknown(value).unwrap() 15 + } 16 + }
+16
shared/src/lexicons/record.rs
··· 7 7 LexiconsCodesAdventChallengeDay( 8 8 Box<crate::lexicons::codes::advent::challenge::day::Record>, 9 9 ), 10 + #[serde(rename = "codes.advent.challenge.shhh")] 11 + LexiconsCodesAdventChallengeShhh( 12 + Box<crate::lexicons::codes::advent::challenge::shhh::Record>, 13 + ), 10 14 } 11 15 impl From<crate::lexicons::codes::advent::challenge::day::Record> for KnownRecord { 12 16 fn from(record: crate::lexicons::codes::advent::challenge::day::Record) -> Self { ··· 18 22 record_data: crate::lexicons::codes::advent::challenge::day::RecordData, 19 23 ) -> Self { 20 24 KnownRecord::LexiconsCodesAdventChallengeDay(Box::new(record_data.into())) 25 + } 26 + } 27 + impl From<crate::lexicons::codes::advent::challenge::shhh::Record> for KnownRecord { 28 + fn from(record: crate::lexicons::codes::advent::challenge::shhh::Record) -> Self { 29 + KnownRecord::LexiconsCodesAdventChallengeShhh(Box::new(record)) 30 + } 31 + } 32 + impl From<crate::lexicons::codes::advent::challenge::shhh::RecordData> for KnownRecord { 33 + fn from( 34 + record_data: crate::lexicons::codes::advent::challenge::shhh::RecordData, 35 + ) -> Self { 36 + KnownRecord::LexiconsCodesAdventChallengeShhh(Box::new(record_data.into())) 21 37 } 22 38 } 23 39 impl Into<atrium_api::types::Unknown> for KnownRecord {
+8 -2
web/src/handlers/auth.rs
··· 125 125 None => {} 126 126 Some(did) => { 127 127 //TODO lots of unwraps 128 - let did = atrium_api::types::string::Did::new(did.clone()).unwrap(); 129 - let client = oauth_client.restore(&did).await.unwrap(); 128 + let did = atrium_api::types::string::Did::new(did.clone()).map_err(|err| { 129 + log::error!("Failed to parse DID: {err}"); 130 + error_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to log out") 131 + })?; 132 + let client = oauth_client.restore(&did).await.map_err(|err| { 133 + log::error!("Failed to restore OAuth client: {err}"); 134 + error_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to log out") 135 + })?; 130 136 let agent = Agent::new(client); 131 137 let _ = agent.api.com.atproto.server.delete_session().await; 132 138 }
+40
web/src/handlers/day.rs
··· 50 50 Day::Five => Ok(Box::new(DayFive { 51 51 pool: state.postgres_pool, 52 52 oauth_client, 53 + secret_agent: state.secret_agent.clone(), 53 54 })), 54 55 Day::Six => Ok(Box::new(DaySix { 55 56 pool: state.postgres_pool, ··· 448 449 } 449 450 } 450 451 } 452 + 453 + /// Endpoint for day 5: creates a record on the secret agent's repo with the user's verification code, 454 + /// then redirects back to /day/5 so the user can find the code via firehose/jetstream. 455 + pub async fn day_five_create_record_handler( 456 + Path(user_did): Path<String>, 457 + state: State<AppState>, 458 + session: AxumSessionStore, 459 + ) -> Result<impl IntoResponse, Response> { 460 + // Verify the user is logged in and the DID matches 461 + let session_did = session.get_did().ok_or_else(|| { 462 + error_response( 463 + StatusCode::FORBIDDEN, 464 + "You need to be logged in to do this", 465 + ) 466 + })?; 467 + 468 + if session_did != user_did { 469 + return Err(error_response( 470 + StatusCode::FORBIDDEN, 471 + "You can only trigger this for your own account", 472 + )); 473 + } 474 + 475 + let day_five = DayFive { 476 + pool: state.postgres_pool.clone(), 477 + oauth_client: None, 478 + secret_agent: state.secret_agent.clone(), 479 + }; 480 + 481 + day_five 482 + .create_secret_record(&user_did) 483 + .await 484 + .map_err(log_and_respond( 485 + StatusCode::INTERNAL_SERVER_ERROR, 486 + "Error creating the secret record", 487 + ))?; 488 + 489 + Ok(Redirect::to("/day/5")) 490 + }
+22
web/src/main.rs
··· 72 72 //Used to get did to handle leaving because I figured we'd need it 73 73 handle_resolver: HandleResolver, 74 74 challenge_agent: Option<PasswordAgent>, 75 + secret_agent: Option<PasswordAgent>, 75 76 } 76 77 77 78 pub fn oauth_scopes() -> Vec<Scope> { ··· 239 240 challenge_agent = Some(Arc::new(agent)); 240 241 } 241 242 243 + // secret challenge account 244 + let mut secret_challenge_agent = None; 245 + let secret_challenge_pds = env::var("SECRET_CHALLENGE_PDS"); 246 + let secret_challenge_identity = env::var("SECRET_CHALLENGE_IDENTITY"); 247 + let secret_challenge_password = env::var("SECRET_CHALLENGE_PASSWORD"); 248 + if let (Ok(pds), Ok(identity), Ok(password)) = ( 249 + secret_challenge_pds, 250 + secret_challenge_identity, 251 + secret_challenge_password, 252 + ) { 253 + let agent = AtpAgent::new(ReqwestClient::new(pds), MemorySessionStore::default()); 254 + agent.login(identity, password).await?; 255 + secret_challenge_agent = Some(Arc::new(agent)); 256 + } 257 + 242 258 let app_state = AppState { 243 259 postgres_pool, 244 260 redis_pool, 245 261 oauth_client: client, 246 262 handle_resolver, 247 263 challenge_agent, 264 + secret_agent: secret_challenge_agent, 248 265 }; 266 + 249 267 //HACK Yeah I don't like it either - bt 250 268 let prod: bool = env::var("PROD") 251 269 .map(|val| val == "true") ··· 271 289 ), 272 290 false => post(handlers::day::post_day_handler), 273 291 }, 292 + ) 293 + .route( 294 + "/day/5/{user_did}", 295 + get(handlers::day::day_five_create_record_handler), 274 296 ) 275 297 .route("/login", get(handlers::auth::login_page_handler)) 276 298 .route("/logout", get(handlers::auth::logout_handler))