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

mia.omg.lol b47f09ec f425b837

verified
+84 -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)]
+80
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 + .left_join(schema::posts::table.on(schema::posts::at_uri.eq(schema::author_feeds::post))) 38 + .filter( 39 + schema::author_feeds::did 40 + .eq_any(follows_query) 41 + .or(schema::author_feeds::did.eq(&did)), 42 + ) 43 + .into_boxed(); 44 + 45 + if let Some(cursor) = datetime_cursor(query.cursor.as_ref()) { 46 + tl_query = tl_query.filter(schema::author_feeds::sort_at.lt(cursor)); 47 + } 48 + 49 + let results = tl_query 50 + .order(schema::author_feeds::sort_at.desc()) 51 + .limit(limit as i64) 52 + .load(&mut conn) 53 + .await?; 54 + 55 + let cursor = results 56 + .last() 57 + .map(|item| item.sort_at.timestamp_millis().to_string()); 58 + 59 + let raw_feed = results 60 + .into_iter() 61 + .filter_map(|item| match &*item.typ { 62 + "post" => Some(RawFeedItem::Post { 63 + uri: item.post, 64 + context: None, 65 + }), 66 + "repost" => Some(RawFeedItem::Repost { 67 + uri: item.uri, 68 + post: item.post, 69 + by: item.did, 70 + at: item.sort_at, 71 + context: None, 72 + }), 73 + _ => None, 74 + }) 75 + .collect::<Vec<_>>(); 76 + 77 + let feed = hyd.hydrate_feed_posts(raw_feed, false).await; 78 + 79 + Ok(Json(FeedRes { cursor, feed })) 80 + }
+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))