use crate::PasswordAgent; use crate::HandleResolver; use atrium_api::app::bsky::feed::post::RecordData; use atrium_api::app::bsky::richtext::facet; use atrium_api::types::string::Datetime; use atrium_api::types::TryIntoUnknown; use atrium_common::resolver::Resolver; /// Fire-and-forget: spawns a task that posts a completion announcement from the /// challenge agent's account, mentioning the user who completed the challenge. pub fn post_completion( agent: Option<&PasswordAgent>, resolver: &HandleResolver, user_did: &str, day: u8, part: u8, ) { let Some(agent) = agent.cloned() else { return; }; let resolver = resolver.clone(); let user_did = user_did.to_string(); tokio::spawn(async move { let handle = resolve_handle(&resolver, &user_did).await; let (text, facets) = build_post_text(&handle, &user_did, day, part); post_status(&agent, text, facets).await; }); } async fn resolve_handle(resolver: &HandleResolver, did: &str) -> Option { let did = match did.parse() { Ok(d) => d, Err(e) => { log::warn!("completion post: bad DID {did}: {e}"); return None; } }; match resolver.resolve(&did).await { Ok(doc) => doc .also_known_as .and_then(|aka: Vec| aka.into_iter().find(|s| s.starts_with("at://"))) .map(|uri: String| uri.strip_prefix("at://").unwrap_or(&uri).to_string()), Err(e) => { log::warn!("completion post: failed to resolve {did:?}: {e}"); None } } } fn build_post_text(handle: &Option, did: &str, day: u8, part: u8) -> (String, Option>) { let mention_text = match handle { Some(h) => format!("@{h}"), None => did.to_string(), }; let byte_start = 6; let text = match part { 1 => format!("Wooo! {mention_text} just completed the first part of Day {day} of at://advent!!"), 2 => format!("Whoa, {mention_text} just did the second part of Day {day} of at://advent!!!"), n => format!("{mention_text} just broke the site for Day {day}, Part {n}????!") }; let facets = handle.as_ref().map(|_| { vec![facet::MainData { index: facet::ByteSliceData { byte_start: byte_start, byte_end: byte_start + mention_text.len(), } .into(), features: vec![atrium_api::types::Union::Refs( facet::MainFeaturesItem::Mention(Box::new( facet::MentionData { did: did.parse().unwrap(), } .into(), )), )], } .into()] }); (text, facets) } async fn post_status(agent: &PasswordAgent, text: String, facets: Option>) { let Some(agent_did) = agent.did().await else { log::error!("completion post: agent has no DID"); return; }; let record = RecordData { created_at: Datetime::now(), embed: None, entities: None, facets, labels: None, langs: Some(vec!["en".parse().unwrap()]), reply: None, tags: None, text, }; let record_value = match record.try_into_unknown() { Ok(v) => v, Err(e) => { log::error!("completion post: failed to serialize record: {e}"); return; } }; let result = agent .api .com .atproto .repo .create_record( atrium_api::com::atproto::repo::create_record::InputData { collection: "app.bsky.feed.post".parse().unwrap(), repo: agent_did.as_ref().parse().unwrap(), rkey: None, record: record_value, swap_commit: None, validate: None, } .into(), ) .await; match result { Ok(output) => log::info!("completion post created: {}", output.uri), Err(e) => log::error!("completion post failed: {e}"), } }