tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
server side cache back in
Orual
1 month ago
e2b0d156
21e174ee
+329
-373
3 changed files
expand all
collapse all
unified
split
crates
weaver-app
src
data.rs
fetch.rs
main.rs
+44
crates/weaver-app/src/data.rs
···
655
655
async move {
656
656
match fetcher.fetch_entries_from_ufos().await {
657
657
Ok(entries) => {
658
658
+
// Cache blobs for each entry's embedded images
659
659
+
for arc in &entries {
660
660
+
let (view, entry, _) = arc.as_ref();
661
661
+
if let Some(embeds) = &entry.embeds {
662
662
+
if let Some(images) = &embeds.images {
663
663
+
use jacquard::smol_str::ToSmolStr;
664
664
+
use jacquard::types::aturi::AtUri;
665
665
+
// Extract ident from the entry's at-uri
666
666
+
if let Ok(at_uri) = AtUri::new(view.uri.as_ref()) {
667
667
+
let ident = at_uri.authority().to_smolstr();
668
668
+
for image in &images.images {
669
669
+
let cid = image.image.blob().cid();
670
670
+
cache_blob(
671
671
+
ident.clone(),
672
672
+
cid.to_smolstr(),
673
673
+
image.name.as_ref().map(|n| n.to_smolstr()),
674
674
+
)
675
675
+
.await
676
676
+
.ok();
677
677
+
}
678
678
+
}
679
679
+
}
680
680
+
}
681
681
+
}
658
682
Some(
659
683
entries
660
684
.iter()
···
945
969
async move {
946
970
match fetcher.get_entry_by_rkey(ident(), rkey()).await {
947
971
Ok(Some(data)) => {
972
972
+
// Cache blobs for embedded images
973
973
+
if let Some(embeds) = &data.entry.embeds {
974
974
+
if let Some(images) = &embeds.images {
975
975
+
use jacquard::smol_str::ToSmolStr;
976
976
+
use jacquard::types::aturi::AtUri;
977
977
+
if let Ok(at_uri) = AtUri::new(data.entry_view.uri.as_ref()) {
978
978
+
let ident_str = at_uri.authority().to_smolstr();
979
979
+
for image in &images.images {
980
980
+
let cid = image.image.blob().cid();
981
981
+
cache_blob(
982
982
+
ident_str.clone(),
983
983
+
cid.to_smolstr(),
984
984
+
image.name.as_ref().map(|n| n.to_smolstr()),
985
985
+
)
986
986
+
.await
987
987
+
.ok();
988
988
+
}
989
989
+
}
990
990
+
}
991
991
+
}
948
992
let entry_json = serde_json::to_value(&data.entry).ok()?;
949
993
let entry_view_json = serde_json::to_value(&data.entry_view).ok()?;
950
994
let notebook_ctx_json = data.notebook_context.as_ref().map(|ctx| {
+142
-328
crates/weaver-app/src/fetch.rs
···
352
352
#[derive(Clone)]
353
353
pub struct Fetcher {
354
354
pub client: Arc<Client>,
355
355
+
#[cfg(feature = "server")]
356
356
+
book_cache: cache_impl::Cache<
357
357
+
(AtIdentifier<'static>, SmolStr),
358
358
+
Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>,
359
359
+
>,
360
360
+
#[cfg(feature = "server")]
361
361
+
entry_cache: cache_impl::Cache<
362
362
+
(AtIdentifier<'static>, SmolStr),
363
363
+
Arc<(BookEntryView<'static>, Entry<'static>)>,
364
364
+
>,
365
365
+
#[cfg(feature = "server")]
366
366
+
profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>,
367
367
+
#[cfg(feature = "server")]
368
368
+
standalone_entry_cache:
369
369
+
cache_impl::Cache<(AtIdentifier<'static>, SmolStr), Arc<StandaloneEntryData>>,
355
370
}
356
371
357
372
//#[cfg(not(feature = "server"))]
···
359
374
pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self {
360
375
Self {
361
376
client: Arc::new(Client::new(client)),
377
377
+
#[cfg(feature = "server")]
378
378
+
book_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)),
379
379
+
#[cfg(feature = "server")]
380
380
+
entry_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)),
381
381
+
#[cfg(feature = "server")]
382
382
+
profile_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(1800)),
383
383
+
#[cfg(feature = "server")]
384
384
+
standalone_entry_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)),
362
385
}
363
386
}
364
387
···
396
419
ident: AtIdentifier<'static>,
397
420
title: SmolStr,
398
421
) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> {
422
422
+
#[cfg(feature = "server")]
423
423
+
if let Some(cached) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) {
424
424
+
return Ok(Some(cached));
425
425
+
}
426
426
+
399
427
let client = self.get_client();
400
428
if let Some((notebook, entries)) = client
401
429
.notebook_by_title(&ident, &title)
···
403
431
.map_err(|e| dioxus::CapturedError::from_display(e))?
404
432
{
405
433
let stored = Arc::new((notebook, entries));
434
434
+
#[cfg(feature = "server")]
435
435
+
cache_impl::insert(&self.book_cache, (ident, title), stored.clone());
406
436
Ok(Some(stored))
407
437
} else {
408
438
Err(dioxus::CapturedError::from_display("Notebook not found"))
···
415
445
book_title: SmolStr,
416
446
entry_title: SmolStr,
417
447
) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> {
448
448
+
#[cfg(feature = "server")]
449
449
+
if let Some(cached) =
450
450
+
cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone()))
451
451
+
{
452
452
+
return Ok(Some(cached));
453
453
+
}
454
454
+
418
455
if let Some(result) = self.get_notebook(ident.clone(), book_title).await? {
419
456
let (notebook, entries) = result.as_ref();
420
457
let client = self.get_client();
···
424
461
.map_err(|e| dioxus::CapturedError::from_display(e))?
425
462
{
426
463
let stored = Arc::new(entry);
464
464
+
#[cfg(feature = "server")]
465
465
+
cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone());
427
466
Ok(Some(stored))
428
467
} else {
429
468
Err(dioxus::CapturedError::from_display("Entry not found"))
···
459
498
);
460
499
let uri = AtUri::new_owned(uri_str)
461
500
.map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?;
462
462
-
463
463
-
// Fetch the full notebook view (which hydrates authors)
464
501
match client.view_notebook(&uri).await {
465
502
Ok((notebook, entries)) => {
466
503
let ident = uri.authority().clone().into_static();
···
471
508
.unwrap_or_else(|| SmolStr::new("Untitled"));
472
509
473
510
let result = Arc::new((notebook, entries));
511
511
+
#[cfg(feature = "server")]
512
512
+
cache_impl::insert(&self.book_cache, (ident, title), result.clone());
474
513
notebooks.push(result);
475
514
}
476
515
Err(_) => continue, // Skip notebooks that fail to load
···
488
527
489
528
let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.entry";
490
529
491
491
-
let response = reqwest::get(url)
492
492
-
.await
493
493
-
.map_err(|e| {
494
494
-
tracing::error!("[fetch_entries_from_ufos] request failed: {:?}", e);
495
495
-
dioxus::CapturedError::from_display(e)
496
496
-
})?;
497
497
-
498
498
-
let mut records: Vec<UfosRecord> = response
499
499
-
.json()
500
500
-
.await
501
501
-
.map_err(|e| {
502
502
-
tracing::error!("[fetch_entries_from_ufos] json parse failed: {:?}", e);
503
503
-
dioxus::CapturedError::from_display(e)
504
504
-
})?;
530
530
+
let response = reqwest::get(url).await.map_err(|e| {
531
531
+
tracing::error!("[fetch_entries_from_ufos] request failed: {:?}", e);
532
532
+
dioxus::CapturedError::from_display(e)
533
533
+
})?;
505
534
506
506
-
// Sort by time_us descending (reverse chronological)
535
535
+
let mut records: Vec<UfosRecord> = response.json().await.map_err(|e| {
536
536
+
tracing::error!("[fetch_entries_from_ufos] json parse failed: {:?}", e);
537
537
+
dioxus::CapturedError::from_display(e)
538
538
+
})?;
507
539
records.sort_by(|a, b| b.time_us.cmp(&a.time_us));
508
540
509
541
let mut entries = Vec::new();
510
542
let client = self.get_client();
511
543
512
544
for ufos_record in records {
513
513
-
// Parse DID
514
545
let did = match Did::new(&ufos_record.did) {
515
546
Ok(d) => d.into_static(),
516
547
Err(e) => {
517
517
-
tracing::warn!("[fetch_entries_from_ufos] invalid DID {}: {:?}", ufos_record.did, e);
548
548
+
tracing::warn!(
549
549
+
"[fetch_entries_from_ufos] invalid DID {}: {:?}",
550
550
+
ufos_record.did,
551
551
+
e
552
552
+
);
518
553
continue;
519
554
}
520
555
};
521
556
let ident = AtIdentifier::Did(did);
522
522
-
523
523
-
// Fetch the entry view
524
524
-
match client
525
525
-
.fetch_entry_by_rkey(&ident, &ufos_record.rkey)
526
526
-
.await
527
527
-
{
557
557
+
match client.fetch_entry_by_rkey(&ident, &ufos_record.rkey).await {
528
558
Ok((entry_view, entry)) => {
529
559
entries.push(Arc::new((
530
560
entry_view.into_static(),
···
533
563
)));
534
564
}
535
565
Err(e) => {
536
536
-
tracing::warn!("[fetch_entries_from_ufos] failed to load entry {}: {:?}", ufos_record.rkey, e);
566
566
+
tracing::warn!(
567
567
+
"[fetch_entries_from_ufos] failed to load entry {}: {:?}",
568
568
+
ufos_record.rkey,
569
569
+
e
570
570
+
);
537
571
continue;
538
572
}
539
573
}
···
592
626
// View the notebook (which hydrates authors)
593
627
match client.view_notebook(&record.uri).await {
594
628
Ok((notebook, entries)) => {
629
629
+
let ident = record.uri.authority().clone().into_static();
630
630
+
let title = notebook
631
631
+
.title
632
632
+
.as_ref()
633
633
+
.map(|t| SmolStr::new(t.as_ref()))
634
634
+
.unwrap_or_else(|| SmolStr::new("Untitled"));
635
635
+
595
636
let result = Arc::new((notebook, entries));
637
637
+
#[cfg(feature = "server")]
638
638
+
cache_impl::insert(&self.book_cache, (ident, title), result.clone());
596
639
notebooks.push(result);
597
640
}
598
641
Err(_) => continue, // Skip notebooks that fail to load
···
607
650
ident: AtIdentifier<'static>,
608
651
book_title: SmolStr,
609
652
) -> Result<Option<Vec<BookEntryView<'static>>>> {
653
653
+
use jacquard::types::aturi::AtUri;
654
654
+
610
655
if let Some(result) = self.get_notebook(ident.clone(), book_title).await? {
611
611
-
let (notebook, entries) = result.as_ref();
656
656
+
let (notebook, entry_refs) = result.as_ref();
612
657
let mut book_entries = Vec::new();
613
658
let client = self.get_client();
614
659
615
615
-
for index in 0..entries.len() {
616
616
-
match client.view_entry(notebook, entries, index).await {
617
617
-
Ok(book_entry) => book_entries.push(book_entry),
618
618
-
Err(_) => continue, // Skip entries that fail to load
660
660
+
for (index, entry_ref) in entry_refs.iter().enumerate() {
661
661
+
// Try to extract rkey from URI
662
662
+
let rkey = AtUri::new(entry_ref.uri.as_ref())
663
663
+
.ok()
664
664
+
.and_then(|uri| uri.rkey().map(|r| SmolStr::new(r.as_ref())));
665
665
+
666
666
+
// Check cache first
667
667
+
#[cfg(feature = "server")]
668
668
+
if let Some(ref rkey) = rkey {
669
669
+
if let Some(cached) =
670
670
+
cache_impl::get(&self.entry_cache, &(ident.clone(), rkey.clone()))
671
671
+
{
672
672
+
book_entries.push(cached.0.clone());
673
673
+
continue;
674
674
+
}
675
675
+
}
676
676
+
677
677
+
// Fetch if not cached
678
678
+
if let Ok(book_entry) = client.view_entry(notebook, entry_refs, index).await {
679
679
+
// Try to populate cache by deserializing Entry from the view's record
680
680
+
#[cfg(feature = "server")]
681
681
+
if let Some(rkey) = rkey {
682
682
+
use jacquard::IntoStatic;
683
683
+
use weaver_api::sh_weaver::notebook::entry::Entry;
684
684
+
if let Ok(entry) =
685
685
+
jacquard::from_data::<Entry<'_>>(&book_entry.entry.record)
686
686
+
{
687
687
+
let cached =
688
688
+
Arc::new((book_entry.clone().into_static(), entry.into_static()));
689
689
+
cache_impl::insert(&self.entry_cache, (ident.clone(), rkey), cached);
690
690
+
}
691
691
+
}
692
692
+
book_entries.push(book_entry);
619
693
}
620
694
}
621
695
···
629
703
&self,
630
704
ident: &AtIdentifier<'_>,
631
705
) -> Result<Arc<ProfileDataView<'static>>> {
706
706
+
use jacquard::IntoStatic;
707
707
+
708
708
+
let ident_static = ident.clone().into_static();
709
709
+
710
710
+
#[cfg(feature = "server")]
711
711
+
if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) {
712
712
+
return Ok(cached);
713
713
+
}
714
714
+
632
715
let client = self.get_client();
633
716
634
717
let did = match ident {
···
644
727
.await
645
728
.map_err(|e| dioxus::CapturedError::from_display(e))?;
646
729
647
647
-
Ok(Arc::new(profile_view))
730
730
+
let result = Arc::new(profile_view);
731
731
+
#[cfg(feature = "server")]
732
732
+
cache_impl::insert(&self.profile_cache, ident_static, result.clone());
733
733
+
734
734
+
Ok(result)
648
735
}
649
736
650
737
/// Fetch an entry by rkey with optional notebook context lookup.
···
654
741
rkey: SmolStr,
655
742
) -> Result<Option<Arc<StandaloneEntryData>>> {
656
743
use jacquard::types::aturi::AtUri;
744
744
+
745
745
+
#[cfg(feature = "server")]
746
746
+
if let Some(cached) =
747
747
+
cache_impl::get(&self.standalone_entry_cache, &(ident.clone(), rkey.clone()))
748
748
+
{
749
749
+
return Ok(Some(cached));
750
750
+
}
657
751
658
752
let client = self.get_client();
659
753
···
711
805
None
712
806
};
713
807
714
714
-
Ok(Some(Arc::new(StandaloneEntryData {
808
808
+
let result = Arc::new(StandaloneEntryData {
715
809
entry,
716
810
entry_view,
717
811
notebook_context,
718
718
-
})))
812
812
+
});
813
813
+
#[cfg(feature = "server")]
814
814
+
cache_impl::insert(&self.standalone_entry_cache, (ident, rkey), result.clone());
815
815
+
816
816
+
Ok(Some(result))
719
817
}
720
818
721
819
/// Fetch an entry by rkey within a specific notebook context.
···
729
827
rkey: SmolStr,
730
828
) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> {
731
829
use jacquard::types::aturi::AtUri;
830
830
+
831
831
+
#[cfg(feature = "server")]
832
832
+
if let Some(cached) = cache_impl::get(&self.entry_cache, &(ident.clone(), rkey.clone())) {
833
833
+
return Ok(Some(cached));
834
834
+
}
732
835
733
836
let client = self.get_client();
734
837
···
783
886
.build();
784
887
}
785
888
786
786
-
Ok(Some(Arc::new((book_entry_view.into_static(), entry))))
889
889
+
let result = Arc::new((book_entry_view.into_static(), entry));
890
890
+
#[cfg(feature = "server")]
891
891
+
cache_impl::insert(&self.entry_cache, (ident, rkey), result.clone());
892
892
+
893
893
+
Ok(Some(result))
787
894
}
788
895
}
789
789
-
790
790
-
// #[cfg(feature = "server")]
791
791
-
// #[derive(Clone)]
792
792
-
// pub struct Fetcher {
793
793
-
// pub client: Arc<Client>,
794
794
-
// book_cache: cache_impl::Cache<
795
795
-
// (AtIdentifier<'static>, SmolStr),
796
796
-
// Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>,
797
797
-
// >,
798
798
-
// entry_cache: cache_impl::Cache<
799
799
-
// (AtIdentifier<'static>, SmolStr),
800
800
-
// Arc<(BookEntryView<'static>, Entry<'static>)>,
801
801
-
// >,
802
802
-
// profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>,
803
803
-
// }
804
804
-
805
805
-
// // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM
806
806
-
// //#[cfg(feature = "server")]
807
807
-
// unsafe impl Sync for Fetcher {}
808
808
-
809
809
-
// // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM
810
810
-
// //#[cfg(feature = "server")]
811
811
-
// unsafe impl Send for Fetcher {}
812
812
-
813
813
-
// #[cfg(feature = "server")]
814
814
-
// impl Fetcher {
815
815
-
// pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self {
816
816
-
// Self {
817
817
-
// client: Arc::new(Client::new(client)),
818
818
-
// book_cache: cache_impl::new_cache(100, Duration::from_secs(30)),
819
819
-
// entry_cache: cache_impl::new_cache(100, Duration::from_secs(30)),
820
820
-
// profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)),
821
821
-
// }
822
822
-
// }
823
823
-
824
824
-
// pub async fn upgrade_to_authenticated(
825
825
-
// &self,
826
826
-
// session: OAuthSession<JacquardResolver, crate::auth::AuthStore>,
827
827
-
// ) {
828
828
-
// let mut session_slot = self.client.session.write().await;
829
829
-
// *session_slot = Some(Arc::new(Agent::new(session)));
830
830
-
// }
831
831
-
832
832
-
// pub async fn downgrade_to_unauthenticated(&self) {
833
833
-
// let mut session_slot = self.client.session.write().await;
834
834
-
// if let Some(session) = session_slot.take() {
835
835
-
// session.inner().logout().await.ok();
836
836
-
// }
837
837
-
// }
838
838
-
839
839
-
// #[allow(dead_code)]
840
840
-
// pub async fn current_did(&self) -> Option<Did<'static>> {
841
841
-
// let session_slot = self.client.session.read().await;
842
842
-
// if let Some(session) = session_slot.as_ref() {
843
843
-
// session.info().await.map(|(d, _)| d)
844
844
-
// } else {
845
845
-
// None
846
846
-
// }
847
847
-
// }
848
848
-
849
849
-
// pub fn get_client(&self) -> Arc<Client> {
850
850
-
// self.client.clone()
851
851
-
// }
852
852
-
853
853
-
// pub async fn get_notebook(
854
854
-
// &self,
855
855
-
// ident: AtIdentifier<'static>,
856
856
-
// title: SmolStr,
857
857
-
// ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> {
858
858
-
// if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) {
859
859
-
// Ok(Some(entry))
860
860
-
// } else {
861
861
-
// let client = self.get_client();
862
862
-
// if let Some((notebook, entries)) = client
863
863
-
// .notebook_by_title(&ident, &title)
864
864
-
// .await
865
865
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?
866
866
-
// {
867
867
-
// let stored = Arc::new((notebook, entries));
868
868
-
// cache_impl::insert(&self.book_cache, (ident, title), stored.clone());
869
869
-
// Ok(Some(stored))
870
870
-
// } else {
871
871
-
// Ok(None)
872
872
-
// }
873
873
-
// }
874
874
-
// }
875
875
-
876
876
-
// pub async fn get_entry(
877
877
-
// &self,
878
878
-
// ident: AtIdentifier<'static>,
879
879
-
// book_title: SmolStr,
880
880
-
// entry_title: SmolStr,
881
881
-
// ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> {
882
882
-
// if let Some(result) = self.get_notebook(ident.clone(), book_title).await? {
883
883
-
// let (notebook, entries) = result.as_ref();
884
884
-
// if let Some(entry) =
885
885
-
// cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone()))
886
886
-
// {
887
887
-
// Ok(Some(entry))
888
888
-
// } else {
889
889
-
// let client = self.get_client();
890
890
-
// if let Some(entry) = client
891
891
-
// .entry_by_title(notebook, entries.as_ref(), &entry_title)
892
892
-
// .await
893
893
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?
894
894
-
// {
895
895
-
// let stored = Arc::new(entry);
896
896
-
// cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone());
897
897
-
// Ok(Some(stored))
898
898
-
// } else {
899
899
-
// Ok(None)
900
900
-
// }
901
901
-
// }
902
902
-
// } else {
903
903
-
// Ok(None)
904
904
-
// }
905
905
-
// }
906
906
-
907
907
-
// pub async fn fetch_notebooks_from_ufos(
908
908
-
// &self,
909
909
-
// ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> {
910
910
-
// use jacquard::{IntoStatic, types::aturi::AtUri};
911
911
-
912
912
-
// let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book";
913
913
-
// let response = reqwest::get(url)
914
914
-
// .await
915
915
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?;
916
916
-
917
917
-
// let records: Vec<UfosRecord> = response
918
918
-
// .json()
919
919
-
// .await
920
920
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?;
921
921
-
922
922
-
// let mut notebooks = Vec::new();
923
923
-
// let client = self.get_client();
924
924
-
925
925
-
// for ufos_record in records {
926
926
-
// // Construct URI
927
927
-
// let uri_str = format!(
928
928
-
// "at://{}/{}/{}",
929
929
-
// ufos_record.did, ufos_record.collection, ufos_record.rkey
930
930
-
// );
931
931
-
// let uri = AtUri::new_owned(uri_str)
932
932
-
// .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?;
933
933
-
934
934
-
// // Fetch the full notebook view (which hydrates authors)
935
935
-
// match client.view_notebook(&uri).await {
936
936
-
// Ok((notebook, entries)) => {
937
937
-
// let ident = uri.authority().clone().into_static();
938
938
-
// let title = notebook
939
939
-
// .title
940
940
-
// .as_ref()
941
941
-
// .map(|t| SmolStr::new(t.as_ref()))
942
942
-
// .unwrap_or_else(|| SmolStr::new("Untitled"));
943
943
-
944
944
-
// let result = Arc::new((notebook, entries));
945
945
-
// // Cache it
946
946
-
// cache_impl::insert(&self.book_cache, (ident, title), result.clone());
947
947
-
// notebooks.push(result);
948
948
-
// }
949
949
-
// Err(_) => continue, // Skip notebooks that fail to load
950
950
-
// }
951
951
-
// }
952
952
-
953
953
-
// Ok(notebooks)
954
954
-
// }
955
955
-
956
956
-
// pub async fn fetch_notebooks_for_did(
957
957
-
// &self,
958
958
-
// ident: &AtIdentifier<'_>,
959
959
-
// ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> {
960
960
-
// use jacquard::{
961
961
-
// IntoStatic,
962
962
-
// types::{collection::Collection, nsid::Nsid},
963
963
-
// xrpc::XrpcExt,
964
964
-
// };
965
965
-
// use weaver_api::{
966
966
-
// com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book,
967
967
-
// };
968
968
-
969
969
-
// let client = self.get_client();
970
970
-
971
971
-
// // Resolve DID and PDS
972
972
-
// let (repo_did, pds_url) = match ident {
973
973
-
// AtIdentifier::Did(did) => {
974
974
-
// let pds = client
975
975
-
// .pds_for_did(did)
976
976
-
// .await
977
977
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?;
978
978
-
// (did.clone(), pds)
979
979
-
// }
980
980
-
// AtIdentifier::Handle(handle) => client
981
981
-
// .pds_for_handle(handle)
982
982
-
// .await
983
983
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?,
984
984
-
// };
985
985
-
986
986
-
// // Fetch all notebook records for this repo
987
987
-
// let resp = client
988
988
-
// .xrpc(pds_url)
989
989
-
// .send(
990
990
-
// &ListRecords::new()
991
991
-
// .repo(repo_did)
992
992
-
// .collection(Nsid::raw(Book::NSID))
993
993
-
// .limit(100)
994
994
-
// .build(),
995
995
-
// )
996
996
-
// .await
997
997
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?;
998
998
-
999
999
-
// let mut notebooks = Vec::new();
1000
1000
-
1001
1001
-
// if let Ok(list) = resp.parse() {
1002
1002
-
// for record in list.records {
1003
1003
-
// // View the notebook (which hydrates authors)
1004
1004
-
// match client.view_notebook(&record.uri).await {
1005
1005
-
// Ok((notebook, entries)) => {
1006
1006
-
// let ident = record.uri.authority().clone().into_static();
1007
1007
-
// let title = notebook
1008
1008
-
// .title
1009
1009
-
// .as_ref()
1010
1010
-
// .map(|t| SmolStr::new(t.as_ref()))
1011
1011
-
// .unwrap_or_else(|| SmolStr::new("Untitled"));
1012
1012
-
1013
1013
-
// let result = Arc::new((notebook, entries));
1014
1014
-
// // Cache it
1015
1015
-
// cache_impl::insert(&self.book_cache, (ident, title), result.clone());
1016
1016
-
// notebooks.push(result);
1017
1017
-
// }
1018
1018
-
// Err(_) => continue, // Skip notebooks that fail to load
1019
1019
-
// }
1020
1020
-
// }
1021
1021
-
// }
1022
1022
-
1023
1023
-
// Ok(notebooks)
1024
1024
-
// }
1025
1025
-
1026
1026
-
// pub async fn list_notebook_entries(
1027
1027
-
// &self,
1028
1028
-
// ident: AtIdentifier<'static>,
1029
1029
-
// book_title: SmolStr,
1030
1030
-
// ) -> Result<Option<Vec<BookEntryView<'static>>>> {
1031
1031
-
// if let Some(result) = self.get_notebook(ident.clone(), book_title).await? {
1032
1032
-
// let (notebook, entries) = result.as_ref();
1033
1033
-
// let mut book_entries = Vec::new();
1034
1034
-
// let client = self.get_client();
1035
1035
-
1036
1036
-
// for index in 0..entries.len() {
1037
1037
-
// match client.view_entry(notebook, entries, index).await {
1038
1038
-
// Ok(book_entry) => book_entries.push(book_entry),
1039
1039
-
// Err(_) => continue, // Skip entries that fail to load
1040
1040
-
// }
1041
1041
-
// }
1042
1042
-
1043
1043
-
// Ok(Some(book_entries))
1044
1044
-
// } else {
1045
1045
-
// Ok(None)
1046
1046
-
// }
1047
1047
-
// }
1048
1048
-
1049
1049
-
// pub async fn fetch_profile(
1050
1050
-
// &self,
1051
1051
-
// ident: &AtIdentifier<'_>,
1052
1052
-
// ) -> Result<Arc<ProfileDataView<'static>>> {
1053
1053
-
// use jacquard::IntoStatic;
1054
1054
-
1055
1055
-
// let ident_static = ident.clone().into_static();
1056
1056
-
1057
1057
-
// if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) {
1058
1058
-
// return Ok(cached);
1059
1059
-
// }
1060
1060
-
1061
1061
-
// let client = self.get_client();
1062
1062
-
1063
1063
-
// let did = match ident {
1064
1064
-
// AtIdentifier::Did(d) => d.clone(),
1065
1065
-
// AtIdentifier::Handle(h) => client
1066
1066
-
// .resolve_handle(h)
1067
1067
-
// .await
1068
1068
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?,
1069
1069
-
// };
1070
1070
-
1071
1071
-
// let (_uri, profile_view) = client
1072
1072
-
// .hydrate_profile_view(&did)
1073
1073
-
// .await
1074
1074
-
// .map_err(|e| dioxus::CapturedError::from_display(e))?;
1075
1075
-
1076
1076
-
// let result = Arc::new(profile_view);
1077
1077
-
// cache_impl::insert(&self.profile_cache, ident_static, result.clone());
1078
1078
-
1079
1079
-
// Ok(result)
1080
1080
-
// }
1081
1081
-
// }
1082
896
1083
897
impl HttpClient for Fetcher {
1084
898
type Error = IdentityError;
+143
-45
crates/weaver-app/src/main.rs
···
31
31
#[cfg(feature = "server")]
32
32
mod blobcache;
33
33
mod cache_impl;
34
34
-
#[cfg(feature = "server")]
35
35
-
mod og;
36
34
/// Define a components module that contains all shared components for our app.
37
35
mod components;
38
36
mod config;
39
37
mod data;
40
38
mod env;
41
39
mod fetch;
40
40
+
#[cfg(feature = "server")]
41
41
+
mod og;
42
42
mod record_utils;
43
43
mod service_worker;
44
44
/// Define a views module that contains the UI for all Layouts and Routes for our app.
···
207
207
}
208
208
}
209
209
}))
210
210
+
// .layer(axum::middleware::from_fn(
211
211
+
// |request: Request, next: Next| async move {
212
212
+
// let mut res = next.run(request).await;
213
213
+
214
214
+
// // Cache all HTML responses
215
215
+
// if res
216
216
+
// .headers()
217
217
+
// .get("content-type")
218
218
+
// .and_then(|v| v.to_str().ok())
219
219
+
// .map(|t| t.contains("text/html"))
220
220
+
// .unwrap_or(false)
221
221
+
// {
222
222
+
// res.headers_mut().insert(
223
223
+
// http::header::CACHE_CONTROL,
224
224
+
// "public, max-age=300".parse().unwrap(),
225
225
+
// );
226
226
+
// }
227
227
+
// res
228
228
+
// },
229
229
+
// ))
210
230
};
211
231
Ok(router)
212
232
});
···
432
452
entry_title: SmolStr,
433
453
) -> Result<axum::response::Response> {
434
454
use axum::{
435
435
-
http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode},
455
455
+
http::{
456
456
+
StatusCode,
457
457
+
header::{CACHE_CONTROL, CONTENT_TYPE},
458
458
+
},
436
459
response::IntoResponse,
437
460
};
438
461
use weaver_api::sh_weaver::actor::ProfileDataViewInner;
···
446
469
};
447
470
448
471
// Fetch entry data
449
449
-
let entry_result = fetcher.get_entry(at_ident.clone(), book_title.clone(), entry_title.into()).await;
472
472
+
let entry_result = fetcher
473
473
+
.get_entry(at_ident.clone(), book_title.clone(), entry_title.into())
474
474
+
.await;
450
475
451
476
let arc_data = match entry_result {
452
477
Ok(Some(data)) => data,
···
470
495
(CACHE_CONTROL, "public, max-age=3600"),
471
496
],
472
497
cached,
473
473
-
).into_response());
498
498
+
)
499
499
+
.into_response());
474
500
}
475
501
476
502
// Extract metadata
···
480
506
// TODO: Could fetch actual notebook record to get display title
481
507
let notebook_title_str: String = book_title.to_string();
482
508
483
483
-
let author_handle = book_entry.entry.authors.first()
509
509
+
let author_handle = book_entry
510
510
+
.entry
511
511
+
.authors
512
512
+
.first()
484
513
.map(|a| match &a.record.inner {
485
514
ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(),
486
515
ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(),
···
504
533
// Build CDN URL
505
534
let cdn_url = format!(
506
535
"https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}",
507
507
-
did.as_str(), cid.as_ref(), format
536
536
+
did.as_str(),
537
537
+
cid.as_ref(),
538
538
+
format
508
539
);
509
540
510
541
// Fetch the image
···
513
544
match response.bytes().await {
514
545
Ok(bytes) => {
515
546
use base64::Engine;
516
516
-
let base64_str = base64::engine::general_purpose::STANDARD.encode(&bytes);
547
547
+
let base64_str =
548
548
+
base64::engine::general_purpose::STANDARD.encode(&bytes);
517
549
Some(format!("data:{};base64,{}", mime, base64_str))
518
550
}
519
519
-
Err(_) => None
551
551
+
Err(_) => None,
520
552
}
521
553
}
522
522
-
_ => None
554
554
+
_ => None,
523
555
}
524
556
} else {
525
557
None
···
556
588
match og::generate_hero_image(hero_data, title, ¬ebook_title_str, &author_handle) {
557
589
Ok(bytes) => bytes,
558
590
Err(e) => {
559
559
-
tracing::error!("Failed to generate hero OG image: {:?}, falling back to text", e);
591
591
+
tracing::error!(
592
592
+
"Failed to generate hero OG image: {:?}, falling back to text",
593
593
+
e
594
594
+
);
560
595
og::generate_text_only(title, &content_snippet, ¬ebook_title_str, &author_handle)
561
596
.map_err(|e| {
562
597
tracing::error!("Failed to generate text OG image: {:?}", e);
···
570
605
Ok(bytes) => bytes,
571
606
Err(e) => {
572
607
tracing::error!("Failed to generate OG image: {:?}", e);
573
573
-
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response());
608
608
+
return Ok((
609
609
+
StatusCode::INTERNAL_SERVER_ERROR,
610
610
+
"Failed to generate image",
611
611
+
)
612
612
+
.into_response());
574
613
}
575
614
}
576
615
};
···
584
623
(CACHE_CONTROL, "public, max-age=3600"),
585
624
],
586
625
png_bytes,
587
587
-
).into_response())
626
626
+
)
627
627
+
.into_response())
588
628
}
589
629
590
630
// Route: /og/notebook/{ident}/{book_title}.png - OpenGraph image for notebook index
···
595
635
book_title: SmolStr,
596
636
) -> Result<axum::response::Response> {
597
637
use axum::{
598
598
-
http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode},
638
638
+
http::{
639
639
+
StatusCode,
640
640
+
header::{CACHE_CONTROL, CONTENT_TYPE},
641
641
+
},
599
642
response::IntoResponse,
600
643
};
601
644
use weaver_api::sh_weaver::actor::ProfileDataViewInner;
···
608
651
};
609
652
610
653
// Fetch notebook data
611
611
-
let notebook_result = fetcher.get_notebook(at_ident.clone(), book_title.into()).await;
654
654
+
let notebook_result = fetcher
655
655
+
.get_notebook(at_ident.clone(), book_title.into())
656
656
+
.await;
612
657
613
658
let arc_data = match notebook_result {
614
659
Ok(Some(data)) => data,
615
660
Ok(None) => return Ok((StatusCode::NOT_FOUND, "Notebook not found").into_response()),
616
661
Err(e) => {
617
662
tracing::error!("Failed to fetch notebook for OG image: {:?}", e);
618
618
-
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch notebook").into_response());
663
663
+
return Ok((
664
664
+
StatusCode::INTERNAL_SERVER_ERROR,
665
665
+
"Failed to fetch notebook",
666
666
+
)
667
667
+
.into_response());
619
668
}
620
669
};
621
670
let (notebook_view, _entries) = arc_data.as_ref();
···
632
681
(CACHE_CONTROL, "public, max-age=3600"),
633
682
],
634
683
cached,
635
635
-
).into_response());
684
684
+
)
685
685
+
.into_response());
636
686
}
637
687
638
688
// Extract metadata
639
639
-
let title = notebook_view.title
689
689
+
let title = notebook_view
690
690
+
.title
640
691
.as_ref()
641
692
.map(|t| t.as_ref())
642
693
.unwrap_or("Untitled Notebook");
643
694
644
644
-
let author_handle = notebook_view.authors.first()
695
695
+
let author_handle = notebook_view
696
696
+
.authors
697
697
+
.first()
645
698
.map(|a| match &a.record.inner {
646
699
ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(),
647
700
ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(),
···
651
704
.unwrap_or_else(|| "unknown".to_string());
652
705
653
706
// Fetch entries to get entry titles and count
654
654
-
let entries_result = fetcher.list_notebook_entries(at_ident.clone(), book_title.into()).await;
707
707
+
let entries_result = fetcher
708
708
+
.list_notebook_entries(at_ident.clone(), book_title.into())
709
709
+
.await;
655
710
let (entry_count, entry_titles) = match entries_result {
656
711
Ok(Some(entries)) => {
657
712
let count = entries.len();
···
659
714
.iter()
660
715
.take(4)
661
716
.map(|e| {
662
662
-
e.entry.title
717
717
+
e.entry
718
718
+
.title
663
719
.as_ref()
664
720
.map(|t| t.as_ref().to_string())
665
721
.unwrap_or_else(|| "Untitled".to_string())
···
671
727
};
672
728
673
729
// Generate image
674
674
-
let png_bytes = match og::generate_notebook_og(title, &author_handle, entry_count, entry_titles) {
730
730
+
let png_bytes = match og::generate_notebook_og(title, &author_handle, entry_count, entry_titles)
731
731
+
{
675
732
Ok(bytes) => bytes,
676
733
Err(e) => {
677
734
tracing::error!("Failed to generate notebook OG image: {:?}", e);
678
678
-
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response());
735
735
+
return Ok((
736
736
+
StatusCode::INTERNAL_SERVER_ERROR,
737
737
+
"Failed to generate image",
738
738
+
)
739
739
+
.into_response());
679
740
}
680
741
};
681
742
···
688
749
(CACHE_CONTROL, "public, max-age=3600"),
689
750
],
690
751
png_bytes,
691
691
-
).into_response())
752
752
+
)
753
753
+
.into_response())
692
754
}
693
755
694
756
// Route: /og/profile/{ident}.png - OpenGraph image for profile/repository
695
757
#[cfg(all(feature = "fullstack-server", feature = "server"))]
696
758
#[get("/og/profile/{ident}", fetcher: Extension<Arc<fetch::Fetcher>>)]
697
697
-
pub async fn og_profile_image(
698
698
-
ident: SmolStr,
699
699
-
) -> Result<axum::response::Response> {
759
759
+
pub async fn og_profile_image(ident: SmolStr) -> Result<axum::response::Response> {
700
760
use axum::{
701
701
-
http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode},
761
761
+
http::{
762
762
+
StatusCode,
763
763
+
header::{CACHE_CONTROL, CONTENT_TYPE},
764
764
+
},
702
765
response::IntoResponse,
703
766
};
704
767
use weaver_api::sh_weaver::actor::ProfileDataViewInner;
···
717
780
Ok(data) => data,
718
781
Err(e) => {
719
782
tracing::error!("Failed to fetch profile for OG image: {:?}", e);
720
720
-
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch profile").into_response());
783
783
+
return Ok(
784
784
+
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch profile").into_response(),
785
785
+
);
721
786
}
722
787
};
723
788
···
725
790
// Use DID as cache key since profiles don't have a CID field
726
791
let (display_name, handle, bio, avatar_url, banner_url, cache_id) = match &profile_view.inner {
727
792
ProfileDataViewInner::ProfileView(p) => (
728
728
-
p.display_name.as_ref().map(|n| n.as_ref().to_string()).unwrap_or_default(),
793
793
+
p.display_name
794
794
+
.as_ref()
795
795
+
.map(|n| n.as_ref().to_string())
796
796
+
.unwrap_or_default(),
729
797
p.handle.as_ref().to_string(),
730
730
-
p.description.as_ref().map(|d| d.as_ref().to_string()).unwrap_or_default(),
798
798
+
p.description
799
799
+
.as_ref()
800
800
+
.map(|d| d.as_ref().to_string())
801
801
+
.unwrap_or_default(),
731
802
p.avatar.as_ref().map(|u| u.as_ref().to_string()),
732
803
None::<String>,
733
804
p.did.as_ref().to_string(),
734
805
),
735
806
ProfileDataViewInner::ProfileViewDetailed(p) => (
736
736
-
p.display_name.as_ref().map(|n| n.as_ref().to_string()).unwrap_or_default(),
807
807
+
p.display_name
808
808
+
.as_ref()
809
809
+
.map(|n| n.as_ref().to_string())
810
810
+
.unwrap_or_default(),
737
811
p.handle.as_ref().to_string(),
738
738
-
p.description.as_ref().map(|d| d.as_ref().to_string()).unwrap_or_default(),
812
812
+
p.description
813
813
+
.as_ref()
814
814
+
.map(|d| d.as_ref().to_string())
815
815
+
.unwrap_or_default(),
739
816
p.avatar.as_ref().map(|u| u.as_ref().to_string()),
740
817
p.banner.as_ref().map(|u| u.as_ref().to_string()),
741
818
p.did.as_ref().to_string(),
···
762
839
(CACHE_CONTROL, "public, max-age=3600"),
763
840
],
764
841
cached,
765
765
-
).into_response());
842
842
+
)
843
843
+
.into_response());
766
844
}
767
845
768
846
// Fetch notebook count
···
828
906
) {
829
907
Ok(bytes) => bytes,
830
908
Err(e) => {
831
831
-
tracing::error!("Failed to generate profile banner OG image: {:?}, falling back", e);
832
832
-
og::generate_profile_og(&display_name, &handle, &bio, avatar_data, notebook_count)
833
833
-
.unwrap_or_default()
909
909
+
tracing::error!(
910
910
+
"Failed to generate profile banner OG image: {:?}, falling back",
911
911
+
e
912
912
+
);
913
913
+
og::generate_profile_og(
914
914
+
&display_name,
915
915
+
&handle,
916
916
+
&bio,
917
917
+
avatar_data,
918
918
+
notebook_count,
919
919
+
)
920
920
+
.unwrap_or_default()
834
921
}
835
922
}
836
923
} else {
···
842
929
Ok(bytes) => bytes,
843
930
Err(e) => {
844
931
tracing::error!("Failed to generate profile OG image: {:?}", e);
845
845
-
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response());
932
932
+
return Ok((
933
933
+
StatusCode::INTERNAL_SERVER_ERROR,
934
934
+
"Failed to generate image",
935
935
+
)
936
936
+
.into_response());
846
937
}
847
938
}
848
939
};
···
856
947
(CACHE_CONTROL, "public, max-age=3600"),
857
948
],
858
949
png_bytes,
859
859
-
).into_response())
950
950
+
)
951
951
+
.into_response())
860
952
}
861
953
862
954
// Route: /og/site.png - OpenGraph image for homepage
···
864
956
#[get("/og/site.png")]
865
957
pub async fn og_site_image() -> Result<axum::response::Response> {
866
958
use axum::{
867
867
-
http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode},
959
959
+
http::{
960
960
+
StatusCode,
961
961
+
header::{CACHE_CONTROL, CONTENT_TYPE},
962
962
+
},
868
963
response::IntoResponse,
869
964
};
870
965
871
966
// Site OG is static, cache aggressively
872
967
static SITE_OG_CACHE: std::sync::OnceLock<Vec<u8>> = std::sync::OnceLock::new();
873
968
874
874
-
let png_bytes = SITE_OG_CACHE.get_or_init(|| {
875
875
-
og::generate_site_og().unwrap_or_default()
876
876
-
});
969
969
+
let png_bytes = SITE_OG_CACHE.get_or_init(|| og::generate_site_og().unwrap_or_default());
877
970
878
971
if png_bytes.is_empty() {
879
879
-
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response());
972
972
+
return Ok((
973
973
+
StatusCode::INTERNAL_SERVER_ERROR,
974
974
+
"Failed to generate image",
975
975
+
)
976
976
+
.into_response());
880
977
}
881
978
882
979
Ok((
···
885
982
(CACHE_CONTROL, "public, max-age=86400"),
886
983
],
887
984
png_bytes.clone(),
888
888
-
).into_response())
985
985
+
)
986
986
+
.into_response())
889
987
}
890
988
891
989
// #[server(endpoint = "static_routes", output = server_fn::codec::Json)]