···11-Day five is a only one part one. We talk about jetstream/firehose. A secret account creates a record and they have to
22-watch create/update to capture the verification code. We hav an added button when clicked it creates the record. 11+All events on the atmosphere are broadcast as events on
22+the [Event Stream](https://atproto.com/specs/event-stream). Every lexicon you create is sent from a websocket
33+on your PDS, picked up by a relay and then distributed to the rest of the network. This is usually referred to as
44+the [firehose](https://docs.bsky.app/docs/advanced-guides/firehose). There is also
55+the [jetstream](https://atproto.com/blog/jetstream) that simplifies the firehose by broadcasting the events in json and
66+strips the authenticated portion to keep things simple. The jetstream also has the advantage of allowing you to filter
77+by `collection` so you only get the events you are interested in.
88+99+Today's challenge will be to find a record that is created by a secret account. When you click the link below our secret
1010+account will create a `codes.advent.challenge.shhh` record with your did as the `subject` field and will hold today's
1111+verificaiton code to enter below. This will come across as a create or update event and you can click the button below
1212+to create the record as many times as you need.
1313+1414+<a href="/day/5/{{did}}" class="button">Create the record (will create as many as you need)</a>
1515+
···11-use crate::OAuthAgentType;
21use crate::advent::day::Day;
33-use crate::advent::{AdventChallenge, AdventError, ChallengeCheckResponse};
22+use crate::advent::{AdventChallenge, AdventError, AdventPart, ChallengeCheckResponse};
33+use crate::lexicons::codes::advent;
44+use crate::lexicons::record::KnownRecord;
55+use crate::{OAuthAgentType, PasswordAgent};
46use async_trait::async_trait;
77+use atrium_api::types::Collection;
88+use atrium_api::types::string::Tid;
99+use serde_json::json;
510use sqlx::PgPool;
611712pub struct DayFive {
813 pub pool: PgPool,
914 pub oauth_client: Option<OAuthAgentType>,
1515+ pub secret_agent: Option<PasswordAgent>,
1616+}
1717+1818+impl DayFive {
1919+ /// Creates the challenge record on the secret agent's repo with the user's verification code.
2020+ /// This is called from the `/day/5/{did}` endpoint.
2121+ pub async fn create_secret_record(&self, did: &str) -> Result<(), AdventError> {
2222+ let Some(agent) = &self.secret_agent else {
2323+ log::warn!("No secret agent configured, skipping record creation for day five");
2424+ return Err(AdventError::ShouldNotHappen(
2525+ "No secret agent configured".to_string(),
2626+ ));
2727+ };
2828+2929+ // Get the user's challenge to find their verification code
3030+ let challenge = self.get_days_challenge(did).await?.ok_or_else(|| {
3131+ AdventError::ShouldNotHappen("Could not find challenge record for day 5".to_string())
3232+ })?;
3333+3434+ let code = challenge.verification_code_one.ok_or_else(|| {
3535+ AdventError::ShouldNotHappen(
3636+ "No verification code found for day 5 challenge".to_string(),
3737+ )
3838+ })?;
3939+4040+ let agent_did = agent
4141+ .did()
4242+ .await
4343+ .ok_or_else(|| AdventError::ShouldNotHappen("Secret agent has no DID".to_string()))?;
4444+4545+ let record_data = advent::challenge::shhh::RecordData {
4646+ secret_part_one: code,
4747+ created_at: atrium_api::types::string::Datetime::now(),
4848+ subject: did.parse().unwrap(),
4949+ };
5050+ let known_record: KnownRecord = record_data.into();
5151+ let record_value: atrium_api::types::Unknown = known_record.into();
5252+5353+ let tid = Tid::from_datetime(23.try_into().unwrap(), challenge.time_started);
5454+ let result = agent
5555+ .api
5656+ .com
5757+ .atproto
5858+ .repo
5959+ .put_record(
6060+ atrium_api::com::atproto::repo::put_record::InputData {
6161+ collection: advent::challenge::Shhh::NSID.parse().unwrap(),
6262+ repo: agent_did.as_ref().parse().unwrap(),
6363+ rkey: tid.as_ref().parse().unwrap(),
6464+ swap_record: None,
6565+ record: record_value,
6666+ swap_commit: None,
6767+ validate: Some(false),
6868+ }
6969+ .into(),
7070+ )
7171+ .await;
7272+7373+ match result {
7474+ Ok(_) => {
7575+ log::info!("Created secret record for day 5 for user: {did}");
7676+ Ok(())
7777+ }
7878+ Err(e) => {
7979+ log::error!("Failed to create secret record for day 5: {e}");
8080+ Err(AdventError::ShouldNotHappen(format!(
8181+ "Failed to create secret record: {e}"
8282+ )))
8383+ }
8484+ }
8585+ }
1086}
11871288#[async_trait]
···27103 true
28104 }
29105106106+ async fn build_additional_context(
107107+ &self,
108108+ did: &str,
109109+ part: &AdventPart,
110110+ _code: &str,
111111+ ) -> Result<Option<serde_json::Value>, AdventError> {
112112+ match part {
113113+ AdventPart::One => Ok(Some(json!({ "did": did }))),
114114+ AdventPart::Two => Ok(None),
115115+ }
116116+ }
117117+30118 async fn check_part_one(
31119 &self,
3232- _did: String,
3333- _verification_code: Option<String>,
120120+ did: String,
121121+ verification_code: Option<String>,
34122 ) -> Result<ChallengeCheckResponse, AdventError> {
3535- todo!()
123123+ let submitted_code = match verification_code {
124124+ Some(code) if !code.is_empty() => code,
125125+ _ => {
126126+ return Ok(ChallengeCheckResponse::Incorrect(
127127+ "Please enter a verification code".to_string(),
128128+ ));
129129+ }
130130+ };
131131+132132+ let Some(challenge) = self.get_days_challenge(&did).await? else {
133133+ log::error!("Could not find a challenge record for day: 5 for the user: {did:?}");
134134+ return Err(AdventError::ShouldNotHappen(
135135+ "Could not find challenge record".to_string(),
136136+ ));
137137+ };
138138+139139+ let expected_code = challenge
140140+ .verification_code_one
141141+ .ok_or(AdventError::ShouldNotHappen(
142142+ "no verification code for day 5 challenge".to_string(),
143143+ ))?;
144144+145145+ Ok(if submitted_code == expected_code {
146146+ ChallengeCheckResponse::Correct
147147+ } else {
148148+ ChallengeCheckResponse::Incorrect(format!("The code {} is incorrect", submitted_code))
149149+ })
36150 }
37151}
+7
shared/src/lexicons/codes/advent/challenge.rs
···11// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22//!Definitions for the `codes.advent.challenge` namespace.
33pub mod day;
44+pub mod shhh;
45#[derive(Debug)]
56pub struct Day;
67impl atrium_api::types::Collection for Day {
78 const NSID: &'static str = "codes.advent.challenge.day";
89 type Record = day::Record;
910}
1111+#[derive(Debug)]
1212+pub struct Shhh;
1313+impl atrium_api::types::Collection for Shhh {
1414+ const NSID: &'static str = "codes.advent.challenge.shhh";
1515+ type Record = shhh::Record;
1616+}
···125125 None => {}
126126 Some(did) => {
127127 //TODO lots of unwraps
128128- let did = atrium_api::types::string::Did::new(did.clone()).unwrap();
129129- let client = oauth_client.restore(&did).await.unwrap();
128128+ let did = atrium_api::types::string::Did::new(did.clone()).map_err(|err| {
129129+ log::error!("Failed to parse DID: {err}");
130130+ error_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to log out")
131131+ })?;
132132+ let client = oauth_client.restore(&did).await.map_err(|err| {
133133+ log::error!("Failed to restore OAuth client: {err}");
134134+ error_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to log out")
135135+ })?;
130136 let agent = Agent::new(client);
131137 let _ = agent.api.com.atproto.server.delete_session().await;
132138 }
+40
web/src/handlers/day.rs
···5050 Day::Five => Ok(Box::new(DayFive {
5151 pool: state.postgres_pool,
5252 oauth_client,
5353+ secret_agent: state.secret_agent.clone(),
5354 })),
5455 Day::Six => Ok(Box::new(DaySix {
5556 pool: state.postgres_pool,
···448449 }
449450 }
450451}
452452+453453+/// Endpoint for day 5: creates a record on the secret agent's repo with the user's verification code,
454454+/// then redirects back to /day/5 so the user can find the code via firehose/jetstream.
455455+pub async fn day_five_create_record_handler(
456456+ Path(user_did): Path<String>,
457457+ state: State<AppState>,
458458+ session: AxumSessionStore,
459459+) -> Result<impl IntoResponse, Response> {
460460+ // Verify the user is logged in and the DID matches
461461+ let session_did = session.get_did().ok_or_else(|| {
462462+ error_response(
463463+ StatusCode::FORBIDDEN,
464464+ "You need to be logged in to do this",
465465+ )
466466+ })?;
467467+468468+ if session_did != user_did {
469469+ return Err(error_response(
470470+ StatusCode::FORBIDDEN,
471471+ "You can only trigger this for your own account",
472472+ ));
473473+ }
474474+475475+ let day_five = DayFive {
476476+ pool: state.postgres_pool.clone(),
477477+ oauth_client: None,
478478+ secret_agent: state.secret_agent.clone(),
479479+ };
480480+481481+ day_five
482482+ .create_secret_record(&user_did)
483483+ .await
484484+ .map_err(log_and_respond(
485485+ StatusCode::INTERNAL_SERVER_ERROR,
486486+ "Error creating the secret record",
487487+ ))?;
488488+489489+ Ok(Redirect::to("/day/5"))
490490+}
+22
web/src/main.rs
···7272 //Used to get did to handle leaving because I figured we'd need it
7373 handle_resolver: HandleResolver,
7474 challenge_agent: Option<PasswordAgent>,
7575+ secret_agent: Option<PasswordAgent>,
7576}
76777778pub fn oauth_scopes() -> Vec<Scope> {
···239240 challenge_agent = Some(Arc::new(agent));
240241 }
241242243243+ // secret challenge account
244244+ let mut secret_challenge_agent = None;
245245+ let secret_challenge_pds = env::var("SECRET_CHALLENGE_PDS");
246246+ let secret_challenge_identity = env::var("SECRET_CHALLENGE_IDENTITY");
247247+ let secret_challenge_password = env::var("SECRET_CHALLENGE_PASSWORD");
248248+ if let (Ok(pds), Ok(identity), Ok(password)) = (
249249+ secret_challenge_pds,
250250+ secret_challenge_identity,
251251+ secret_challenge_password,
252252+ ) {
253253+ let agent = AtpAgent::new(ReqwestClient::new(pds), MemorySessionStore::default());
254254+ agent.login(identity, password).await?;
255255+ secret_challenge_agent = Some(Arc::new(agent));
256256+ }
257257+242258 let app_state = AppState {
243259 postgres_pool,
244260 redis_pool,
245261 oauth_client: client,
246262 handle_resolver,
247263 challenge_agent,
264264+ secret_agent: secret_challenge_agent,
248265 };
266266+249267 //HACK Yeah I don't like it either - bt
250268 let prod: bool = env::var("PROD")
251269 .map(|val| val == "true")
···271289 ),
272290 false => post(handlers::day::post_day_handler),
273291 },
292292+ )
293293+ .route(
294294+ "/day/5/{user_did}",
295295+ get(handlers::day::day_five_create_record_handler),
274296 )
275297 .route("/login", get(handlers::auth::login_page_handler))
276298 .route("/logout", get(handlers::auth::logout_handler))