and now everything works well

Orual 6188b05c 17cf62c2

+346 -322
+16
Cargo.toml
··· 56 57 [profile] 58 59 [profile.wasm-dev] 60 inherits = "dev" 61 debug = true ··· 63 [profile.server-dev] 64 inherits = "dev" 65 debug = true 66 67 [profile.android-dev] 68 inherits = "dev"
··· 56 57 [profile] 58 59 + [profile.wasm-release] 60 + inherits = "release" 61 + opt-level = "z" 62 + debug = false 63 + lto = true 64 + codegen-units = 1 65 + panic = "abort" 66 + incremental = false 67 + strip = "debuginfo" 68 + 69 + 70 [profile.wasm-dev] 71 inherits = "dev" 72 debug = true ··· 74 [profile.server-dev] 75 inherits = "dev" 76 debug = true 77 + 78 + 79 + [profile.server-release] 80 + inherits = "release" 81 + opt-level = 2 82 83 [profile.android-dev] 84 inherits = "dev"
+10
crates/weaver-app/.env-example
···
··· 1 + WEAVER_APP_ENV="dev" 2 + WEAVER_APP_HOST="http://localhost" 3 + WEAVER_APP_DOMAIN="" 4 + WEAVER_PORT=8080 5 + WEAVER_APP_SCOPES="atproto transition:generic" 6 + WEAVER_CLIENT_NAME="Weaver" 7 + 8 + WEAVER_LOGO_URI="" 9 + WEAVER_TOS_URI="" 10 + WEAVER_PRIVACY_POLICY_URI=""
-10
crates/weaver-app/.env-prod
··· 1 - WEAVER_APP_ENV="prod" 2 - WEAVER_APP_HOST="https://alpha.weaver.sh" 3 - WEAVER_APP_DOMAIN="https://alpha.weaver.sh" 4 - WEAVER_PORT=8080 5 - WEAVER_APP_SCOPES="atproto transition:generic" 6 - WEAVER_CLIENT_NAME="Weaver" 7 - 8 - WEAVER_LOGO_URI="" 9 - WEAVER_TOS_URI="" 10 - WEAVER_PRIVACY_POLICY_URI=""
···
+1 -1
crates/weaver-app/Cargo.toml
··· 52 53 [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 54 webbrowser = "1.0.6" 55 - reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 56 57 [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] 58 reqwest = { version = "0.12", default-features = false, features = ["json"] }
··· 52 53 [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 54 webbrowser = "1.0.6" 55 + reqwest = { version = "0.12", default-features = false, features = ["json"] } 56 57 [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] 58 reqwest = { version = "0.12", default-features = false, features = ["json"] }
crates/weaver-app/public/weaver_photo_sm.jpg

This is a binary file and will not be displayed.

+3
crates/weaver-app/src/blobcache.rs
··· 42 } 43 AtIdentifier::Handle(handle) => self.client.pds_for_handle(&handle).await?, 44 }; 45 let blob = if let Ok(blob_stream) = self 46 .client 47 .xrpc(pds_url)
··· 42 } 43 AtIdentifier::Handle(handle) => self.client.pds_for_handle(&handle).await?, 44 }; 45 + if self.get_cid(&cid).is_some() { 46 + return Ok(()); 47 + } 48 let blob = if let Ok(blob_stream) = self 49 .client 50 .xrpc(pds_url)
+4 -2
crates/weaver-app/src/components/entry.rs
··· 69 match &*entry.read() { 70 Some((book_entry_view, entry_record)) => { 71 if let Some(embeds) = &entry_record.embeds { 72 - if let Some(images) = &embeds.images { 73 // Register blob mappings with service worker (client-side only) 74 #[cfg(all( 75 target_family = "wasm", ··· 83 let _ = crate::service_worker::register_entry_blobs( 84 &ident(), 85 book_title().as_str(), 86 - &images, 87 &fetcher, 88 ) 89 .await; ··· 117 .as_ref() 118 .map(|t| t.as_ref()) 119 .unwrap_or("Untitled"); 120 121 rsx! { 122 // Set page title
··· 69 match &*entry.read() { 70 Some((book_entry_view, entry_record)) => { 71 if let Some(embeds) = &entry_record.embeds { 72 + if let Some(_images) = &embeds.images { 73 // Register blob mappings with service worker (client-side only) 74 #[cfg(all( 75 target_family = "wasm", ··· 83 let _ = crate::service_worker::register_entry_blobs( 84 &ident(), 85 book_title().as_str(), 86 + &_images, 87 &fetcher, 88 ) 89 .await; ··· 117 .as_ref() 118 .map(|t| t.as_ref()) 119 .unwrap_or("Untitled"); 120 + 121 + tracing::info!("Entry: {book_title} - {title}"); 122 123 rsx! { 124 // Set page title
+4 -8
crates/weaver-app/src/data.rs
··· 470 Memo<Option<ProfileDataView<'static>>>, 471 ) { 472 let fetcher = use_context::<crate::fetch::Fetcher>(); 473 - let res = use_server_future(use_reactive!(|ident| { 474 let fetcher = fetcher.clone(); 475 async move { 476 - tracing::debug!("use_profile_data server future STARTED for {:?}", ident); 477 - let result = fetcher 478 .fetch_profile(&ident()) 479 .await 480 .ok() 481 .map(|arc| serde_json::to_value(&*arc).ok()) 482 - .flatten(); 483 - tracing::debug!("use_profile_data server future COMPLETED for {:?}", ident); 484 - result 485 } 486 })); 487 let memo = use_memo(use_reactive!(|res| { 488 - let res = res.as_ref().ok()?; 489 if let Some(Some(value)) = &*res.read() { 490 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 491 } else { 492 None 493 } 494 })); 495 - (res, memo) 496 } 497 498 /// Fetches profile data client-side only (no SSR)
··· 470 Memo<Option<ProfileDataView<'static>>>, 471 ) { 472 let fetcher = use_context::<crate::fetch::Fetcher>(); 473 + let res = use_resource(use_reactive!(|ident| { 474 let fetcher = fetcher.clone(); 475 async move { 476 + fetcher 477 .fetch_profile(&ident()) 478 .await 479 .ok() 480 .map(|arc| serde_json::to_value(&*arc).ok()) 481 + .flatten() 482 } 483 })); 484 let memo = use_memo(use_reactive!(|res| { 485 if let Some(Some(value)) = &*res.read() { 486 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 487 } else { 488 None 489 } 490 })); 491 + (Ok(res), memo) 492 } 493 494 /// Fetches profile data client-side only (no SSR)
+4 -4
crates/weaver-app/src/env.rs
··· 1 // This file is automatically generated by build.rs 2 3 #[allow(unused)] 4 - pub const WEAVER_APP_ENV: &'static str = "dev"; 5 #[allow(unused)] 6 - pub const WEAVER_APP_HOST: &'static str = "http://localhost"; 7 #[allow(unused)] 8 - pub const WEAVER_APP_DOMAIN: &'static str = ""; 9 #[allow(unused)] 10 pub const WEAVER_PORT: &'static str = "8080"; 11 #[allow(unused)] ··· 13 #[allow(unused)] 14 pub const WEAVER_CLIENT_NAME: &'static str = "Weaver"; 15 #[allow(unused)] 16 - pub const WEAVER_LOGO_URI: &'static str = ""; 17 #[allow(unused)] 18 pub const WEAVER_TOS_URI: &'static str = ""; 19 #[allow(unused)]
··· 1 // This file is automatically generated by build.rs 2 3 #[allow(unused)] 4 + pub const WEAVER_APP_ENV: &'static str = "prod"; 5 #[allow(unused)] 6 + pub const WEAVER_APP_HOST: &'static str = "https://alpha.weaver.sh"; 7 #[allow(unused)] 8 + pub const WEAVER_APP_DOMAIN: &'static str = "https://alpha.weaver.sh"; 9 #[allow(unused)] 10 pub const WEAVER_PORT: &'static str = "8080"; 11 #[allow(unused)] ··· 13 #[allow(unused)] 14 pub const WEAVER_CLIENT_NAME: &'static str = "Weaver"; 15 #[allow(unused)] 16 + pub const WEAVER_LOGO_URI: &'static str = "https://alpha.weaver.sh/favicon.ico"; 17 #[allow(unused)] 18 pub const WEAVER_TOS_URI: &'static str = ""; 19 #[allow(unused)]
+264 -269
crates/weaver-app/src/fetch.rs
··· 5 use jacquard::CowStr; 6 use jacquard::IntoStatic; 7 use jacquard::client::Agent; 8 use jacquard::client::AgentKind; 9 use jacquard::error::ClientError; 10 use jacquard::error::XrpcResult; ··· 28 use std::future::Future; 29 use std::{sync::Arc, time::Duration}; 30 use tokio::sync::RwLock; 31 use weaver_api::{ 32 com_atproto::repo::strong_ref::StrongRef, 33 sh_weaver::{ ··· 35 notebook::{BookEntryView, NotebookView, entry::Entry}, 36 }, 37 }; 38 use weaver_common::WeaverExt; 39 40 #[derive(Debug, Clone, Deserialize, Serialize)] ··· 326 } 327 } 328 329 - #[cfg(not(feature = "server"))] 330 #[derive(Clone)] 331 pub struct Fetcher { 332 pub client: Arc<Client>, 333 } 334 335 - #[cfg(not(feature = "server"))] 336 impl Fetcher { 337 pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 338 Self { ··· 508 // View the notebook (which hydrates authors) 509 match client.view_notebook(&record.uri).await { 510 Ok((notebook, entries)) => { 511 - let ident = record.uri.authority().clone().into_static(); 512 - let title = notebook 513 - .title 514 - .as_ref() 515 - .map(|t| SmolStr::new(t.as_ref())) 516 - .unwrap_or_else(|| SmolStr::new("Untitled")); 517 - 518 let result = Arc::new((notebook, entries)); 519 notebooks.push(result); 520 } ··· 522 } 523 } 524 } 525 - 526 Ok(notebooks) 527 } 528 ··· 568 .await 569 .map_err(|e| dioxus::CapturedError::from_display(e))?; 570 571 - let result = Arc::new(profile_view); 572 - 573 - Ok(result) 574 } 575 } 576 577 - #[cfg(feature = "server")] 578 - #[derive(Clone)] 579 - pub struct Fetcher { 580 - pub client: Arc<Client>, 581 - book_cache: cache_impl::Cache< 582 - (AtIdentifier<'static>, SmolStr), 583 - Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>, 584 - >, 585 - entry_cache: cache_impl::Cache< 586 - (AtIdentifier<'static>, SmolStr), 587 - Arc<(BookEntryView<'static>, Entry<'static>)>, 588 - >, 589 - profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 590 - } 591 592 - // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 593 - //#[cfg(feature = "server")] 594 - unsafe impl Sync for Fetcher {} 595 596 - // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 597 - //#[cfg(feature = "server")] 598 - unsafe impl Send for Fetcher {} 599 600 - #[cfg(feature = "server")] 601 - impl Fetcher { 602 - pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 603 - Self { 604 - client: Arc::new(Client::new(client)), 605 - book_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 606 - entry_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 607 - profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), 608 - } 609 - } 610 611 - pub async fn upgrade_to_authenticated( 612 - &self, 613 - session: OAuthSession<JacquardResolver, crate::auth::AuthStore>, 614 - ) { 615 - let mut session_slot = self.client.session.write().await; 616 - *session_slot = Some(Arc::new(Agent::new(session))); 617 - } 618 619 - pub async fn downgrade_to_unauthenticated(&self) { 620 - let mut session_slot = self.client.session.write().await; 621 - if let Some(session) = session_slot.take() { 622 - session.inner().logout().await.ok(); 623 - } 624 - } 625 626 - #[allow(dead_code)] 627 - pub async fn current_did(&self) -> Option<Did<'static>> { 628 - let session_slot = self.client.session.read().await; 629 - if let Some(session) = session_slot.as_ref() { 630 - session.info().await.map(|(d, _)| d) 631 - } else { 632 - None 633 - } 634 - } 635 636 - pub fn get_client(&self) -> Arc<Client> { 637 - self.client.clone() 638 - } 639 640 - pub async fn get_notebook( 641 - &self, 642 - ident: AtIdentifier<'static>, 643 - title: SmolStr, 644 - ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 645 - if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 646 - Ok(Some(entry)) 647 - } else { 648 - let client = self.get_client(); 649 - if let Some((notebook, entries)) = client 650 - .notebook_by_title(&ident, &title) 651 - .await 652 - .map_err(|e| dioxus::CapturedError::from_display(e))? 653 - { 654 - let stored = Arc::new((notebook, entries)); 655 - cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); 656 - Ok(Some(stored)) 657 - } else { 658 - Ok(None) 659 - } 660 - } 661 - } 662 663 - pub async fn get_entry( 664 - &self, 665 - ident: AtIdentifier<'static>, 666 - book_title: SmolStr, 667 - entry_title: SmolStr, 668 - ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 669 - if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 670 - let (notebook, entries) = result.as_ref(); 671 - if let Some(entry) = 672 - cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 673 - { 674 - Ok(Some(entry)) 675 - } else { 676 - let client = self.get_client(); 677 - if let Some(entry) = client 678 - .entry_by_title(notebook, entries.as_ref(), &entry_title) 679 - .await 680 - .map_err(|e| dioxus::CapturedError::from_display(e))? 681 - { 682 - let stored = Arc::new(entry); 683 - cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 684 - Ok(Some(stored)) 685 - } else { 686 - Ok(None) 687 - } 688 - } 689 - } else { 690 - Ok(None) 691 - } 692 - } 693 694 - pub async fn fetch_notebooks_from_ufos( 695 - &self, 696 - ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 697 - use jacquard::{IntoStatic, types::aturi::AtUri}; 698 699 - let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book"; 700 - let response = reqwest::get(url) 701 - .await 702 - .map_err(|e| dioxus::CapturedError::from_display(e))?; 703 704 - let records: Vec<UfosRecord> = response 705 - .json() 706 - .await 707 - .map_err(|e| dioxus::CapturedError::from_display(e))?; 708 709 - let mut notebooks = Vec::new(); 710 - let client = self.get_client(); 711 712 - for ufos_record in records { 713 - // Construct URI 714 - let uri_str = format!( 715 - "at://{}/{}/{}", 716 - ufos_record.did, ufos_record.collection, ufos_record.rkey 717 - ); 718 - let uri = AtUri::new_owned(uri_str) 719 - .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; 720 721 - // Fetch the full notebook view (which hydrates authors) 722 - match client.view_notebook(&uri).await { 723 - Ok((notebook, entries)) => { 724 - let ident = uri.authority().clone().into_static(); 725 - let title = notebook 726 - .title 727 - .as_ref() 728 - .map(|t| SmolStr::new(t.as_ref())) 729 - .unwrap_or_else(|| SmolStr::new("Untitled")); 730 731 - let result = Arc::new((notebook, entries)); 732 - // Cache it 733 - cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 734 - notebooks.push(result); 735 - } 736 - Err(_) => continue, // Skip notebooks that fail to load 737 - } 738 - } 739 740 - Ok(notebooks) 741 - } 742 743 - pub async fn fetch_notebooks_for_did( 744 - &self, 745 - ident: &AtIdentifier<'_>, 746 - ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 747 - use jacquard::{ 748 - IntoStatic, 749 - types::{collection::Collection, nsid::Nsid}, 750 - xrpc::XrpcExt, 751 - }; 752 - use weaver_api::{ 753 - com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book, 754 - }; 755 756 - let client = self.get_client(); 757 758 - // Resolve DID and PDS 759 - let (repo_did, pds_url) = match ident { 760 - AtIdentifier::Did(did) => { 761 - let pds = client 762 - .pds_for_did(did) 763 - .await 764 - .map_err(|e| dioxus::CapturedError::from_display(e))?; 765 - (did.clone(), pds) 766 - } 767 - AtIdentifier::Handle(handle) => client 768 - .pds_for_handle(handle) 769 - .await 770 - .map_err(|e| dioxus::CapturedError::from_display(e))?, 771 - }; 772 773 - // Fetch all notebook records for this repo 774 - let resp = client 775 - .xrpc(pds_url) 776 - .send( 777 - &ListRecords::new() 778 - .repo(repo_did) 779 - .collection(Nsid::raw(Book::NSID)) 780 - .limit(100) 781 - .build(), 782 - ) 783 - .await 784 - .map_err(|e| dioxus::CapturedError::from_display(e))?; 785 786 - let mut notebooks = Vec::new(); 787 788 - if let Ok(list) = resp.parse() { 789 - for record in list.records { 790 - // View the notebook (which hydrates authors) 791 - match client.view_notebook(&record.uri).await { 792 - Ok((notebook, entries)) => { 793 - let ident = record.uri.authority().clone().into_static(); 794 - let title = notebook 795 - .title 796 - .as_ref() 797 - .map(|t| SmolStr::new(t.as_ref())) 798 - .unwrap_or_else(|| SmolStr::new("Untitled")); 799 800 - let result = Arc::new((notebook, entries)); 801 - // Cache it 802 - cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 803 - notebooks.push(result); 804 - } 805 - Err(_) => continue, // Skip notebooks that fail to load 806 - } 807 - } 808 - } 809 810 - Ok(notebooks) 811 - } 812 813 - pub async fn list_notebook_entries( 814 - &self, 815 - ident: AtIdentifier<'static>, 816 - book_title: SmolStr, 817 - ) -> Result<Option<Vec<BookEntryView<'static>>>> { 818 - if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 819 - let (notebook, entries) = result.as_ref(); 820 - let mut book_entries = Vec::new(); 821 - let client = self.get_client(); 822 823 - for index in 0..entries.len() { 824 - match client.view_entry(notebook, entries, index).await { 825 - Ok(book_entry) => book_entries.push(book_entry), 826 - Err(_) => continue, // Skip entries that fail to load 827 - } 828 - } 829 830 - Ok(Some(book_entries)) 831 - } else { 832 - Ok(None) 833 - } 834 - } 835 836 - pub async fn fetch_profile( 837 - &self, 838 - ident: &AtIdentifier<'_>, 839 - ) -> Result<Arc<ProfileDataView<'static>>> { 840 - use jacquard::IntoStatic; 841 842 - let ident_static = ident.clone().into_static(); 843 844 - if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 845 - return Ok(cached); 846 - } 847 848 - let client = self.get_client(); 849 850 - let did = match ident { 851 - AtIdentifier::Did(d) => d.clone(), 852 - AtIdentifier::Handle(h) => client 853 - .resolve_handle(h) 854 - .await 855 - .map_err(|e| dioxus::CapturedError::from_display(e))?, 856 - }; 857 858 - let (_uri, profile_view) = client 859 - .hydrate_profile_view(&did) 860 - .await 861 - .map_err(|e| dioxus::CapturedError::from_display(e))?; 862 863 - let result = Arc::new(profile_view); 864 - cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 865 866 - Ok(result) 867 - } 868 - } 869 870 impl HttpClient for Fetcher { 871 #[doc = " Error type returned by the HTTP client"]
··· 5 use jacquard::CowStr; 6 use jacquard::IntoStatic; 7 use jacquard::client::Agent; 8 + use jacquard::client::AgentError; 9 use jacquard::client::AgentKind; 10 use jacquard::error::ClientError; 11 use jacquard::error::XrpcResult; ··· 29 use std::future::Future; 30 use std::{sync::Arc, time::Duration}; 31 use tokio::sync::RwLock; 32 + use weaver_api::app_bsky::actor::get_profile::GetProfile; 33 + use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; 34 + use weaver_api::sh_weaver::actor::ProfileDataViewInner; 35 use weaver_api::{ 36 com_atproto::repo::strong_ref::StrongRef, 37 sh_weaver::{ ··· 39 notebook::{BookEntryView, NotebookView, entry::Entry}, 40 }, 41 }; 42 + use weaver_common::WeaverError; 43 use weaver_common::WeaverExt; 44 45 #[derive(Debug, Clone, Deserialize, Serialize)] ··· 331 } 332 } 333 334 + //#[cfg(not(feature = "server"))] 335 #[derive(Clone)] 336 pub struct Fetcher { 337 pub client: Arc<Client>, 338 } 339 340 + //#[cfg(not(feature = "server"))] 341 impl Fetcher { 342 pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 343 Self { ··· 513 // View the notebook (which hydrates authors) 514 match client.view_notebook(&record.uri).await { 515 Ok((notebook, entries)) => { 516 let result = Arc::new((notebook, entries)); 517 notebooks.push(result); 518 } ··· 520 } 521 } 522 } 523 Ok(notebooks) 524 } 525 ··· 565 .await 566 .map_err(|e| dioxus::CapturedError::from_display(e))?; 567 568 + Ok(Arc::new(profile_view)) 569 } 570 } 571 572 + // #[cfg(feature = "server")] 573 + // #[derive(Clone)] 574 + // pub struct Fetcher { 575 + // pub client: Arc<Client>, 576 + // book_cache: cache_impl::Cache< 577 + // (AtIdentifier<'static>, SmolStr), 578 + // Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>, 579 + // >, 580 + // entry_cache: cache_impl::Cache< 581 + // (AtIdentifier<'static>, SmolStr), 582 + // Arc<(BookEntryView<'static>, Entry<'static>)>, 583 + // >, 584 + // profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 585 + // } 586 587 + // // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 588 + // //#[cfg(feature = "server")] 589 + // unsafe impl Sync for Fetcher {} 590 591 + // // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 592 + // //#[cfg(feature = "server")] 593 + // unsafe impl Send for Fetcher {} 594 595 + // #[cfg(feature = "server")] 596 + // impl Fetcher { 597 + // pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 598 + // Self { 599 + // client: Arc::new(Client::new(client)), 600 + // book_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 601 + // entry_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 602 + // profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), 603 + // } 604 + // } 605 606 + // pub async fn upgrade_to_authenticated( 607 + // &self, 608 + // session: OAuthSession<JacquardResolver, crate::auth::AuthStore>, 609 + // ) { 610 + // let mut session_slot = self.client.session.write().await; 611 + // *session_slot = Some(Arc::new(Agent::new(session))); 612 + // } 613 614 + // pub async fn downgrade_to_unauthenticated(&self) { 615 + // let mut session_slot = self.client.session.write().await; 616 + // if let Some(session) = session_slot.take() { 617 + // session.inner().logout().await.ok(); 618 + // } 619 + // } 620 621 + // #[allow(dead_code)] 622 + // pub async fn current_did(&self) -> Option<Did<'static>> { 623 + // let session_slot = self.client.session.read().await; 624 + // if let Some(session) = session_slot.as_ref() { 625 + // session.info().await.map(|(d, _)| d) 626 + // } else { 627 + // None 628 + // } 629 + // } 630 631 + // pub fn get_client(&self) -> Arc<Client> { 632 + // self.client.clone() 633 + // } 634 635 + // pub async fn get_notebook( 636 + // &self, 637 + // ident: AtIdentifier<'static>, 638 + // title: SmolStr, 639 + // ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 640 + // if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 641 + // Ok(Some(entry)) 642 + // } else { 643 + // let client = self.get_client(); 644 + // if let Some((notebook, entries)) = client 645 + // .notebook_by_title(&ident, &title) 646 + // .await 647 + // .map_err(|e| dioxus::CapturedError::from_display(e))? 648 + // { 649 + // let stored = Arc::new((notebook, entries)); 650 + // cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); 651 + // Ok(Some(stored)) 652 + // } else { 653 + // Ok(None) 654 + // } 655 + // } 656 + // } 657 658 + // pub async fn get_entry( 659 + // &self, 660 + // ident: AtIdentifier<'static>, 661 + // book_title: SmolStr, 662 + // entry_title: SmolStr, 663 + // ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 664 + // if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 665 + // let (notebook, entries) = result.as_ref(); 666 + // if let Some(entry) = 667 + // cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 668 + // { 669 + // Ok(Some(entry)) 670 + // } else { 671 + // let client = self.get_client(); 672 + // if let Some(entry) = client 673 + // .entry_by_title(notebook, entries.as_ref(), &entry_title) 674 + // .await 675 + // .map_err(|e| dioxus::CapturedError::from_display(e))? 676 + // { 677 + // let stored = Arc::new(entry); 678 + // cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 679 + // Ok(Some(stored)) 680 + // } else { 681 + // Ok(None) 682 + // } 683 + // } 684 + // } else { 685 + // Ok(None) 686 + // } 687 + // } 688 689 + // pub async fn fetch_notebooks_from_ufos( 690 + // &self, 691 + // ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 692 + // use jacquard::{IntoStatic, types::aturi::AtUri}; 693 694 + // let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book"; 695 + // let response = reqwest::get(url) 696 + // .await 697 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 698 699 + // let records: Vec<UfosRecord> = response 700 + // .json() 701 + // .await 702 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 703 704 + // let mut notebooks = Vec::new(); 705 + // let client = self.get_client(); 706 707 + // for ufos_record in records { 708 + // // Construct URI 709 + // let uri_str = format!( 710 + // "at://{}/{}/{}", 711 + // ufos_record.did, ufos_record.collection, ufos_record.rkey 712 + // ); 713 + // let uri = AtUri::new_owned(uri_str) 714 + // .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; 715 716 + // // Fetch the full notebook view (which hydrates authors) 717 + // match client.view_notebook(&uri).await { 718 + // Ok((notebook, entries)) => { 719 + // let ident = uri.authority().clone().into_static(); 720 + // let title = notebook 721 + // .title 722 + // .as_ref() 723 + // .map(|t| SmolStr::new(t.as_ref())) 724 + // .unwrap_or_else(|| SmolStr::new("Untitled")); 725 726 + // let result = Arc::new((notebook, entries)); 727 + // // Cache it 728 + // cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 729 + // notebooks.push(result); 730 + // } 731 + // Err(_) => continue, // Skip notebooks that fail to load 732 + // } 733 + // } 734 735 + // Ok(notebooks) 736 + // } 737 738 + // pub async fn fetch_notebooks_for_did( 739 + // &self, 740 + // ident: &AtIdentifier<'_>, 741 + // ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 742 + // use jacquard::{ 743 + // IntoStatic, 744 + // types::{collection::Collection, nsid::Nsid}, 745 + // xrpc::XrpcExt, 746 + // }; 747 + // use weaver_api::{ 748 + // com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book, 749 + // }; 750 751 + // let client = self.get_client(); 752 753 + // // Resolve DID and PDS 754 + // let (repo_did, pds_url) = match ident { 755 + // AtIdentifier::Did(did) => { 756 + // let pds = client 757 + // .pds_for_did(did) 758 + // .await 759 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 760 + // (did.clone(), pds) 761 + // } 762 + // AtIdentifier::Handle(handle) => client 763 + // .pds_for_handle(handle) 764 + // .await 765 + // .map_err(|e| dioxus::CapturedError::from_display(e))?, 766 + // }; 767 768 + // // Fetch all notebook records for this repo 769 + // let resp = client 770 + // .xrpc(pds_url) 771 + // .send( 772 + // &ListRecords::new() 773 + // .repo(repo_did) 774 + // .collection(Nsid::raw(Book::NSID)) 775 + // .limit(100) 776 + // .build(), 777 + // ) 778 + // .await 779 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 780 781 + // let mut notebooks = Vec::new(); 782 783 + // if let Ok(list) = resp.parse() { 784 + // for record in list.records { 785 + // // View the notebook (which hydrates authors) 786 + // match client.view_notebook(&record.uri).await { 787 + // Ok((notebook, entries)) => { 788 + // let ident = record.uri.authority().clone().into_static(); 789 + // let title = notebook 790 + // .title 791 + // .as_ref() 792 + // .map(|t| SmolStr::new(t.as_ref())) 793 + // .unwrap_or_else(|| SmolStr::new("Untitled")); 794 795 + // let result = Arc::new((notebook, entries)); 796 + // // Cache it 797 + // cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 798 + // notebooks.push(result); 799 + // } 800 + // Err(_) => continue, // Skip notebooks that fail to load 801 + // } 802 + // } 803 + // } 804 805 + // Ok(notebooks) 806 + // } 807 808 + // pub async fn list_notebook_entries( 809 + // &self, 810 + // ident: AtIdentifier<'static>, 811 + // book_title: SmolStr, 812 + // ) -> Result<Option<Vec<BookEntryView<'static>>>> { 813 + // if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 814 + // let (notebook, entries) = result.as_ref(); 815 + // let mut book_entries = Vec::new(); 816 + // let client = self.get_client(); 817 818 + // for index in 0..entries.len() { 819 + // match client.view_entry(notebook, entries, index).await { 820 + // Ok(book_entry) => book_entries.push(book_entry), 821 + // Err(_) => continue, // Skip entries that fail to load 822 + // } 823 + // } 824 825 + // Ok(Some(book_entries)) 826 + // } else { 827 + // Ok(None) 828 + // } 829 + // } 830 831 + // pub async fn fetch_profile( 832 + // &self, 833 + // ident: &AtIdentifier<'_>, 834 + // ) -> Result<Arc<ProfileDataView<'static>>> { 835 + // use jacquard::IntoStatic; 836 837 + // let ident_static = ident.clone().into_static(); 838 839 + // if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 840 + // return Ok(cached); 841 + // } 842 843 + // let client = self.get_client(); 844 845 + // let did = match ident { 846 + // AtIdentifier::Did(d) => d.clone(), 847 + // AtIdentifier::Handle(h) => client 848 + // .resolve_handle(h) 849 + // .await 850 + // .map_err(|e| dioxus::CapturedError::from_display(e))?, 851 + // }; 852 853 + // let (_uri, profile_view) = client 854 + // .hydrate_profile_view(&did) 855 + // .await 856 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 857 858 + // let result = Arc::new(profile_view); 859 + // cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 860 861 + // Ok(result) 862 + // } 863 + // } 864 865 impl HttpClient for Fetcher { 866 #[doc = " Error type returned by the HTTP client"]
+13
crates/weaver-app/src/main.rs
··· 98 #[cfg(target_arch = "wasm32")] 99 console_error_panic_hook::set_once(); 100 101 // Run `serve()` on the server only 102 #[cfg(feature = "server")] 103 dioxus::serve(|| async move { ··· 125 UnauthenticatedSession::new_public(), 126 ))); 127 axum::Router::new() 128 // Server side render the application, serve static assets, and register server functions 129 .serve_dioxus_application( 130 ServeConfig::builder(), // Enable incremental rendering ··· 257 Outlet::<Route> {} 258 } 259 } 260 } 261 262 #[cfg(all(feature = "fullstack-server", feature = "server"))]
··· 98 #[cfg(target_arch = "wasm32")] 99 console_error_panic_hook::set_once(); 100 101 + #[cfg(feature = "server")] 102 + std::panic::set_hook(Box::new(|panic_info| { 103 + tracing::error!("PANIC: {:?}", panic_info); 104 + })); 105 + 106 // Run `serve()` on the server only 107 #[cfg(feature = "server")] 108 dioxus::serve(|| async move { ··· 130 UnauthenticatedSession::new_public(), 131 ))); 132 axum::Router::new() 133 + .route("/favicon.ico", get(favicon)) 134 // Server side render the application, serve static assets, and register server functions 135 .serve_dioxus_application( 136 ServeConfig::builder(), // Enable incremental rendering ··· 263 Outlet::<Route> {} 264 } 265 } 266 + } 267 + 268 + #[cfg(all(feature = "fullstack-server", feature = "server"))] 269 + pub async fn favicon() -> axum::response::Response { 270 + use axum::{http::header::CONTENT_TYPE, response::IntoResponse}; 271 + let bytes = include_bytes!("../assets/weaver_photo_sm.jpg"); 272 + ([(CONTENT_TYPE, "image/jpg")], bytes).into_response() 273 } 274 275 #[cfg(all(feature = "fullstack-server", feature = "server"))]
+27 -28
crates/weaver-common/src/agent.rs
··· 1 // Re-export view types for use elsewhere 2 pub use weaver_api::sh_weaver::notebook::{ 3 AuthorListView, BookEntryRef, BookEntryView, EntryView, NotebookView, ··· 35 /// - `agent.upload_blob()` - Upload a single blob 36 /// 37 /// This trait is for multi-step workflows that coordinate between multiple operations. 38 - //#[trait_variant::make(Send)] 39 - pub trait WeaverExt: AgentSessionExt + XrpcExt { 40 - /// Publish a notebook directory to the user's PDS 41 - /// 42 - /// Multi-step workflow: 43 - /// 1. Parse markdown files in directory 44 - /// 2. Extract and upload images/assets → BlobRefs 45 - /// 3. Transform markdown refs to point at uploaded blobs 46 - /// 4. Create entry records for each file 47 - /// 5. Create book record with entry refs 48 - /// 49 - /// Returns the AT-URI of the published book 50 - fn publish_notebook( 51 - &self, 52 - path: &Path, 53 - ) -> impl Future<Output = Result<PublishResult<'_>, WeaverError>> { 54 - async { todo!() } 55 - } 56 - 57 /// Publish a blob to the user's PDS 58 /// 59 /// Multi-step workflow: ··· 62 /// 63 /// Returns the AT-URI of the published blob 64 fn publish_blob<'a>( 65 - &self, 66 blob: Bytes, 67 url_path: &'a str, 68 prev: Option<Tid>, 69 - ) -> impl Future<Output = Result<(StrongRef<'a>, PublishedBlob<'a>), WeaverError>> { 70 async move { 71 let mime_type = 72 MimeType::new_owned(blob.sniff_mime_type().unwrap_or("application/octet-stream")); 73 74 - let blob = self.upload_blob(blob, mime_type).await?; 75 let publish_record = PublishedBlob::new() 76 .path(url_path) 77 .upload(BlobRef::Blob(blob)) ··· 86 } 87 } 88 89 - fn confirm_record_ref( 90 - &self, 91 - uri: &AtUri<'_>, 92 - ) -> impl Future<Output = Result<StrongRef<'_>, WeaverError>> { 93 async move { 94 let rkey = uri.rkey().ok_or_else(|| { 95 AgentError::from( ··· 314 let tags = notebook.value.tags.clone(); 315 316 let mut authors = Vec::new(); 317 318 for (index, author) in notebook.value.authors.iter().enumerate() { 319 let (profile_uri, profile_view) = self.hydrate_profile_view(&author.did).await?; ··· 540 let tags = notebook.tags.clone(); 541 542 let mut authors = Vec::new(); 543 544 for (index, author) in notebook.authors.iter().enumerate() { 545 let (profile_uri, profile_view) = ··· 684 .send(GetProfile::new().actor(did.clone()).build()) 685 .await 686 { 687 - if let Ok(output) = bsky_resp.parse() { 688 let bsky_uri = 689 BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", did)) 690 .map_err(|_| { ··· 696 Some(bsky_uri.as_uri().clone().into_static()), 697 ProfileDataView::new() 698 .inner(ProfileDataViewInner::ProfileViewDetailed(Box::new( 699 - output.value.into_static(), 700 ))) 701 .build() 702 .into_static(),
··· 1 + use weaver_api::app_bsky::actor::get_profile::GetProfile; 2 // Re-export view types for use elsewhere 3 pub use weaver_api::sh_weaver::notebook::{ 4 AuthorListView, BookEntryRef, BookEntryView, EntryView, NotebookView, ··· 36 /// - `agent.upload_blob()` - Upload a single blob 37 /// 38 /// This trait is for multi-step workflows that coordinate between multiple operations. 39 + //#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 40 + pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync { 41 /// Publish a blob to the user's PDS 42 /// 43 /// Multi-step workflow: ··· 46 /// 47 /// Returns the AT-URI of the published blob 48 fn publish_blob<'a>( 49 + &'a self, 50 blob: Bytes, 51 url_path: &'a str, 52 prev: Option<Tid>, 53 + ) -> impl Future<Output = Result<(StrongRef<'a>, PublishedBlob<'a>), WeaverError>> + 'a { 54 async move { 55 let mime_type = 56 MimeType::new_owned(blob.sniff_mime_type().unwrap_or("application/octet-stream")); 57 58 + let blob = self.upload_blob(blob, mime_type.into_static()).await?; 59 let publish_record = PublishedBlob::new() 60 .path(url_path) 61 .upload(BlobRef::Blob(blob)) ··· 70 } 71 } 72 73 + fn confirm_record_ref<'a>( 74 + &'a self, 75 + uri: &'a AtUri<'a>, 76 + ) -> impl Future<Output = Result<StrongRef<'static>, WeaverError>> + 'a { 77 async move { 78 let rkey = uri.rkey().ok_or_else(|| { 79 AgentError::from( ··· 298 let tags = notebook.value.tags.clone(); 299 300 let mut authors = Vec::new(); 301 + use weaver_api::app_bsky::actor::{ 302 + ProfileViewDetailed, get_profile::GetProfile, profile::Profile as BskyProfile, 303 + }; 304 + use weaver_api::sh_weaver::actor::{ 305 + ProfileDataView, ProfileDataViewInner, ProfileView, 306 + profile::Profile as WeaverProfile, 307 + }; 308 309 for (index, author) in notebook.value.authors.iter().enumerate() { 310 let (profile_uri, profile_view) = self.hydrate_profile_view(&author.did).await?; ··· 531 let tags = notebook.tags.clone(); 532 533 let mut authors = Vec::new(); 534 + use weaver_api::app_bsky::actor::{ 535 + ProfileViewDetailed, get_profile::GetProfile, 536 + profile::Profile as BskyProfile, 537 + }; 538 + use weaver_api::sh_weaver::actor::{ 539 + ProfileDataView, ProfileDataViewInner, ProfileView, 540 + profile::Profile as WeaverProfile, 541 + }; 542 543 for (index, author) in notebook.authors.iter().enumerate() { 544 let (profile_uri, profile_view) = ··· 683 .send(GetProfile::new().actor(did.clone()).build()) 684 .await 685 { 686 + if let Ok(output) = bsky_resp.into_output() { 687 let bsky_uri = 688 BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", did)) 689 .map_err(|_| { ··· 695 Some(bsky_uri.as_uri().clone().into_static()), 696 ProfileDataView::new() 697 .inner(ProfileDataViewInner::ProfileViewDetailed(Box::new( 698 + output.value, 699 ))) 700 .build() 701 .into_static(),