at main 1624 lines 56 kB view raw
1use crate::auth::AuthStore; 2use crate::cache_impl; 3use dioxus::Result; 4use jacquard::AuthorizationToken; 5use jacquard::CowStr; 6use jacquard::IntoStatic; 7use jacquard::client::Agent; 8use jacquard::client::AgentError; 9use jacquard::client::AgentKind; 10use jacquard::error::ClientError; 11use jacquard::error::XrpcResult; 12use jacquard::from_data; 13use jacquard::from_data_owned; 14use jacquard::identity::JacquardResolver; 15use jacquard::identity::lexicon_resolver::{ 16 LexiconResolutionError, LexiconSchemaResolver, ResolvedLexiconSchema, 17}; 18use jacquard::identity::resolver::DidDocResponse; 19use jacquard::identity::resolver::IdentityError; 20use jacquard::identity::resolver::ResolverOptions; 21use jacquard::oauth::client::OAuthClient; 22use jacquard::oauth::client::OAuthSession; 23use jacquard::prelude::*; 24use jacquard::types::string::Did; 25use jacquard::types::string::Handle; 26use jacquard::types::string::Nsid; 27use jacquard::xrpc::XrpcResponse; 28use jacquard::xrpc::*; 29use jacquard::{ 30 smol_str::{SmolStr, format_smolstr}, 31 types::aturi::AtUri, 32 types::ident::AtIdentifier, 33}; 34use serde::{Deserialize, Serialize}; 35use std::future::Future; 36use std::{sync::Arc, time::Duration}; 37use tokio::sync::RwLock; 38use weaver_api::app_bsky::actor::get_profile::GetProfile; 39use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; 40use weaver_api::sh_weaver::actor::ProfileDataViewInner; 41use weaver_api::sh_weaver::notebook::EntryView; 42use weaver_api::{ 43 com_atproto::repo::strong_ref::StrongRef, 44 sh_weaver::{ 45 actor::ProfileDataView, 46 notebook::{BookEntryView, NotebookView, entry::Entry}, 47 }, 48}; 49use weaver_common::WeaverError; 50use weaver_common::WeaverExt; 51use weaver_common::agent::title_matches; 52 53#[derive(Debug, Clone, Deserialize, Serialize)] 54struct UfosRecord { 55 collection: String, 56 did: String, 57 record: serde_json::Value, 58 rkey: String, 59 time_us: u64, 60} 61 62/// Data for a standalone entry (may or may not have notebook context) 63#[derive(Clone, PartialEq)] 64pub struct StandaloneEntryData { 65 pub entry: Entry<'static>, 66 pub entry_view: EntryView<'static>, 67 /// Present if entry is in exactly one notebook 68 pub notebook_context: Option<NotebookContext>, 69} 70 71/// Notebook context for an entry 72#[derive(Clone, PartialEq)] 73pub struct NotebookContext { 74 pub notebook: NotebookView<'static>, 75 /// BookEntryView with prev/next navigation 76 pub book_entry_view: BookEntryView<'static>, 77} 78 79/// Data for a WhiteWind blog entry 80#[derive(Clone, PartialEq)] 81pub struct WhiteWindEntryData { 82 pub entry: weaver_api::com_whtwnd::blog::entry::Entry<'static>, 83 pub profile: ProfileDataView<'static>, 84} 85 86/// Data for a Leaflet document 87#[derive(Clone, PartialEq)] 88pub struct LeafletDocumentData { 89 pub document: weaver_api::pub_leaflet::document::Document<'static>, 90 pub profile: ProfileDataView<'static>, 91 /// Publication base_path for constructing external URL (e.g., "connectedplaces.leaflet.pub") 92 pub publication_base_path: Option<String>, 93 /// Pre-rendered HTML content 94 pub rendered_html: Option<String>, 95} 96 97/// Data for a site.standard / blog.pckt document 98#[cfg(feature = "pckt")] 99#[derive(Clone, PartialEq)] 100pub struct PcktDocumentData { 101 pub document: weaver_api::site_standard::document::Document<'static>, 102 pub profile: ProfileDataView<'static>, 103 /// Publication URL for constructing external URL (e.g., "https://crypto.pckt.blog") 104 pub publication_url: Option<String>, 105 /// Pre-rendered HTML content 106 pub rendered_html: Option<String>, 107} 108 109pub struct Client { 110 pub oauth_client: Arc<OAuthClient<JacquardResolver, AuthStore>>, 111 pub session: RwLock<Option<Arc<Agent<OAuthSession<JacquardResolver, AuthStore>>>>>, 112} 113 114impl Client { 115 pub fn new(oauth_client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 116 Self { 117 oauth_client: Arc::new(oauth_client), 118 session: RwLock::new(None), 119 } 120 } 121} 122 123impl HttpClient for Client { 124 type Error = IdentityError; 125 126 #[cfg(not(target_arch = "wasm32"))] 127 fn send_http( 128 &self, 129 request: http::Request<Vec<u8>>, 130 ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> + Send 131 { 132 self.oauth_client.send_http(request) 133 } 134 135 #[cfg(target_arch = "wasm32")] 136 fn send_http( 137 &self, 138 request: http::Request<Vec<u8>>, 139 ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> { 140 self.oauth_client.send_http(request) 141 } 142} 143 144impl XrpcClient for Client { 145 #[doc = " Get the base URI for the client."] 146 fn base_uri(&self) -> impl Future<Output = CowStr<'static>> + Send { 147 async { 148 let guard = self.session.read().await; 149 if let Some(session) = guard.clone() { 150 session.base_uri().await 151 } else { 152 // When unauthenticated, use index if configured 153 #[cfg(feature = "use-index")] 154 if !crate::env::WEAVER_INDEXER_URL.is_empty() { 155 return CowStr::from(crate::env::WEAVER_INDEXER_URL); 156 } 157 self.oauth_client.base_uri().await 158 } 159 } 160 } 161 162 #[doc = " Send an XRPC request and parse the response"] 163 #[cfg(not(target_arch = "wasm32"))] 164 fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send 165 where 166 R: XrpcRequest + Send + Sync, 167 <R as XrpcRequest>::Response: Send + Sync, 168 Self: Sync, 169 { 170 async { 171 let guard = self.session.read().await; 172 if let Some(session) = guard.clone() { 173 session.send(request).await 174 } else { 175 self.oauth_client.send(request).await 176 } 177 } 178 } 179 180 #[doc = " Send an XRPC request and parse the response"] 181 #[cfg(not(target_arch = "wasm32"))] 182 fn send_with_opts<R>( 183 &self, 184 request: R, 185 opts: CallOptions<'_>, 186 ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send 187 where 188 R: XrpcRequest + Send + Sync, 189 <R as XrpcRequest>::Response: Send + Sync, 190 Self: Sync, 191 { 192 async { 193 let guard = self.session.read().await; 194 if let Some(session) = guard.clone() { 195 session.send_with_opts(request, opts).await 196 } else { 197 self.oauth_client.send_with_opts(request, opts).await 198 } 199 } 200 } 201 202 #[doc = " Send an XRPC request and parse the response"] 203 #[cfg(target_arch = "wasm32")] 204 fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 205 where 206 R: XrpcRequest + Send + Sync, 207 <R as XrpcRequest>::Response: Send + Sync, 208 { 209 async { 210 let guard = self.session.read().await; 211 if let Some(session) = guard.clone() { 212 session.send(request).await 213 } else { 214 self.oauth_client.send(request).await 215 } 216 } 217 } 218 219 #[doc = " Send an XRPC request and parse the response"] 220 #[cfg(target_arch = "wasm32")] 221 fn send_with_opts<R>( 222 &self, 223 request: R, 224 opts: CallOptions<'_>, 225 ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 226 where 227 R: XrpcRequest + Send + Sync, 228 <R as XrpcRequest>::Response: Send + Sync, 229 { 230 async { 231 let guard = self.session.read().await; 232 if let Some(session) = guard.clone() { 233 session.send_with_opts(request, opts).await 234 } else { 235 self.oauth_client.send_with_opts(request, opts).await 236 } 237 } 238 } 239 240 #[doc = " Set the base URI for the client."] 241 fn set_base_uri(&self, url: jacquard::url::Url) -> impl Future<Output = ()> + Send { 242 async { 243 let guard = self.session.read().await; 244 if let Some(session) = guard.clone() { 245 session.set_base_uri(url).await 246 } else { 247 self.oauth_client.set_base_uri(url).await 248 } 249 } 250 } 251 252 #[doc = " Get the call options for the client."] 253 fn opts(&self) -> impl Future<Output = CallOptions<'_>> + Send { 254 async { 255 let guard = self.session.read().await; 256 if let Some(session) = guard.clone() { 257 session.opts().await.into_static() 258 } else { 259 self.oauth_client.opts().await 260 } 261 } 262 } 263 264 #[doc = " Set the call options for the client."] 265 fn set_opts(&self, opts: CallOptions) -> impl Future<Output = ()> + Send { 266 async { 267 let guard = self.session.read().await; 268 if let Some(session) = guard.clone() { 269 session.set_opts(opts).await 270 } else { 271 self.oauth_client.set_opts(opts).await 272 } 273 } 274 } 275} 276 277impl IdentityResolver for Client { 278 #[doc = " Access options for validation decisions in default methods"] 279 fn options(&self) -> &ResolverOptions { 280 self.oauth_client.client.options() 281 } 282 283 #[doc = " Resolve handle"] 284 #[cfg(not(target_arch = "wasm32"))] 285 fn resolve_handle( 286 &self, 287 handle: &Handle<'_>, 288 ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> + Send 289 where 290 Self: Sync, 291 { 292 self.oauth_client.client.resolve_handle(handle) 293 } 294 295 #[doc = " Resolve DID document"] 296 #[cfg(not(target_arch = "wasm32"))] 297 fn resolve_did_doc( 298 &self, 299 did: &Did<'_>, 300 ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> + Send 301 where 302 Self: Sync, 303 { 304 self.oauth_client.client.resolve_did_doc(did) 305 } 306 307 #[doc = " Resolve handle"] 308 #[cfg(target_arch = "wasm32")] 309 fn resolve_handle( 310 &self, 311 handle: &Handle<'_>, 312 ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> { 313 self.oauth_client.client.resolve_handle(handle) 314 } 315 316 #[doc = " Resolve DID document"] 317 #[cfg(target_arch = "wasm32")] 318 fn resolve_did_doc( 319 &self, 320 did: &Did<'_>, 321 ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> { 322 self.oauth_client.client.resolve_did_doc(did) 323 } 324} 325 326impl LexiconSchemaResolver for Client { 327 #[cfg(not(target_arch = "wasm32"))] 328 async fn resolve_lexicon_schema( 329 &self, 330 nsid: &Nsid<'_>, 331 ) -> std::result::Result<ResolvedLexiconSchema<'static>, LexiconResolutionError> { 332 self.oauth_client.client.resolve_lexicon_schema(nsid).await 333 } 334 335 #[cfg(target_arch = "wasm32")] 336 async fn resolve_lexicon_schema( 337 &self, 338 nsid: &Nsid<'_>, 339 ) -> std::result::Result<ResolvedLexiconSchema<'static>, LexiconResolutionError> { 340 self.oauth_client.client.resolve_lexicon_schema(nsid).await 341 } 342} 343 344impl AgentSession for Client { 345 #[doc = " Identify the kind of session."] 346 fn session_kind(&self) -> AgentKind { 347 self.oauth_client.session_kind() 348 } 349 350 #[doc = " Return current DID and an optional session id (always Some for OAuth)."] 351 async fn session_info(&self) -> Option<(Did<'static>, Option<CowStr<'static>>)> { 352 let guard = self.session.read().await; 353 if let Some(session) = guard.clone() { 354 session.info().await 355 } else { 356 None 357 } 358 } 359 360 #[doc = " Current base endpoint."] 361 async fn endpoint(&self) -> CowStr<'static> { 362 let guard = self.session.read().await; 363 if let Some(session) = guard.clone() { 364 session.endpoint().await 365 } else { 366 self.oauth_client.endpoint().await 367 } 368 } 369 370 #[doc = " Override per-session call options."] 371 async fn set_options<'a>(&'a self, opts: CallOptions<'a>) { 372 let guard = self.session.read().await; 373 if let Some(session) = guard.clone() { 374 session.set_options(opts).await 375 } else { 376 self.oauth_client.set_options(opts).await 377 } 378 } 379 380 #[doc = " Refresh the session and return a fresh AuthorizationToken."] 381 async fn refresh(&self) -> XrpcResult<AuthorizationToken<'static>> { 382 let guard = self.session.read().await; 383 if let Some(session) = guard.clone() { 384 session.refresh().await 385 } else { 386 Err(ClientError::auth( 387 jacquard::error::AuthError::NotAuthenticated, 388 )) 389 } 390 } 391} 392 393#[derive(Clone)] 394pub struct Fetcher { 395 pub client: Arc<Client>, 396 #[cfg(feature = "server")] 397 book_cache: cache_impl::Cache< 398 (AtIdentifier<'static>, SmolStr), 399 Arc<(NotebookView<'static>, Vec<BookEntryView<'static>>)>, 400 >, 401 /// Maps notebook title OR path to ident (book_cache accepts either as key) 402 #[cfg(feature = "server")] 403 notebook_key_cache: cache_impl::Cache<SmolStr, AtIdentifier<'static>>, 404 #[cfg(feature = "server")] 405 entry_cache: cache_impl::Cache< 406 (AtIdentifier<'static>, SmolStr), 407 Arc<(BookEntryView<'static>, Entry<'static>)>, 408 >, 409 #[cfg(feature = "server")] 410 profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 411 #[cfg(feature = "server")] 412 standalone_entry_cache: 413 cache_impl::Cache<(AtIdentifier<'static>, SmolStr), Arc<StandaloneEntryData>>, 414} 415 416impl Fetcher { 417 pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 418 // Set indexer URL for unauthenticated requests 419 #[cfg(feature = "use-index")] 420 if !crate::env::WEAVER_INDEXER_URL.is_empty() { 421 if let Ok(url) = jacquard::url::Url::parse(crate::env::WEAVER_INDEXER_URL) { 422 if let Ok(mut guard) = client.endpoint.try_write() { 423 use jacquard::cowstr::ToCowStr; 424 425 *guard = Some(url.to_cowstr().into_static()); 426 } 427 } 428 } 429 430 Self { 431 client: Arc::new(Client::new(client)), 432 #[cfg(feature = "server")] 433 book_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)), 434 #[cfg(feature = "server")] 435 notebook_key_cache: cache_impl::new_cache(500, std::time::Duration::from_secs(30)), 436 #[cfg(feature = "server")] 437 entry_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)), 438 #[cfg(feature = "server")] 439 profile_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(1800)), 440 #[cfg(feature = "server")] 441 standalone_entry_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)), 442 } 443 } 444 445 pub async fn upgrade_to_authenticated( 446 &self, 447 session: OAuthSession<JacquardResolver, crate::auth::AuthStore>, 448 ) { 449 let agent = Arc::new(Agent::new(session)); 450 451 // When use-index is enabled, set the atproto_proxy header for service proxying 452 #[cfg(feature = "use-index")] 453 if !crate::env::WEAVER_INDEXER_DID.is_empty() { 454 let proxy_value = format!("{}#atproto_index", crate::env::WEAVER_INDEXER_DID); 455 let mut opts = agent.opts().await; 456 opts.atproto_proxy = Some(CowStr::from(proxy_value)); 457 agent.set_opts(opts).await; 458 } 459 460 let mut session_slot = self.client.session.write().await; 461 *session_slot = Some(agent); 462 } 463 464 pub async fn downgrade_to_unauthenticated(&self) { 465 let mut session_slot = self.client.session.write().await; 466 if let Some(session) = session_slot.take() { 467 session.inner().logout().await.ok(); 468 } 469 } 470 471 #[allow(dead_code)] 472 pub async fn current_did(&self) -> Option<Did<'static>> { 473 let session_slot = self.client.session.read().await; 474 if let Some(session) = session_slot.as_ref() { 475 session.info().await.map(|(d, _)| d) 476 } else { 477 None 478 } 479 } 480 481 pub fn get_client(&self) -> Arc<Client> { 482 self.client.clone() 483 } 484 485 pub async fn get_notebook( 486 &self, 487 ident: AtIdentifier<'static>, 488 title: SmolStr, 489 ) -> Result<Option<Arc<(NotebookView<'static>, Vec<BookEntryView<'static>>)>>> { 490 #[cfg(feature = "server")] 491 if let Some(cached) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 492 return Ok(Some(cached)); 493 } 494 495 let client = self.get_client(); 496 if let Some((notebook, entries)) = client 497 .notebook_by_title(&ident, &title) 498 .await 499 .map_err(|e| dioxus::CapturedError::from_display(e))? 500 { 501 let stored = Arc::new((notebook, entries)); 502 #[cfg(feature = "server")] 503 { 504 // Cache by title 505 cache_impl::insert(&self.notebook_key_cache, title.clone(), ident.clone()); 506 cache_impl::insert(&self.book_cache, (ident.clone(), title), stored.clone()); 507 // Also cache by path if available 508 if let Some(path) = stored.0.path.as_ref() { 509 let path: SmolStr = path.as_ref().into(); 510 cache_impl::insert(&self.notebook_key_cache, path.clone(), ident.clone()); 511 cache_impl::insert(&self.book_cache, (ident, path), stored.clone()); 512 } 513 } 514 Ok(Some(stored)) 515 } else { 516 Err(dioxus::CapturedError::from_display("Notebook not found")) 517 } 518 } 519 520 /// Get notebook by title or path (for image resolution without knowing owner). 521 /// Checks notebook_key_cache first, falls back to UFOS discovery. 522 #[cfg(feature = "server")] 523 pub async fn get_notebook_by_key( 524 &self, 525 key: &str, 526 ) -> Result<Option<Arc<(NotebookView<'static>, Vec<BookEntryView<'static>>)>>> { 527 let key: SmolStr = key.into(); 528 529 // Check cache first (key could be title or path) 530 if let Some(ident) = cache_impl::get(&self.notebook_key_cache, &key) { 531 return self.get_notebook(ident, key).await; 532 } 533 534 // Fallback: query UFOS and populate caches 535 let notebooks = self.fetch_notebooks_from_ufos().await?; 536 let notebook = notebooks.into_iter().find(|arc| { 537 let (view, _) = arc.as_ref(); 538 view.title.as_deref() == Some(key.as_str()) 539 || view.path.as_deref() == Some(key.as_str()) 540 }); 541 if let Some(notebook) = notebook { 542 let ident = notebook.0.uri.authority().clone().into_static(); 543 return self.get_notebook(ident, key).await; 544 } 545 Ok(None) 546 } 547 548 pub async fn get_entry( 549 &self, 550 ident: AtIdentifier<'static>, 551 book_title: SmolStr, 552 entry_title: SmolStr, 553 ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 554 #[cfg(feature = "server")] 555 if let Some(cached) = 556 cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 557 { 558 return Ok(Some(cached)); 559 } 560 561 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 562 let (notebook, entries) = result.as_ref(); 563 if let Some(entry) = entries.iter().find(|e| { 564 if let Some(path) = e.entry.path.as_deref() { 565 path == entry_title.as_str() 566 } else if let Some(title) = e.entry.title.as_deref() { 567 title_matches(title, &entry_title) 568 } else { 569 false 570 } 571 }) { 572 let stored = Arc::new(( 573 entry.clone(), 574 from_data_owned(entry.entry.record.clone()).expect("should deserialize"), 575 )); 576 #[cfg(feature = "server")] 577 cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 578 Ok(Some(stored)) 579 } else { 580 Err(dioxus::CapturedError::from_display("Entry not found")) 581 } 582 } else { 583 Err(dioxus::CapturedError::from_display("Notebook not found")) 584 } 585 } 586 587 #[cfg(feature = "use-index")] 588 pub async fn fetch_notebooks_from_ufos( 589 &self, 590 ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 591 use weaver_api::sh_weaver::notebook::book::Book; 592 use weaver_api::sh_weaver::notebook::get_notebook_feed::GetNotebookFeed; 593 594 let client = self.get_client(); 595 596 let resp = client 597 .send(GetNotebookFeed::new().limit(100).build()) 598 .await 599 .map_err(|e| dioxus::CapturedError::from_display(e))?; 600 601 let output = resp 602 .into_output() 603 .map_err(|e| dioxus::CapturedError::from_display(e))?; 604 605 let mut notebooks = Vec::new(); 606 607 for notebook in output.notebooks { 608 // Extract entry_list from the record 609 let book: Book = jacquard::from_data(&notebook.record) 610 .map_err(|e| dioxus::CapturedError::from_display(e))?; 611 let book = book.into_static(); 612 613 let entries: Vec<StrongRef<'static>> = book 614 .entry_list 615 .into_iter() 616 .map(IntoStatic::into_static) 617 .collect(); 618 619 let ident = notebook.uri.authority().clone().into_static(); 620 let title = notebook 621 .title 622 .as_ref() 623 .map(|t| SmolStr::new(t.as_ref())) 624 .unwrap_or_else(|| SmolStr::new("Untitled")); 625 626 let result = Arc::new((notebook.into_static(), entries)); 627 #[cfg(feature = "server")] 628 { 629 cache_impl::insert(&self.notebook_key_cache, title.clone(), ident.clone()); 630 #[cfg(not(feature = "use-index"))] 631 cache_impl::insert(&self.book_cache, (ident.clone(), title), result.clone()); 632 633 if let Some(path) = result.0.path.as_ref() { 634 let path: SmolStr = path.as_ref().into(); 635 cache_impl::insert(&self.notebook_key_cache, path.clone(), ident.clone()); 636 #[cfg(not(feature = "use-index"))] 637 cache_impl::insert(&self.book_cache, (ident, path), result.clone()); 638 } 639 } 640 notebooks.push(result); 641 } 642 643 Ok(notebooks) 644 } 645 646 #[cfg(not(feature = "use-index"))] 647 pub async fn fetch_notebooks_from_ufos( 648 &self, 649 ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 650 use jacquard::{IntoStatic, types::aturi::AtUri}; 651 652 let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book"; 653 let response = reqwest::get(url) 654 .await 655 .map_err(|e| dioxus::CapturedError::from_display(e))?; 656 657 let records: Vec<UfosRecord> = response 658 .json() 659 .await 660 .map_err(|e| dioxus::CapturedError::from_display(e))?; 661 662 let mut notebooks = Vec::new(); 663 let client = self.get_client(); 664 665 for ufos_record in records { 666 // Construct URI 667 let uri_str = format_smolstr!( 668 "at://{}/{}/{}", 669 ufos_record.did, 670 ufos_record.collection, 671 ufos_record.rkey 672 ); 673 let uri = AtUri::new_owned(uri_str).map_err(|e| { 674 dioxus::CapturedError::from_display(format_smolstr!("Invalid URI: {}", e).as_str()) 675 })?; 676 match client.view_notebook(&uri).await { 677 Ok((notebook, entries)) => { 678 let ident = uri.authority().clone().into_static(); 679 let title = notebook 680 .title 681 .as_ref() 682 .map(|t| SmolStr::new(t.as_ref())) 683 .unwrap_or_else(|| SmolStr::new("Untitled")); 684 685 let result = Arc::new((notebook, entries)); 686 #[cfg(feature = "server")] 687 { 688 // Cache by title 689 cache_impl::insert(&self.notebook_key_cache, title.clone(), ident.clone()); 690 691 #[cfg(not(feature = "use-index"))] 692 cache_impl::insert( 693 &self.book_cache, 694 (ident.clone(), title), 695 result.clone(), 696 ); 697 // Also cache by path if available 698 if let Some(path) = result.0.path.as_ref() { 699 let path: SmolStr = path.as_ref().into(); 700 cache_impl::insert( 701 &self.notebook_key_cache, 702 path.clone(), 703 ident.clone(), 704 ); 705 706 #[cfg(not(feature = "use-index"))] 707 cache_impl::insert(&self.book_cache, (ident, path), result.clone()); 708 } 709 } 710 notebooks.push(result); 711 } 712 Err(_) => continue, // Skip notebooks that fail to load 713 } 714 } 715 716 Ok(notebooks) 717 } 718 719 /// Fetch entries from index feed (reverse chronological) 720 #[cfg(feature = "use-index")] 721 pub async fn fetch_entries_from_ufos( 722 &self, 723 ) -> Result<Vec<Arc<(EntryView<'static>, Entry<'static>, u64)>>> { 724 use jacquard::IntoStatic; 725 use weaver_api::sh_weaver::notebook::entry::Entry; 726 use weaver_api::sh_weaver::notebook::get_entry_feed::GetEntryFeed; 727 728 let client = self.get_client(); 729 730 let resp = client 731 .send(GetEntryFeed::new().limit(100).build()) 732 .await 733 .map_err(|e| dioxus::CapturedError::from_display(e))?; 734 735 let output = resp 736 .into_output() 737 .map_err(|e| dioxus::CapturedError::from_display(e))?; 738 739 let mut entries = Vec::new(); 740 741 for feed_entry in output.feed { 742 let entry_view = feed_entry.entry; 743 // indexed_at is ISO datetime, parse to get millisecond timestamp 744 let timestamp = chrono::DateTime::parse_from_rfc3339(entry_view.indexed_at.as_str()) 745 .map(|dt| dt.timestamp_millis() as u64) 746 .unwrap_or(0); 747 748 let entry: Entry = jacquard::from_data(&entry_view.record) 749 .map_err(|e| dioxus::CapturedError::from_display(e))?; 750 let entry = entry.into_static(); 751 752 entries.push(Arc::new((entry_view.into_static(), entry, timestamp))); 753 } 754 755 Ok(entries) 756 } 757 758 /// Fetch entries from UFOS discovery service (reverse chronological) 759 #[cfg(not(feature = "use-index"))] 760 pub async fn fetch_entries_from_ufos( 761 &self, 762 ) -> Result<Vec<Arc<(EntryView<'static>, Entry<'static>, u64)>>> { 763 use jacquard::{IntoStatic, types::aturi::AtUri, types::ident::AtIdentifier}; 764 765 let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.entry"; 766 767 let response = reqwest::get(url).await.map_err(|e| { 768 tracing::error!("[fetch_entries_from_ufos] request failed: {:?}", e); 769 dioxus::CapturedError::from_display(e) 770 })?; 771 772 let mut records: Vec<UfosRecord> = response.json().await.map_err(|e| { 773 tracing::error!("[fetch_entries_from_ufos] json parse failed: {:?}", e); 774 dioxus::CapturedError::from_display(e) 775 })?; 776 records.sort_by(|a, b| b.time_us.cmp(&a.time_us)); 777 778 let mut entries = Vec::new(); 779 let client = self.get_client(); 780 781 for ufos_record in records { 782 let did = match Did::new(&ufos_record.did) { 783 Ok(d) => d.into_static(), 784 Err(e) => { 785 tracing::warn!( 786 "[fetch_entries_from_ufos] invalid DID {}: {:?}", 787 ufos_record.did, 788 e 789 ); 790 continue; 791 } 792 }; 793 let ident = AtIdentifier::Did(did); 794 match client.fetch_entry_by_rkey(&ident, &ufos_record.rkey).await { 795 Ok((entry_view, entry)) => { 796 entries.push(Arc::new(( 797 entry_view.into_static(), 798 entry.into_static(), 799 ufos_record.time_us, 800 ))); 801 } 802 Err(e) => { 803 tracing::warn!( 804 "[fetch_entries_from_ufos] failed to load entry {}: {:?}", 805 ufos_record.rkey, 806 e 807 ); 808 continue; 809 } 810 } 811 } 812 813 Ok(entries) 814 } 815 816 #[cfg(feature = "use-index")] 817 pub async fn fetch_notebooks_for_did( 818 &self, 819 ident: &AtIdentifier<'_>, 820 ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 821 use weaver_api::sh_weaver::actor::get_actor_notebooks::GetActorNotebooks; 822 use weaver_api::sh_weaver::notebook::book::Book; 823 824 let client = self.get_client(); 825 826 let resp = client 827 .send( 828 GetActorNotebooks::new() 829 .actor(ident.clone()) 830 .limit(100) 831 .build(), 832 ) 833 .await 834 .map_err(|e| dioxus::CapturedError::from_display(e))?; 835 836 let output = resp 837 .into_output() 838 .map_err(|e| dioxus::CapturedError::from_display(e))?; 839 840 let mut notebooks = Vec::new(); 841 842 for notebook in output.notebooks { 843 // Extract entry_list from the record 844 let book: Book = jacquard::from_data(&notebook.record) 845 .map_err(|e| dioxus::CapturedError::from_display(e))?; 846 let book = book.into_static(); 847 848 let entries: Vec<StrongRef<'static>> = book 849 .entry_list 850 .into_iter() 851 .map(IntoStatic::into_static) 852 .collect(); 853 854 let ident_static = notebook.uri.authority().clone().into_static(); 855 let title = notebook 856 .title 857 .as_ref() 858 .map(|t| SmolStr::new(t.as_ref())) 859 .unwrap_or_else(|| SmolStr::new("Untitled")); 860 861 let result = Arc::new((notebook.into_static(), entries)); 862 #[cfg(feature = "server")] 863 { 864 cache_impl::insert( 865 &self.notebook_key_cache, 866 title.clone(), 867 ident_static.clone(), 868 ); 869 if let Some(path) = result.0.path.as_ref() { 870 let path: SmolStr = path.as_ref().into(); 871 cache_impl::insert( 872 &self.notebook_key_cache, 873 path.clone(), 874 ident_static.clone(), 875 ); 876 } 877 } 878 notebooks.push(result); 879 } 880 881 Ok(notebooks) 882 } 883 884 #[cfg(not(feature = "use-index"))] 885 pub async fn fetch_notebooks_for_did( 886 &self, 887 ident: &AtIdentifier<'_>, 888 ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 889 use jacquard::{ 890 IntoStatic, 891 types::{collection::Collection, nsid::Nsid}, 892 xrpc::XrpcExt, 893 }; 894 use weaver_api::{ 895 com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book, 896 }; 897 898 let client = self.get_client(); 899 900 // Resolve DID and PDS 901 let (repo_did, pds_url) = match ident { 902 AtIdentifier::Did(did) => { 903 let pds = client 904 .pds_for_did(did) 905 .await 906 .map_err(|e| dioxus::CapturedError::from_display(e))?; 907 (did.clone(), pds) 908 } 909 AtIdentifier::Handle(handle) => client 910 .pds_for_handle(handle) 911 .await 912 .map_err(|e| dioxus::CapturedError::from_display(e))?, 913 }; 914 915 // Fetch all notebook records for this repo 916 tracing::info!( 917 "fetch_notebooks_for_did: pds_url={}, repo_did={}", 918 pds_url, 919 repo_did 920 ); 921 922 let resp = client 923 .xrpc(pds_url.clone()) 924 .send( 925 &ListRecords::new() 926 .repo(repo_did) 927 .collection(Nsid::raw(Book::NSID)) 928 .limit(100) 929 .build(), 930 ) 931 .await 932 .map_err(|e| { 933 tracing::error!( 934 "fetch_notebooks_for_did: xrpc failed: {} pds url {}", 935 e, 936 pds_url 937 ); 938 dioxus::CapturedError::from_display(e) 939 })?; 940 941 let mut notebooks = Vec::new(); 942 943 if let Ok(list) = resp.parse() { 944 for record in list.records { 945 // View the notebook (which hydrates authors) 946 match client.view_notebook(&record.uri).await { 947 Ok((notebook, entries)) => { 948 let ident = record.uri.authority().clone().into_static(); 949 let title = notebook 950 .title 951 .as_ref() 952 .map(|t| SmolStr::new(t.as_ref())) 953 .unwrap_or_else(|| SmolStr::new("Untitled")); 954 955 let result = Arc::new((notebook, entries)); 956 #[cfg(feature = "server")] 957 { 958 // Cache by title 959 cache_impl::insert( 960 &self.notebook_key_cache, 961 title.clone(), 962 ident.clone(), 963 ); 964 cache_impl::insert( 965 &self.book_cache, 966 (ident.clone(), title), 967 result.clone(), 968 ); 969 // Also cache by path if available 970 if let Some(path) = result.0.path.as_ref() { 971 let path: SmolStr = path.as_ref().into(); 972 cache_impl::insert( 973 &self.notebook_key_cache, 974 path.clone(), 975 ident.clone(), 976 ); 977 cache_impl::insert(&self.book_cache, (ident, path), result.clone()); 978 } 979 } 980 notebooks.push(result); 981 } 982 Err(e) => { 983 tracing::warn!( 984 "fetch_notebooks_for_did: view_notebook failed for {}: {}", 985 record.uri, 986 e 987 ); 988 continue; 989 } 990 } 991 } 992 } 993 Ok(notebooks) 994 } 995 996 /// Fetch all entries for a DID (for profile timeline) 997 #[cfg(feature = "use-index")] 998 pub async fn fetch_entries_for_did( 999 &self, 1000 ident: &AtIdentifier<'_>, 1001 ) -> Result<Vec<Arc<(EntryView<'static>, Entry<'static>)>>> { 1002 use weaver_api::sh_weaver::actor::get_actor_entries::GetActorEntries; 1003 1004 let client = self.get_client(); 1005 1006 let resp = client 1007 .send( 1008 GetActorEntries::new() 1009 .actor(ident.clone()) 1010 .limit(100) 1011 .build(), 1012 ) 1013 .await 1014 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1015 1016 let output = resp 1017 .into_output() 1018 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1019 1020 let mut entries = Vec::new(); 1021 1022 for entry_view in output.entries { 1023 // Deserialize Entry from the record field 1024 let entry: Entry = jacquard::from_data(&entry_view.record) 1025 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1026 let entry = entry.into_static(); 1027 1028 entries.push(Arc::new((entry_view.into_static(), entry))); 1029 } 1030 1031 Ok(entries) 1032 } 1033 1034 /// Fetch all entries for a DID (for profile timeline) 1035 #[cfg(not(feature = "use-index"))] 1036 pub async fn fetch_entries_for_did( 1037 &self, 1038 ident: &AtIdentifier<'_>, 1039 ) -> Result<Vec<Arc<(EntryView<'static>, Entry<'static>)>>> { 1040 use jacquard::{ 1041 IntoStatic, 1042 types::{collection::Collection, nsid::Nsid}, 1043 xrpc::XrpcExt, 1044 }; 1045 use weaver_api::com_atproto::repo::list_records::ListRecords; 1046 1047 let client = self.get_client(); 1048 1049 // Resolve DID and PDS 1050 let (repo_did, pds_url) = match ident { 1051 AtIdentifier::Did(did) => { 1052 let pds = client 1053 .pds_for_did(did) 1054 .await 1055 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1056 (did.clone(), pds) 1057 } 1058 AtIdentifier::Handle(handle) => client 1059 .pds_for_handle(handle) 1060 .await 1061 .map_err(|e| dioxus::CapturedError::from_display(e))?, 1062 }; 1063 1064 // Fetch all entry records for this repo 1065 let resp = client 1066 .xrpc(pds_url) 1067 .send( 1068 &ListRecords::new() 1069 .repo(repo_did) 1070 .collection(Nsid::raw(Entry::NSID)) 1071 .limit(100) 1072 .build(), 1073 ) 1074 .await 1075 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1076 1077 let mut entries = Vec::new(); 1078 let ident_static = ident.clone().into_static(); 1079 1080 if let Ok(list) = resp.parse() { 1081 for record in list.records { 1082 // Extract rkey from URI 1083 let rkey = record.uri.rkey().map(|r| r.0.as_str()).unwrap_or_default(); 1084 1085 // Fetch the entry with hydration 1086 match client.fetch_entry_by_rkey(&ident_static, rkey).await { 1087 Ok((entry_view, entry)) => { 1088 entries.push(Arc::new((entry_view.into_static(), entry.into_static()))); 1089 } 1090 Err(e) => { 1091 tracing::warn!( 1092 "[fetch_entries_for_did] failed to load entry {}: {:?}", 1093 rkey, 1094 e 1095 ); 1096 continue; 1097 } 1098 } 1099 } 1100 } 1101 1102 Ok(entries) 1103 } 1104 1105 pub async fn list_notebook_entries( 1106 &self, 1107 ident: AtIdentifier<'static>, 1108 book_title: SmolStr, 1109 ) -> Result<Option<Vec<BookEntryView<'static>>>> { 1110 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 1111 Ok(Some(result.as_ref().1.clone())) 1112 } else { 1113 Err(dioxus::CapturedError::from_display("Notebook not found")) 1114 } 1115 } 1116 1117 pub async fn fetch_profile( 1118 &self, 1119 ident: &AtIdentifier<'_>, 1120 ) -> Result<Arc<ProfileDataView<'static>>> { 1121 #[cfg(feature = "server")] 1122 use jacquard::IntoStatic; 1123 1124 #[cfg(feature = "server")] 1125 let ident_static = ident.clone().into_static(); 1126 1127 #[cfg(feature = "server")] 1128 if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 1129 return Ok(cached); 1130 } 1131 1132 let client = self.get_client(); 1133 1134 let (_uri, profile_view) = client 1135 .hydrate_profile_view(&ident) 1136 .await 1137 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1138 1139 let result = Arc::new(profile_view); 1140 #[cfg(feature = "server")] 1141 cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 1142 1143 Ok(result) 1144 } 1145 1146 /// Fetch an entry by rkey with optional notebook context lookup. 1147 pub async fn get_entry_by_rkey( 1148 &self, 1149 ident: AtIdentifier<'static>, 1150 rkey: SmolStr, 1151 ) -> Result<Option<Arc<StandaloneEntryData>>> { 1152 use jacquard::types::aturi::AtUri; 1153 1154 #[cfg(feature = "server")] 1155 if let Some(cached) = 1156 cache_impl::get(&self.standalone_entry_cache, &(ident.clone(), rkey.clone())) 1157 { 1158 return Ok(Some(cached)); 1159 } 1160 1161 let client = self.get_client(); 1162 1163 // Fetch entry directly by rkey 1164 let (entry_view, entry) = client 1165 .fetch_entry_by_rkey(&ident, &rkey) 1166 .await 1167 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1168 1169 // Try to find notebook context via constellation 1170 let entry_uri = entry_view.uri.clone(); 1171 let at_uri = AtUri::new(entry_uri.as_ref()).map_err(|e| { 1172 dioxus::CapturedError::from_display( 1173 format_smolstr!("Invalid entry URI: {}", e).as_str(), 1174 ) 1175 })?; 1176 1177 let (total, first_notebook) = client 1178 .find_notebooks_for_entry(&at_uri) 1179 .await 1180 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1181 1182 // Only provide notebook context if entry is in exactly one notebook 1183 let notebook_context = if total == 1 { 1184 if let Some(notebook_id) = first_notebook { 1185 // Construct notebook URI from RecordId 1186 let notebook_uri_str = format_smolstr!( 1187 "at://{}/{}/{}", 1188 notebook_id.did.as_str(), 1189 notebook_id.collection.as_str(), 1190 notebook_id.rkey.0.as_str() 1191 ); 1192 let notebook_uri = AtUri::new_owned(notebook_uri_str).map_err(|e| { 1193 dioxus::CapturedError::from_display( 1194 format_smolstr!("Invalid notebook URI: {}", e).as_str(), 1195 ) 1196 })?; 1197 1198 // Fetch notebook and find entry position 1199 if let Ok((notebook, entries)) = client.view_notebook(&notebook_uri).await { 1200 if let Ok(Some(book_entry_view)) = client 1201 .entry_in_notebook_by_rkey(&notebook, &entries, &rkey) 1202 .await 1203 { 1204 Some(NotebookContext { 1205 notebook: notebook.into_static(), 1206 book_entry_view: book_entry_view.into_static(), 1207 }) 1208 } else { 1209 None 1210 } 1211 } else { 1212 None 1213 } 1214 } else { 1215 None 1216 } 1217 } else { 1218 None 1219 }; 1220 1221 let result = Arc::new(StandaloneEntryData { 1222 entry, 1223 entry_view, 1224 notebook_context, 1225 }); 1226 #[cfg(feature = "server")] 1227 cache_impl::insert(&self.standalone_entry_cache, (ident, rkey), result.clone()); 1228 1229 Ok(Some(result)) 1230 } 1231 1232 /// Fetch an entry by rkey within a specific notebook context. 1233 /// 1234 /// The book_title parameter provides the notebook context. 1235 /// Returns BookEntryView without prev/next if entry is in multiple notebooks. 1236 pub async fn get_notebook_entry_by_rkey( 1237 &self, 1238 ident: AtIdentifier<'static>, 1239 book_title: SmolStr, 1240 rkey: SmolStr, 1241 ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 1242 use jacquard::types::aturi::AtUri; 1243 1244 #[cfg(feature = "server")] 1245 if let Some(cached) = cache_impl::get(&self.entry_cache, &(ident.clone(), rkey.clone())) { 1246 return Ok(Some(cached)); 1247 } 1248 1249 let client = self.get_client(); 1250 1251 // Fetch entry directly by rkey 1252 let (entry_view, entry) = client 1253 .fetch_entry_by_rkey(&ident, &rkey) 1254 .await 1255 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1256 1257 // Fetch notebook by title 1258 let notebook_result = client 1259 .notebook_by_title(&ident, &book_title) 1260 .await 1261 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1262 1263 let (notebook, entries) = match notebook_result { 1264 Some((n, e)) => (n, e), 1265 None => return Err(dioxus::CapturedError::from_display("Notebook not found")), 1266 }; 1267 1268 // Find entry position in notebook 1269 let book_entry_view = entries 1270 .iter() 1271 .find(|e| e.entry.uri.rkey().as_ref().map(|k| k.as_ref()) == Some(rkey.as_ref())); 1272 1273 let mut book_entry_view = match book_entry_view { 1274 Some(bev) => bev.clone(), 1275 None => { 1276 // Entry not in this notebook's entry list - return basic view without nav 1277 use weaver_api::sh_weaver::notebook::BookEntryView; 1278 BookEntryView::new().entry(entry_view).index(0).build() 1279 } 1280 }; 1281 1282 // Check if entry is in multiple notebooks - if so, clear prev/next 1283 let entry_uri = book_entry_view.entry.uri.clone(); 1284 let at_uri = AtUri::new(entry_uri.as_ref()).map_err(|e| { 1285 dioxus::CapturedError::from_display( 1286 format_smolstr!("Invalid entry URI: {}", e).as_str(), 1287 ) 1288 })?; 1289 1290 let (total, _) = client 1291 .find_notebooks_for_entry(&at_uri) 1292 .await 1293 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1294 1295 if total >= 2 { 1296 // Entry is in multiple notebooks - clear prev/next to avoid ambiguity 1297 book_entry_view = BookEntryView::new() 1298 .entry(book_entry_view.entry) 1299 .index(book_entry_view.index) 1300 .build(); 1301 } 1302 1303 let result = Arc::new((book_entry_view.into_static(), entry)); 1304 #[cfg(feature = "server")] 1305 cache_impl::insert(&self.entry_cache, (ident, rkey), result.clone()); 1306 1307 Ok(Some(result)) 1308 } 1309} 1310 1311impl HttpClient for Fetcher { 1312 type Error = IdentityError; 1313 1314 #[cfg(not(target_arch = "wasm32"))] 1315 fn send_http( 1316 &self, 1317 request: http::Request<Vec<u8>>, 1318 ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> + Send 1319 { 1320 async { 1321 let client = self.get_client(); 1322 client.send_http(request).await 1323 } 1324 } 1325 1326 #[cfg(target_arch = "wasm32")] 1327 fn send_http( 1328 &self, 1329 request: http::Request<Vec<u8>>, 1330 ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> { 1331 async { 1332 let client = self.get_client(); 1333 client.send_http(request).await 1334 } 1335 } 1336} 1337 1338impl XrpcClient for Fetcher { 1339 #[doc = " Get the base URI for the client."] 1340 fn base_uri(&self) -> impl Future<Output = CowStr<'static>> + Send { 1341 self.client.base_uri() 1342 } 1343 1344 #[doc = " Send an XRPC request and parse the response"] 1345 #[cfg(not(target_arch = "wasm32"))] 1346 fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send 1347 where 1348 R: XrpcRequest + Send + Sync, 1349 <R as XrpcRequest>::Response: Send + Sync, 1350 Self: Sync, 1351 { 1352 self.client.send(request) 1353 } 1354 1355 #[doc = " Send an XRPC request and parse the response"] 1356 #[cfg(not(target_arch = "wasm32"))] 1357 fn send_with_opts<R>( 1358 &self, 1359 request: R, 1360 opts: CallOptions<'_>, 1361 ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send 1362 where 1363 R: XrpcRequest + Send + Sync, 1364 <R as XrpcRequest>::Response: Send + Sync, 1365 Self: Sync, 1366 { 1367 self.client.send_with_opts(request, opts) 1368 } 1369 1370 #[doc = " Send an XRPC request and parse the response"] 1371 #[cfg(target_arch = "wasm32")] 1372 fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 1373 where 1374 R: XrpcRequest + Send + Sync, 1375 <R as XrpcRequest>::Response: Send + Sync, 1376 { 1377 self.client.send(request) 1378 } 1379 1380 #[doc = " Send an XRPC request and parse the response"] 1381 #[cfg(target_arch = "wasm32")] 1382 fn send_with_opts<R>( 1383 &self, 1384 request: R, 1385 opts: CallOptions<'_>, 1386 ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 1387 where 1388 R: XrpcRequest + Send + Sync, 1389 <R as XrpcRequest>::Response: Send + Sync, 1390 { 1391 self.client.send_with_opts(request, opts) 1392 } 1393 1394 #[doc = " Set the base URI for the client."] 1395 fn set_base_uri(&self, url: jacquard::url::Url) -> impl Future<Output = ()> + Send { 1396 self.client.set_base_uri(url) 1397 } 1398 1399 #[doc = " Get the call options for the client."] 1400 fn opts(&self) -> impl Future<Output = CallOptions<'_>> + Send { 1401 self.client.opts() 1402 } 1403 1404 #[doc = " Set the call options for the client."] 1405 fn set_opts(&self, opts: CallOptions) -> impl Future<Output = ()> + Send { 1406 self.client.set_opts(opts) 1407 } 1408} 1409 1410impl IdentityResolver for Fetcher { 1411 #[doc = " Access options for validation decisions in default methods"] 1412 fn options(&self) -> &ResolverOptions { 1413 self.client.options() 1414 } 1415 1416 #[doc = " Resolve handle"] 1417 #[cfg(not(target_arch = "wasm32"))] 1418 fn resolve_handle( 1419 &self, 1420 handle: &Handle<'_>, 1421 ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> + Send 1422 where 1423 Self: Sync, 1424 { 1425 self.client.resolve_handle(handle) 1426 } 1427 1428 #[doc = " Resolve DID document"] 1429 #[cfg(not(target_arch = "wasm32"))] 1430 fn resolve_did_doc( 1431 &self, 1432 did: &Did<'_>, 1433 ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> + Send 1434 where 1435 Self: Sync, 1436 { 1437 self.client.resolve_did_doc(did) 1438 } 1439 1440 #[doc = " Resolve handle"] 1441 #[cfg(target_arch = "wasm32")] 1442 fn resolve_handle( 1443 &self, 1444 handle: &Handle<'_>, 1445 ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> { 1446 self.client.resolve_handle(handle) 1447 } 1448 1449 #[doc = " Resolve DID document"] 1450 #[cfg(target_arch = "wasm32")] 1451 fn resolve_did_doc( 1452 &self, 1453 did: &Did<'_>, 1454 ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> { 1455 self.client.resolve_did_doc(did) 1456 } 1457} 1458 1459impl LexiconSchemaResolver for Fetcher { 1460 #[cfg(not(target_arch = "wasm32"))] 1461 async fn resolve_lexicon_schema( 1462 &self, 1463 nsid: &Nsid<'_>, 1464 ) -> std::result::Result<ResolvedLexiconSchema<'static>, LexiconResolutionError> { 1465 self.client.resolve_lexicon_schema(nsid).await 1466 } 1467 1468 #[cfg(target_arch = "wasm32")] 1469 async fn resolve_lexicon_schema( 1470 &self, 1471 nsid: &Nsid<'_>, 1472 ) -> std::result::Result<ResolvedLexiconSchema<'static>, LexiconResolutionError> { 1473 self.client.resolve_lexicon_schema(nsid).await 1474 } 1475} 1476 1477// ============================================================================ 1478// Collaboration & Edit methods (use-index gated) 1479// ============================================================================ 1480 1481impl Fetcher { 1482 /// Get edit history for a resource from weaver-index. 1483 /// 1484 /// Returns edit roots and diffs for the given resource URI. 1485 #[cfg(feature = "use-index")] 1486 pub async fn get_edit_history( 1487 &self, 1488 resource_uri: &AtUri<'_>, 1489 ) -> Result<weaver_api::sh_weaver::edit::get_edit_history::GetEditHistoryOutput<'static>> { 1490 use weaver_api::sh_weaver::edit::get_edit_history::GetEditHistory; 1491 1492 let client = self.get_client(); 1493 let resp = client 1494 .send(GetEditHistory::new().resource(resource_uri.clone()).build()) 1495 .await 1496 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1497 1498 resp.into_output() 1499 .map(|o| o.into_static()) 1500 .map_err(|e| dioxus::CapturedError::from_display(e)) 1501 } 1502 1503 /// List drafts for an actor from weaver-index. 1504 #[cfg(feature = "use-index")] 1505 pub async fn list_drafts( 1506 &self, 1507 actor: &AtIdentifier<'_>, 1508 ) -> Result<weaver_api::sh_weaver::edit::list_drafts::ListDraftsOutput<'static>> { 1509 use weaver_api::sh_weaver::edit::list_drafts::ListDrafts; 1510 1511 let client = self.get_client(); 1512 let resp = client 1513 .send(ListDrafts::new().actor(actor.clone()).build()) 1514 .await 1515 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1516 1517 resp.into_output() 1518 .map(|o| o.into_static()) 1519 .map_err(|e| dioxus::CapturedError::from_display(e)) 1520 } 1521 1522 /// Get resource sessions from weaver-index. 1523 /// 1524 /// Returns active collaboration sessions for the given resource. 1525 #[cfg(feature = "use-index")] 1526 pub async fn get_resource_sessions( 1527 &self, 1528 resource_uri: &AtUri<'_>, 1529 ) -> Result< 1530 weaver_api::sh_weaver::collab::get_resource_sessions::GetResourceSessionsOutput<'static>, 1531 > { 1532 use weaver_api::sh_weaver::collab::get_resource_sessions::GetResourceSessions; 1533 1534 let client = self.get_client(); 1535 let resp = client 1536 .send( 1537 GetResourceSessions::new() 1538 .resource(resource_uri.clone()) 1539 .build(), 1540 ) 1541 .await 1542 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1543 1544 resp.into_output() 1545 .map(|o| o.into_static()) 1546 .map_err(|e| dioxus::CapturedError::from_display(e)) 1547 } 1548 1549 /// Get resource participants from weaver-index. 1550 /// 1551 /// Returns owner and collaborators who can edit the resource. 1552 #[cfg(feature = "use-index")] 1553 pub async fn get_resource_participants( 1554 &self, 1555 resource_uri: &AtUri<'_>, 1556 ) -> Result< 1557 weaver_api::sh_weaver::collab::get_resource_participants::GetResourceParticipantsOutput< 1558 'static, 1559 >, 1560 > { 1561 use weaver_api::sh_weaver::collab::get_resource_participants::GetResourceParticipants; 1562 1563 let client = self.get_client(); 1564 let resp = client 1565 .send( 1566 GetResourceParticipants::new() 1567 .resource(resource_uri.clone()) 1568 .build(), 1569 ) 1570 .await 1571 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1572 1573 resp.into_output() 1574 .map(|o| o.into_static()) 1575 .map_err(|e| dioxus::CapturedError::from_display(e)) 1576 } 1577 1578 /// Get contributors for a resource from weaver-index. 1579 #[cfg(feature = "use-index")] 1580 pub async fn get_contributors( 1581 &self, 1582 resource_uri: &AtUri<'_>, 1583 ) -> Result<weaver_api::sh_weaver::edit::get_contributors::GetContributorsOutput<'static>> { 1584 use weaver_api::sh_weaver::edit::get_contributors::GetContributors; 1585 1586 let client = self.get_client(); 1587 let resp = client 1588 .send( 1589 GetContributors::new() 1590 .resource(resource_uri.clone()) 1591 .build(), 1592 ) 1593 .await 1594 .map_err(|e| dioxus::CapturedError::from_display(e))?; 1595 1596 resp.into_output() 1597 .map(|o| o.into_static()) 1598 .map_err(|e| dioxus::CapturedError::from_display(e)) 1599 } 1600} 1601 1602impl AgentSession for Fetcher { 1603 #[doc = " Identify the kind of session."] 1604 fn session_kind(&self) -> AgentKind { 1605 self.client.session_kind() 1606 } 1607 1608 #[doc = " Return current DID and an optional session id (always Some for OAuth)."] 1609 async fn session_info(&self) -> Option<(Did<'static>, Option<CowStr<'static>>)> { 1610 self.client.session_info().await 1611 } 1612 1613 async fn endpoint(&self) -> CowStr<'static> { 1614 self.client.endpoint().await 1615 } 1616 1617 async fn set_options<'a>(&'a self, opts: CallOptions<'a>) { 1618 self.client.set_options(opts).await 1619 } 1620 1621 async fn refresh(&self) -> XrpcResult<AuthorizationToken<'static>> { 1622 self.client.refresh().await 1623 } 1624}