//! Document sync helpers for site.standard.document records. //! //! Creates/updates site.standard.document records for notebook entries //! when the notebook has publishGlobal enabled. use jacquard::IntoStatic; use jacquard::cowstr::ToCowStr; use jacquard::prelude::*; use jacquard::to_data; use jacquard::types::ident::AtIdentifier; use jacquard::types::recordkey::RecordKey; use jacquard::types::string::{AtUri, Nsid}; use weaver_api::com_atproto::repo::create_record::CreateRecord; use weaver_api::com_atproto::repo::get_record::GetRecord; use weaver_api::sh_weaver::domain::generate_document::GenerateDocument; use weaver_api::sh_weaver::notebook::entry::Entry; use weaver_common::{WeaverError, slugify}; use crate::fetch::Fetcher; const DOCUMENT_NSID: &str = "site.standard.document"; /// Create a site.standard.document for an entry if the notebook has publishGlobal. /// /// Returns Ok(Some(document_uri)) if created, Ok(None) if not needed, Err on failure. pub async fn create_document_for_entry( fetcher: &Fetcher, entry_uri: &AtUri<'_>, entry_record: &Entry<'_>, publication_uri: &AtUri<'_>, ) -> Result>, WeaverError> { // Build the document path from entry path (or fallback to slugified title). let path = if !entry_record.path.is_empty() { entry_record.path.to_cowstr() } else if !entry_record.title.is_empty() { slugify(entry_record.title.as_ref()) .to_cowstr() .into_static() } else { "untitled".to_cowstr() }; // Serialize entry record for the generateDocument call. let entry_data = to_data(entry_record) .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to serialize entry: {}", e)))?; // Call generateDocument endpoint (proxied through PDS with auth). let request = GenerateDocument::new() .entry(entry_uri.clone()) .entry_record(entry_data) .publication(publication_uri.clone()) .path(path) .build(); let response = fetcher .send(request) .await .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to generate document: {}", e)))?; let output = response .into_output() .map_err(|e| WeaverError::InvalidNotebook(format!("generateDocument failed: {}", e)))?; // Write the document to the PDS. let did = fetcher .current_did() .await .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; // Use entry rkey for 1:1 mapping. let entry_rkey = entry_uri .rkey() .ok_or_else(|| WeaverError::InvalidNotebook("Entry URI missing rkey".into()))?; let collection = Nsid::new(DOCUMENT_NSID).map_err(WeaverError::AtprotoString)?; let document_data = to_data(&output.record).map_err(|e| { WeaverError::InvalidNotebook(format!("Failed to serialize document: {}", e)) })?; let rkey = RecordKey::any(entry_rkey.as_ref()) .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; let request = CreateRecord::new() .repo(AtIdentifier::Did(did)) .collection(collection) .rkey(rkey) .record(document_data) .build(); let response = fetcher .send(request) .await .map_err(jacquard::client::AgentError::from)?; let create_output = response .into_output() .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to create document: {}", e)))?; Ok(Some(create_output.uri.into_static())) } /// Check if a site.standard.document exists for an entry. pub async fn document_exists(fetcher: &Fetcher, entry_rkey: &str) -> Result { let did = fetcher .current_did() .await .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; let collection = Nsid::new(DOCUMENT_NSID).map_err(WeaverError::AtprotoString)?; let rkey = RecordKey::any(entry_rkey).map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; let request = GetRecord::new() .repo(AtIdentifier::Did(did)) .collection(collection) .rkey(rkey) .build(); match fetcher.send(request).await { Ok(response) => Ok(response.into_output().is_ok()), Err(_) => Ok(false), } } /// Get the publication URI for a notebook (same rkey). pub fn publication_uri_for_notebook(notebook_uri: &AtUri<'_>) -> Option> { let did = notebook_uri.authority(); let rkey = notebook_uri.rkey()?; let uri_str = format!("at://{}/site.standard.publication/{}", did, rkey.as_ref()); AtUri::new(&uri_str).ok().map(|u| u.into_static()) }