this repo has no description
at main 135 lines 4.2 kB view raw
1use crate::PasswordAgent; 2use crate::HandleResolver; 3use atrium_api::app::bsky::feed::post::RecordData; 4use atrium_api::app::bsky::richtext::facet; 5use atrium_api::types::string::Datetime; 6use atrium_api::types::TryIntoUnknown; 7use atrium_common::resolver::Resolver; 8 9/// Fire-and-forget: spawns a task that posts a completion announcement from the 10/// challenge agent's account, mentioning the user who completed the challenge. 11pub fn post_completion( 12 agent: Option<&PasswordAgent>, 13 resolver: &HandleResolver, 14 user_did: &str, 15 day: u8, 16 part: u8, 17) { 18 let Some(agent) = agent.cloned() else { 19 return; 20 }; 21 let resolver = resolver.clone(); 22 let user_did = user_did.to_string(); 23 24 tokio::spawn(async move { 25 let handle = resolve_handle(&resolver, &user_did).await; 26 let (text, facets) = build_post_text(&handle, &user_did, day, part); 27 post_status(&agent, text, facets).await; 28 }); 29} 30 31async fn resolve_handle(resolver: &HandleResolver, did: &str) -> Option<String> { 32 let did = match did.parse() { 33 Ok(d) => d, 34 Err(e) => { 35 log::warn!("completion post: bad DID {did}: {e}"); 36 return None; 37 } 38 }; 39 match resolver.resolve(&did).await { 40 Ok(doc) => doc 41 .also_known_as 42 .and_then(|aka: Vec<String>| aka.into_iter().find(|s| s.starts_with("at://"))) 43 .map(|uri: String| uri.strip_prefix("at://").unwrap_or(&uri).to_string()), 44 Err(e) => { 45 log::warn!("completion post: failed to resolve {did:?}: {e}"); 46 None 47 } 48 } 49} 50 51fn build_post_text(handle: &Option<String>, did: &str, day: u8, part: u8) -> (String, Option<Vec<facet::Main>>) { 52 let mention_text = match handle { 53 Some(h) => format!("@{h}"), 54 None => did.to_string(), 55 }; 56 57 let byte_start = 6; 58 59 let text = match part { 60 1 => format!("Wooo! {mention_text} just completed the first part of Day {day} of at://advent!!"), 61 2 => format!("Whoa, {mention_text} just did the second part of Day {day} of at://advent!!!"), 62 n => format!("{mention_text} just broke the site for Day {day}, Part {n}????!") 63 }; 64 65 let facets = handle.as_ref().map(|_| { 66 vec![facet::MainData { 67 index: facet::ByteSliceData { 68 byte_start: byte_start, 69 byte_end: byte_start + mention_text.len(), 70 } 71 .into(), 72 features: vec![atrium_api::types::Union::Refs( 73 facet::MainFeaturesItem::Mention(Box::new( 74 facet::MentionData { 75 did: did.parse().unwrap(), 76 } 77 .into(), 78 )), 79 )], 80 } 81 .into()] 82 }); 83 84 (text, facets) 85} 86 87async fn post_status(agent: &PasswordAgent, text: String, facets: Option<Vec<facet::Main>>) { 88 let Some(agent_did) = agent.did().await else { 89 log::error!("completion post: agent has no DID"); 90 return; 91 }; 92 93 let record = RecordData { 94 created_at: Datetime::now(), 95 embed: None, 96 entities: None, 97 facets, 98 labels: None, 99 langs: Some(vec!["en".parse().unwrap()]), 100 reply: None, 101 tags: None, 102 text, 103 }; 104 105 let record_value = match record.try_into_unknown() { 106 Ok(v) => v, 107 Err(e) => { 108 log::error!("completion post: failed to serialize record: {e}"); 109 return; 110 } 111 }; 112 113 let result = agent 114 .api 115 .com 116 .atproto 117 .repo 118 .create_record( 119 atrium_api::com::atproto::repo::create_record::InputData { 120 collection: "app.bsky.feed.post".parse().unwrap(), 121 repo: agent_did.as_ref().parse().unwrap(), 122 rkey: None, 123 record: record_value, 124 swap_commit: None, 125 validate: None, 126 } 127 .into(), 128 ) 129 .await; 130 131 match result { 132 Ok(output) => log::info!("completion post created: {}", output.uri), 133 Err(e) => log::error!("completion post failed: {e}"), 134 } 135}