at main 132 lines 4.7 kB view raw
1//! Document sync helpers for site.standard.document records. 2//! 3//! Creates/updates site.standard.document records for notebook entries 4//! when the notebook has publishGlobal enabled. 5 6use jacquard::IntoStatic; 7use jacquard::cowstr::ToCowStr; 8use jacquard::prelude::*; 9use jacquard::to_data; 10use jacquard::types::ident::AtIdentifier; 11use jacquard::types::recordkey::RecordKey; 12use jacquard::types::string::{AtUri, Nsid}; 13use weaver_api::com_atproto::repo::create_record::CreateRecord; 14use weaver_api::com_atproto::repo::get_record::GetRecord; 15use weaver_api::sh_weaver::domain::generate_document::GenerateDocument; 16use weaver_api::sh_weaver::notebook::entry::Entry; 17use weaver_common::{WeaverError, slugify}; 18 19use crate::fetch::Fetcher; 20 21const DOCUMENT_NSID: &str = "site.standard.document"; 22 23/// Create a site.standard.document for an entry if the notebook has publishGlobal. 24/// 25/// Returns Ok(Some(document_uri)) if created, Ok(None) if not needed, Err on failure. 26pub async fn create_document_for_entry( 27 fetcher: &Fetcher, 28 entry_uri: &AtUri<'_>, 29 entry_record: &Entry<'_>, 30 publication_uri: &AtUri<'_>, 31) -> Result<Option<AtUri<'static>>, WeaverError> { 32 // Build the document path from entry path (or fallback to slugified title). 33 let path = if !entry_record.path.is_empty() { 34 entry_record.path.to_cowstr() 35 } else if !entry_record.title.is_empty() { 36 slugify(entry_record.title.as_ref()) 37 .to_cowstr() 38 .into_static() 39 } else { 40 "untitled".to_cowstr() 41 }; 42 43 // Serialize entry record for the generateDocument call. 44 let entry_data = to_data(entry_record) 45 .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to serialize entry: {}", e)))?; 46 47 // Call generateDocument endpoint (proxied through PDS with auth). 48 let request = GenerateDocument::new() 49 .entry(entry_uri.clone()) 50 .entry_record(entry_data) 51 .publication(publication_uri.clone()) 52 .path(path) 53 .build(); 54 55 let response = fetcher 56 .send(request) 57 .await 58 .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to generate document: {}", e)))?; 59 60 let output = response 61 .into_output() 62 .map_err(|e| WeaverError::InvalidNotebook(format!("generateDocument failed: {}", e)))?; 63 64 // Write the document to the PDS. 65 let did = fetcher 66 .current_did() 67 .await 68 .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 69 70 // Use entry rkey for 1:1 mapping. 71 let entry_rkey = entry_uri 72 .rkey() 73 .ok_or_else(|| WeaverError::InvalidNotebook("Entry URI missing rkey".into()))?; 74 75 let collection = Nsid::new(DOCUMENT_NSID).map_err(WeaverError::AtprotoString)?; 76 let document_data = to_data(&output.record).map_err(|e| { 77 WeaverError::InvalidNotebook(format!("Failed to serialize document: {}", e)) 78 })?; 79 80 let rkey = RecordKey::any(entry_rkey.as_ref()) 81 .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; 82 83 let request = CreateRecord::new() 84 .repo(AtIdentifier::Did(did)) 85 .collection(collection) 86 .rkey(rkey) 87 .record(document_data) 88 .build(); 89 90 let response = fetcher 91 .send(request) 92 .await 93 .map_err(jacquard::client::AgentError::from)?; 94 95 let create_output = response 96 .into_output() 97 .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to create document: {}", e)))?; 98 99 Ok(Some(create_output.uri.into_static())) 100} 101 102/// Check if a site.standard.document exists for an entry. 103pub async fn document_exists(fetcher: &Fetcher, entry_rkey: &str) -> Result<bool, WeaverError> { 104 let did = fetcher 105 .current_did() 106 .await 107 .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 108 109 let collection = Nsid::new(DOCUMENT_NSID).map_err(WeaverError::AtprotoString)?; 110 let rkey = 111 RecordKey::any(entry_rkey).map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; 112 113 let request = GetRecord::new() 114 .repo(AtIdentifier::Did(did)) 115 .collection(collection) 116 .rkey(rkey) 117 .build(); 118 119 match fetcher.send(request).await { 120 Ok(response) => Ok(response.into_output().is_ok()), 121 Err(_) => Ok(false), 122 } 123} 124 125/// Get the publication URI for a notebook (same rkey). 126pub fn publication_uri_for_notebook(notebook_uri: &AtUri<'_>) -> Option<AtUri<'static>> { 127 let did = notebook_uri.authority(); 128 let rkey = notebook_uri.rkey()?; 129 130 let uri_str = format!("at://{}/site.standard.publication/{}", did, rkey.as_ref()); 131 AtUri::new(&uri_str).ok().map(|u| u.into_static()) 132}