forked from
oppi.li/at-advent
this repo has no description
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}