Parakeet is a Rust-based Bluesky AppServer aiming to implement most of the functionality required to support the Bluesky client
appview atproto bluesky rust appserver

feat(parakeet): timelines

authored by mia.omg.lol and committed by tangled.org c3b4c199 f425b837

+83 -4
-1
README.md
··· 11 11 - Notifications 12 12 - Search 13 13 - Pinned Posts 14 - - The Timeline 15 14 - Monitoring: metrics, tracing, and health checks. 16 15 17 16 ## The Code
+1
crates/parakeet/src/xrpc/app_bsky/feed/mod.rs
··· 1 1 pub mod feedgen; 2 2 pub mod likes; 3 3 pub mod posts; 4 + pub mod timeline;
+2 -2
crates/parakeet/src/xrpc/app_bsky/feed/posts.rs
··· 33 33 #[derive(Debug, Serialize)] 34 34 pub struct FeedRes { 35 35 #[serde(skip_serializing_if = "Option::is_none")] 36 - cursor: Option<String>, 37 - feed: Vec<FeedViewPost>, 36 + pub cursor: Option<String>, 37 + pub feed: Vec<FeedViewPost>, 38 38 } 39 39 40 40 #[derive(Debug, Deserialize)]
+79
crates/parakeet/src/xrpc/app_bsky/feed/timeline.rs
··· 1 + use super::posts::FeedRes; 2 + use crate::hydration::posts::RawFeedItem; 3 + use crate::hydration::StatefulHydrator; 4 + use crate::xrpc::error::XrpcResult; 5 + use crate::xrpc::extract::{AtpAcceptLabelers, AtpAuth}; 6 + use crate::xrpc::{datetime_cursor, CursorQuery}; 7 + use crate::GlobalState; 8 + use axum::extract::{Query, State}; 9 + use axum::Json; 10 + use diesel::prelude::*; 11 + use diesel_async::RunQueryDsl; 12 + use parakeet_db::{models, schema}; 13 + 14 + // okay so there's debate as to if the TL should show all posts from everyone you follow, or 15 + // just where you follow the poster and the person they're replying to. Maybe this could be an 16 + // option in the config?? Currently, this is the "old" behaviour. 17 + // If we want the "new" version, we'll need to add it into hydrate_feed_posts... 18 + // <mia opinion>i like how it works currently on bsky</mia opinion> 19 + pub async fn get_timeline( 20 + State(state): State<GlobalState>, 21 + AtpAcceptLabelers(labelers): AtpAcceptLabelers, 22 + auth: AtpAuth, 23 + Query(query): Query<CursorQuery>, 24 + ) -> XrpcResult<Json<FeedRes>> { 25 + let mut conn = state.pool.get().await?; 26 + 27 + let did = auth.0.clone(); 28 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, Some(auth)); 29 + let limit = query.limit.unwrap_or(50).clamp(1, 100); 30 + 31 + let follows_query = schema::follows::table 32 + .select(schema::follows::subject) 33 + .filter(schema::follows::did.eq(&did)); 34 + 35 + let mut tl_query = schema::author_feeds::table 36 + .select(models::AuthorFeedItem::as_select()) 37 + .filter( 38 + schema::author_feeds::did 39 + .eq_any(follows_query) 40 + .or(schema::author_feeds::did.eq(&did)), 41 + ) 42 + .into_boxed(); 43 + 44 + if let Some(cursor) = datetime_cursor(query.cursor.as_ref()) { 45 + tl_query = tl_query.filter(schema::author_feeds::sort_at.lt(cursor)); 46 + } 47 + 48 + let results = tl_query 49 + .order(schema::author_feeds::sort_at.desc()) 50 + .limit(limit as i64) 51 + .load(&mut conn) 52 + .await?; 53 + 54 + let cursor = results 55 + .last() 56 + .map(|item| item.sort_at.timestamp_millis().to_string()); 57 + 58 + let raw_feed = results 59 + .into_iter() 60 + .filter_map(|item| match &*item.typ { 61 + "post" => Some(RawFeedItem::Post { 62 + uri: item.post, 63 + context: None, 64 + }), 65 + "repost" => Some(RawFeedItem::Repost { 66 + uri: item.uri, 67 + post: item.post, 68 + by: item.did, 69 + at: item.sort_at, 70 + context: None, 71 + }), 72 + _ => None, 73 + }) 74 + .collect::<Vec<_>>(); 75 + 76 + let feed = hyd.hydrate_feed_posts(raw_feed, false).await; 77 + 78 + Ok(Json(FeedRes { cursor, feed })) 79 + }
+1 -1
crates/parakeet/src/xrpc/app_bsky/mod.rs
··· 34 34 .route("/app.bsky.feed.getQuotes", get(feed::posts::get_quotes)) 35 35 .route("/app.bsky.feed.getRepostedBy", get(feed::posts::get_reposted_by)) 36 36 // TODO: app.bsky.feed.getSuggestedFeeds (recs) 37 - // TODO: app.bsky.feed.getTimeline (complicated) 37 + .route("/app.bsky.feed.getTimeline", get(feed::timeline::get_timeline)) 38 38 // TODO: app.bsky.feed.searchPosts (search) 39 39 .route("/app.bsky.graph.getActorStarterPacks", get(graph::starter_packs::get_actor_starter_packs)) 40 40 .route("/app.bsky.graph.getBlocks", get(graph::relations::get_blocks))