Something went wrong. Try again.
A personal app view to see Bsky posts of your followers (for when their app view goes down)
Something went wrong. Try again.
13 kB · 423 lines
Rust
at test
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424use super::profile;use crate::external_data;use crate::records;use crate::server::AppState;use crate::server::XrpcErrorResponse;use crate::store;use crate::store::PostRecord;use crate::xrpc::get_blob_uri;
use axum::{Json, extract::State};use jacquard::CowStr;use jacquard::Data;use jacquard::api::app_bsky::feed::post::Post;use jacquard::types::datetime;use jacquard::types::{aturi::AtUri, cid::Cid, ident::AtIdentifier};use jacquard_api::app_bsky::actor::ProfileViewBasic;use jacquard_api::app_bsky::embed::external;use jacquard_api::app_bsky::embed::external::ExternalRecord;use jacquard_api::app_bsky::embed::images;use jacquard_api::app_bsky::embed::images::Images;use jacquard_api::app_bsky::embed::record::Record;use jacquard_api::app_bsky::embed::record::{View, ViewRecord, ViewUnionRecord};use jacquard_api::app_bsky::embed::video;use jacquard_api::app_bsky::embed::video::Video;use jacquard_api::app_bsky::feed::FeedViewPost;use jacquard_api::app_bsky::feed::PostViewEmbed;use jacquard_api::app_bsky::feed::get_timeline::{GetTimelineOutput, GetTimelineRequest};use jacquard_api::app_bsky::feed::post::PostEmbed;use jacquard_api::app_bsky::feed::{FeedViewPostReason, PostView, ReasonRepost};use jacquard_axum::ExtractXrpc;use jacquard_axum::service_auth::ExtractOptionalServiceAuth;use jacquard_common::IntoStatic;use jacquard_identity::JacquardResolver;use std::str::FromStr;
pub async fn app_bsky_feed_get_timeline<'a>( State(state): State<AppState>, ExtractOptionalServiceAuth(_auth): ExtractOptionalServiceAuth, ExtractXrpc(req): ExtractXrpc<GetTimelineRequest>,) -> Result<Json<GetTimelineOutput<'static>>, XrpcErrorResponse> { let posts = store::get_posts(&state.pool, req.cursor, req.limit) .await .map_err(|err| { println!("fetching posts: {err}"); XrpcErrorResponse::internal_server_error() })?;
let mut results: Vec<FeedViewPost> = Vec::with_capacity(posts.len());
let mut last_record_cursor: i64 = 0; for post in posts { last_record_cursor = post.clone().created; let cid = post.cid.clone(); if post.subject != "" { let subject = post.subject.clone(); match build_feed_view_from_repost(state.resolver.clone(), state.pool.clone(), post) .await { Ok(feed_view_post) => results.push(feed_view_post), Err(e) => println!("building feed view repost {cid} {subject} {e}"), } } else { match build_feed_view_from_post( state.resolver.clone(), state.pool.clone(), post.clone(), ) .await { Ok(feed_view_post) => results.push(feed_view_post), Err(e) => println!("building feed view post {e}"), } } }
let mut cursor: Option<CowStr<'a>> = None; if last_record_cursor != 0 { cursor = Some(CowStr::from(last_record_cursor.to_string())); }
Ok(Json(GetTimelineOutput { cursor: cursor.into_static(), feed: results, extra_data: None, }))}
async fn build_feed_view_from_post<'a>( resolver: JacquardResolver, pool: sqlx::SqlitePool, post_record: PostRecord,) -> anyhow::Result<FeedViewPost<'a>> { let post_author = post_record.clone().author.to_string(); let did = AtIdentifier::from_str(&post_record.clone().author.to_string()).unwrap();
let did_doc = external_data::get_did_doc(resolver.clone(), &pool, &post_author).await?;
let author = profile::get_profile_basic(resolver.clone(), pool.clone(), did.to_string()) .await .map_err(|_| anyhow::Error::msg("getting profile"))?;
let indexed_at = datetime::Datetime::from_str(&post_record.clone().indexed.to_rfc3339())?; let raw_blob = post_record.raw_blob.into_static().clone(); let record: Data<'_> = serde_json::from_slice(&raw_blob)?;
let cid = Cid::from_str(&post_record.cid)?; let rkey = post_record.rkey; let at_uri = AtUri::from_str(format!("at://{post_author}/app.bsky.feed.post/{rkey}").as_str())?; let mut post_view = PostView { author: author, bookmark_count: None, cid: cid, debug: None, embed: None, indexed_at: indexed_at.clone(), labels: None, like_count: None, quote_count: None, reply_count: None, repost_count: None, threadgate: None, uri: at_uri.fast_clone(), viewer: None, extra_data: None, record: record.clone().into_static(), };
let post: Post<'_> = serde_json::from_slice(&raw_blob)?;
if let Some(embeded_post) = post.embed { match embeded_post { PostEmbed::Record(rec) => { post_view.embed = get_record_embed(resolver.clone(), pool, rec).await } PostEmbed::Images(images) => { post_view.embed = get_image_embed(images, post_author, did_doc.pds_endpoint); } PostEmbed::Video(video) => { post_view.embed = get_video_embed(video, post_author, did_doc.pds_endpoint) } PostEmbed::External(external) => { post_view.embed = get_external_embed(external, post_author, did_doc.pds_endpoint) } _ => {} } }
Ok(FeedViewPost { feed_context: None, post: post_view.into_static(), reason: None, reply: None, req_id: None, extra_data: None, })}
async fn build_feed_view_from_repost<'a>( resolver: JacquardResolver, pool: sqlx::SqlitePool, post_record: PostRecord,) -> anyhow::Result<FeedViewPost<'a>> { let at_uri = AtUri::raw(post_record.subject.as_str()); let cid = Cid::from_str(&post_record.cid)?;
let subject_author = profile::get_profile_basic( resolver.clone(), pool.clone(), at_uri.authority().to_string(), ) .await .map_err(|_| anyhow::Error::msg("getting profile"))?;
let indexed_at = datetime::Datetime::from_str(&post_record.clone().indexed.to_rfc3339())?; let repost_author_did = post_record.author; let rkey = post_record.rkey; let raw_blob = post_record.subject_raw_blob.into_static().clone(); let record: Data<'_> = serde_json::from_slice(&raw_blob)?;
let mut post_view = PostView { author: subject_author, bookmark_count: None, cid: cid, debug: None, embed: None, indexed_at: indexed_at.clone(), labels: None, like_count: None, quote_count: None, reply_count: None, repost_count: None, threadgate: None, uri: at_uri.fast_clone(), viewer: None, extra_data: None, record: record.clone().into_static(), };
let repost_author = profile::get_profile_basic(resolver.clone(), pool.clone(), repost_author_did.clone()) .await .map_err(|_| { println!("getting repost author profile"); anyhow::Error::msg("getting repost author profile") })?;
let did_doc = external_data::get_did_doc(resolver.clone(), &pool, &at_uri.authority().to_string()) .await?;
let post: Post<'_> = serde_json::from_slice(&raw_blob)?;
if let Some(embeded_post) = post.embed { match embeded_post { PostEmbed::Record(rec) => { post_view.embed = get_record_embed(resolver.clone(), pool, rec).await } PostEmbed::Images(images) => { post_view.embed = get_image_embed(images, did_doc.did, did_doc.pds_endpoint); } PostEmbed::External(external) => { post_view.embed = get_external_embed(external, did_doc.did, did_doc.pds_endpoint); } _ => {} } }
let repost_at_uri = AtUri::from_str(format!("at://{repost_author_did}/app.bsky.feed.post/{rkey}").as_str())?; let reason_repost = ReasonRepost { by: repost_author, cid: None, indexed_at: indexed_at.clone(), uri: Some(repost_at_uri), extra_data: None, };
let reason = FeedViewPostReason::ReasonRepost(Box::new(reason_repost));
Ok(FeedViewPost { feed_context: None, post: post_view.into_static(), reason: Some(reason), reply: None, req_id: None, extra_data: None, })}
async fn get_record_embed<'a>( resolver: JacquardResolver, pool: sqlx::SqlitePool, rec: Box<Record<'a>>,) -> Option<PostViewEmbed<'a>> { if let Some(view_record) = created_embeded_record_view(resolver, pool, rec).await { let view = View { record: ViewUnionRecord::ViewRecord(Box::new(view_record)), extra_data: None, }; return Some(PostViewEmbed::RecordView(Box::new(view))); } None}
fn get_image_embed<'a>( images: Box<Images<'a>>, post_author: String, pds_endpoint: String,) -> Option<PostViewEmbed<'a>> { let images_view = images .images .into_iter() .filter_map(|image| { let blob_uri = get_blob_uri(Some(image.image), post_author.clone(), pds_endpoint.clone())?;
Some(images::ViewImage { alt: image.alt, aspect_ratio: image.aspect_ratio, fullsize: blob_uri.clone(), thumb: blob_uri, extra_data: None, }) }) .collect(); let view = images::View { images: images_view, extra_data: None, };
return Some(PostViewEmbed::ImagesView(Box::new(view)));}
fn get_video_embed<'a>( video: Box<Video<'a>>, post_author: String, pds_endpoint: String,) -> Option<PostViewEmbed<'a>> { let cid = video.video.blob().cid().clone(); let playlist = get_blob_uri(Some(video.video), post_author, pds_endpoint)?; let presentation = video .presentation .map(|pres| video::ViewPresentation::from(pres.to_string())); let view = video::View { aspect_ratio: video.aspect_ratio, alt: video.alt, cid: cid, playlist: playlist, presentation: presentation, extra_data: None, thumbnail: None, };
return Some(PostViewEmbed::VideoView(Box::new(view)));}
fn get_external_embed<'a>( external: Box<ExternalRecord<'a>>, post_author: String, pds_endpoint: String,) -> Option<PostViewEmbed<'a>> { let thumb_uri = get_blob_uri( external.external.thumb, post_author.clone(), pds_endpoint.clone(), );
let external_view = external::ViewExternal { description: external.external.description, thumb: thumb_uri, title: external.external.title, uri: external.external.uri, extra_data: None, };
let view = external::View { external: external_view, extra_data: None, }; return Some(PostViewEmbed::ExternalView(Box::new(view)));}
async fn created_embeded_record_view<'a>( resolver: JacquardResolver, pool: sqlx::SqlitePool, record: Box<Record<'a>>,) -> Option<ViewRecord<'a>> { let uri = record.record.uri.to_string(); let author = match get_embeded_post_author_profile(resolver.clone(), pool.clone(), uri.clone()).await { Some(profile) => profile, None => return None, };
let at_uri = match AtUri::new(&uri) { Err(_) => return None, Ok(at_uri) => at_uri, };
let did = match at_uri.authority() { AtIdentifier::Did(did) => did, _ => return None, };
let did_doc = match external_data::get_did_doc(resolver.clone(), &pool, &did.to_string()).await { Ok(doc) => doc, Err(e) => { println!("getting did doc {e}"); return None; } };
let rkey = match at_uri.rkey() { None => return None, Some(rkey) => rkey, };
let embeded_record = match records::get_post_from_pds(did.to_string(), did_doc.pds_endpoint, rkey.0.to_string()) .await { Err(err) => { println!("getting embeded record from pds: {err}"); return None; } Ok(record) => record, };
return Some(ViewRecord { author: author, cid: record.record.cid, embeds: None, indexed_at: datetime::Datetime::now(), labels: None, like_count: None, quote_count: None, reply_count: None, repost_count: None, uri: record.record.uri, value: embeded_record.value, extra_data: None, });}
async fn get_embeded_post_author_profile( resolver: JacquardResolver, pool: sqlx::SqlitePool, uri: String,) -> Option<ProfileViewBasic<'static>> { match AtUri::new(&uri) { Err(_) => return None, Ok(at_uri) => match at_uri.authority() { AtIdentifier::Did(did) => { match profile::get_profile_basic(resolver, pool, did.to_string()).await { Ok(profile) => return Some(profile), Err(_) => return None, } } _ => return None, }, }}