ssh-portfolio/src/atproto/mod.rs
Erica Marigold 3c089bbdc3
style: apply rustfmt formatting to all files
Also moves rust toolchain to a nightly equivalent of stable 1.87, in
order to allow for existing rustfmt configs such as imports granularity
to be used.
2025-08-19 07:01:31 +01:00

105 lines
3.7 KiB
Rust

#[cfg(feature = "blog")]
pub mod com;
#[cfg(feature = "blog")]
pub mod blog {
use std::str::FromStr as _;
use atrium_api::agent::atp_agent::store::MemorySessionStore;
use atrium_api::agent::atp_agent::CredentialSession;
use atrium_api::agent::Agent;
use atrium_api::com::atproto::repo::list_records;
use atrium_api::com::atproto::server::create_session::OutputData as SessionOutputData;
use atrium_api::types::string::{AtIdentifier, Handle};
use atrium_api::types::{Collection as _, Object, Unknown};
use atrium_common::store::memory::MemoryStore;
use atrium_common::store::Store;
use atrium_xrpc_client::reqwest::ReqwestClient;
use color_eyre::eyre::eyre;
use color_eyre::Result;
use ipld_core::ipld::Ipld;
use lazy_static::lazy_static;
use tokio::time::{Duration, Instant};
use tracing::instrument;
use super::*;
const CACHE_INVALIDATION_PERIOD: Duration = Duration::from_secs(30 * 60); // 30 minutes
lazy_static! {
static ref POSTS_CACHE_STORE: MemoryStore<usize, (Instant, com::whtwnd::blog::entry::Record)> =
MemoryStore::default();
static ref AGENT: Agent<CredentialSession<MemoryStore<(), Object<SessionOutputData>>, ReqwestClient>> =
Agent::new(CredentialSession::new(
ReqwestClient::new("https://bsky.social"),
MemorySessionStore::default(),
));
}
#[instrument(level = "debug")]
pub async fn get_all_posts() -> Result<Vec<com::whtwnd::blog::entry::Record>> {
let mut i = 0;
let mut posts = Vec::new();
while let Some((cache_creation_time, post)) = POSTS_CACHE_STORE.get(&i).await? {
if cache_creation_time.elapsed() > CACHE_INVALIDATION_PERIOD {
tracing::info!("Cache for post #{} is stale, fetching new posts", i);
POSTS_CACHE_STORE.clear().await?;
return fetch_posts_into_cache().await;
}
posts.push(post);
i += 1;
}
if posts.is_empty() {
tracing::info!("No blog posts found in cache, fetching from ATProto");
return fetch_posts_into_cache().await;
}
Ok(posts)
}
#[instrument(level = "trace")]
async fn fetch_posts_into_cache() -> Result<Vec<com::whtwnd::blog::entry::Record>> {
let records = &AGENT
.api
.com
.atproto
.repo
.list_records(list_records::Parameters {
extra_data: Ipld::Null,
data: list_records::ParametersData {
collection: com::whtwnd::blog::Entry::nsid(),
cursor: None,
limit: None,
reverse: None,
repo: AtIdentifier::Handle(
Handle::from_str("devcomp.xyz")
.map_err(|_| eyre!("Invalid repo handle"))?,
),
},
})
.await?
.records;
let posts = records
.iter()
.map(|elem| {
if let Unknown::Object(btree_map) = &elem.data.value {
let ser = serde_json::to_string(&btree_map)?;
let des = serde_json::from_str::<com::whtwnd::blog::entry::Record>(&ser)?;
return Ok(des);
}
Err(eyre!("Did not get posts back from atproto"))
})
.collect::<Result<Vec<com::whtwnd::blog::entry::Record>>>()?;
for (i, post) in posts.iter().enumerate() {
POSTS_CACHE_STORE.set(i, (Instant::now(), post.clone())).await?;
}
Ok(posts)
}
}