···11-Day four is the first one is create a tid from a given clock id and date time (random id and time the event started?)11+You may have seen some record keys that look a bit like this `3mhhb2bgzs22p`, these are
22+called [TIDs](https://atproto.com/specs/tid), or Timestamp Identifiers. These are the most common record keys. They
33+are
44+made up of a `clock id` and a `timestamp` then encoded in a way that you can sort and order by time. The clock id helps
55+with uniqueness if you are running multiple nodes, and the timestamps are in nanoseconds as well to help with this.
66+77+Today you will be making a `tid` withe clock id of `{{clock_id}}` and a timestamp of `{{tid_time}}`.
88+99+Hint: May check out [@atcute/tid](https://github.com/mary-ext/atcute/tree/trunk/packages/utilities/tid)
+121-5
shared/src/advent/challenges/day_four.rs
···11use crate::OAuthAgentType;
22use crate::advent::day::Day;
33-use crate::advent::{AdventChallenge, AdventError, ChallengeCheckResponse};
33+use crate::advent::{AdventChallenge, AdventError, AdventPart, ChallengeCheckResponse};
44use async_trait::async_trait;
55-use sqlx::PgPool;
55+use atrium_api::types::LimitedU32;
66+use atrium_api::types::string::Tid;
77+use rand::{Rng, rng};
88+use serde_json::json;
99+use sqlx::{PgPool, types::chrono, types::chrono::DateTime};
1010+use std::str::FromStr;
1111+use std::time::{SystemTime, UNIX_EPOCH};
612713pub struct DayFour {
814 pub pool: PgPool,
···2733 true
2834 }
29353636+ async fn build_additional_context(
3737+ &self,
3838+ _did: &str,
3939+ part: &AdventPart,
4040+ _code: &str,
4141+ ) -> Result<Option<serde_json::Value>, AdventError> {
4242+ match part {
4343+ AdventPart::One => {
4444+ let mut rng = rng();
4545+ let clock_id: LimitedU32<1023> = rng
4646+ .random_range(1..=1023)
4747+ .try_into()
4848+ .unwrap_or(LimitedU32::<1023>::MAX);
4949+ //Creating the datetime a bit oddly so we can zero out nano seconds to make tid creations a bit nicer
5050+ let now = SystemTime::now()
5151+ .duration_since(UNIX_EPOCH)
5252+ .expect("system time before Unix epoch");
5353+ let time_now = DateTime::from_timestamp(now.as_secs() as i64, 0).unwrap();
5454+ let tid = Tid::from_datetime(clock_id, time_now);
5555+5656+ Ok(Some(
5757+ json!({ "clock_id": clock_id, "tid_time": time_now, "tid": tid }),
5858+ ))
5959+ }
6060+ AdventPart::Two => Ok(None),
6161+ }
6262+ }
6363+3064 async fn check_part_one(
3165 &self,
3232- _did: String,
3333- _verification_code: Option<String>,
6666+ did: String,
6767+ verification_code: Option<String>,
3468 ) -> Result<ChallengeCheckResponse, AdventError> {
3535- todo!()
6969+ let verification_code = verification_code.unwrap_or_default();
7070+ let tid = match Tid::from_str(verification_code.as_str()) {
7171+ Ok(tid) => tid,
7272+ Err(err) => {
7373+ log::error!("Error parsing TID: {}", err);
7474+ return Ok(ChallengeCheckResponse::Incorrect(
7575+ "Could not parse the TID, please try again.".to_string(),
7676+ ));
7777+ }
7878+ };
7979+8080+ let days_challenge = match self.get_days_challenge(did.as_str()).await? {
8181+ None => {
8282+ log::error!("Could not find a challenge record for day: 4 for the user: {did:?}");
8383+ return Err(AdventError::ShouldNotHappen(
8484+ "Could not find challenge record".to_string(),
8585+ ));
8686+ }
8787+ Some(challenge) => challenge,
8888+ };
8989+9090+ let additional_context = match days_challenge.additional_context {
9191+ Some(ctx) => ctx,
9292+ None => {
9393+ log::error!("No additional_context found for day 4 challenge for user: {did:?}");
9494+ return Err(AdventError::ShouldNotHappen(
9595+ "Could not find challenge context".to_string(),
9696+ ));
9797+ }
9898+ };
9999+100100+ let expected_clock_id = additional_context["clock_id"].as_u64().unwrap_or(0) as u32;
101101+ let expected_time_str = additional_context["tid_time"].as_str().unwrap_or_default();
102102+103103+ let expected_time = match chrono::DateTime::parse_from_rfc3339(expected_time_str) {
104104+ Ok(dt) => dt.timestamp(),
105105+ Err(err) => {
106106+ log::error!("Failed to parse stored tid_time: {}", err);
107107+ return Err(AdventError::ShouldNotHappen(
108108+ "Could not parse stored challenge time".to_string(),
109109+ ));
110110+ }
111111+ };
112112+113113+ let (submitted_timestamp_micros, submitted_clock_id) = match decode_tid(tid.as_str()) {
114114+ Some(values) => values,
115115+ None => {
116116+ return Ok(ChallengeCheckResponse::Incorrect(
117117+ "Could not decode the TID, please try again.".to_string(),
118118+ ));
119119+ }
120120+ };
121121+122122+ if submitted_clock_id != expected_clock_id {
123123+ return Ok(ChallengeCheckResponse::Incorrect(
124124+ "The clock ID in your TID does not match the expected clock ID.".to_string(),
125125+ ));
126126+ }
127127+128128+ let submitted_timestamp_secs = submitted_timestamp_micros / 1_000_000;
129129+ let time_diff = (submitted_timestamp_secs - expected_time).abs();
130130+131131+ if time_diff > 300 {
132132+ return Ok(ChallengeCheckResponse::Incorrect(
133133+ "The timestamp in your TID is not within 5 minutes of the expected time."
134134+ .to_string(),
135135+ ));
136136+ }
137137+138138+ Ok(ChallengeCheckResponse::Correct)
36139 }
37140}
141141+142142+/// Decodes a TID string and gets the timestamp and clock id
143143+fn decode_tid(tid_str: &str) -> Option<(i64, u32)> {
144144+ const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
145145+ let mut n: u64 = 0;
146146+ for c in tid_str.bytes() {
147147+ let val = CHARSET.iter().position(|&b| b == c)? as u64;
148148+ n = (n << 5) | val;
149149+ }
150150+ let timestamp_micros = (n >> 10) as i64;
151151+ let clock_id = (n & 0x3FF) as u32;
152152+ Some((timestamp_micros, clock_id))
153153+}