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

make author feeds work like bluesky

mia.omg.lol 6bfd6ae7 f652576d

verified
+113 -53
+102 -41
parakeet/src/hydration/posts.rs
··· 187 187 .collect() 188 188 } 189 189 190 - pub async fn hydrate_feed_posts(&self, posts: Vec<String>) -> HashMap<String, FeedViewPost> { 190 + pub async fn hydrate_feed_posts( 191 + &self, 192 + posts: Vec<String>, 193 + author_threads_only: bool, 194 + ) -> HashMap<String, FeedViewPost> { 191 195 let stats = self.loaders.post_stats.load_many(posts.clone()).await; 192 196 let posts = self.loaders.posts.load_many(posts).await; 193 197 ··· 199 203 200 204 let post_labels = self.get_label_many(&post_uris).await; 201 205 let viewer_data = self.get_post_viewer_states(&post_uris).await; 202 - let embeds = self.hydrate_embeds(post_uris).await; 206 + let embeds = self.hydrate_embeds(post_uris.clone()).await; 203 207 204 208 // we shouldn't show the parent when the post violates a threadgate. 205 209 let reply_refs = posts ··· 211 215 212 216 let reply_posts = self.hydrate_posts(reply_refs).await; 213 217 214 - posts 218 + // hydrate all the posts. 219 + let mut posts = posts 215 220 .into_iter() 216 - .filter_map(|(post_uri, (post, _))| { 217 - let author = authors.get(&post.did)?; 218 - 219 - let root = post.root_uri.as_ref().and_then(|uri| reply_posts.get(uri)); 220 - let parent = post 221 - .parent_uri 222 - .as_ref() 223 - .and_then(|uri| reply_posts.get(uri)); 224 - 225 - let reply = if post.parent_uri.is_some() && post.root_uri.is_some() { 226 - Some(ReplyRef { 227 - root: root.cloned().map(postview_to_replyref).unwrap_or( 228 - ReplyRefPost::NotFound { 229 - uri: post.root_uri.as_ref().unwrap().clone(), 230 - not_found: true, 231 - }, 232 - ), 233 - parent: parent.cloned().map(postview_to_replyref).unwrap_or( 234 - ReplyRefPost::NotFound { 235 - uri: post.parent_uri.as_ref().unwrap().clone(), 236 - not_found: true, 237 - }, 238 - ), 239 - grandparent_author: None, 240 - }) 241 - } else { 242 - None 243 - }; 221 + .filter_map(|(post_uri, (raw, _))| { 222 + let root = raw.root_uri.clone(); 223 + let parent = raw.parent_uri.clone(); 244 224 225 + let author = authors.get(&raw.did)?; 245 226 let embed = embeds.get(&post_uri).cloned(); 246 227 let labels = post_labels.get(&post_uri).cloned().unwrap_or_default(); 247 228 let stats = stats.get(&post_uri).cloned(); 248 229 let viewer = viewer_data.get(&post_uri).cloned(); 249 230 let post = 250 - build_postview(post, author.to_owned(), labels, embed, None, viewer, stats); 231 + build_postview(raw, author.to_owned(), labels, embed, None, viewer, stats); 251 232 252 - Some(( 253 - post_uri, 254 - FeedViewPost { 255 - post, 256 - reply, 257 - reason: None, 258 - feed_context: None, 259 - }, 260 - )) 233 + Some((post_uri, (post, root, parent))) 234 + }) 235 + .collect::<HashMap<_, _>>(); 236 + 237 + post_uris 238 + .into_iter() 239 + .filter_map(|post_uri| { 240 + let item = if author_threads_only { 241 + compile_feed_authors_threads_only(&post_uri, &mut posts)? 242 + } else { 243 + compile_feed(&post_uri, &mut posts, &reply_posts)? 244 + }; 245 + 246 + Some((post_uri, item)) 261 247 }) 262 248 .collect() 263 249 } ··· 301 287 _ => ReplyRefPost::Post(post), 302 288 } 303 289 } 290 + 291 + type FeedViewPartData = (PostView, Option<String>, Option<String>); 292 + 293 + // this is the 'normal' one that runs in most places 294 + fn compile_feed( 295 + uri: &String, 296 + posts: &mut HashMap<String, FeedViewPartData>, 297 + reply_posts: &HashMap<String, PostView>, 298 + ) -> Option<FeedViewPost> { 299 + let (post, root_uri, parent_uri) = posts.remove(uri)?; 300 + 301 + let root = root_uri.as_ref().and_then(|uri| reply_posts.get(uri)); 302 + let parent = parent_uri.as_ref().and_then(|uri| reply_posts.get(uri)); 303 + 304 + let reply = if parent_uri.is_some() && root_uri.is_some() { 305 + Some(ReplyRef { 306 + root: root 307 + .cloned() 308 + .map(postview_to_replyref) 309 + .unwrap_or(ReplyRefPost::NotFound { 310 + uri: root_uri.as_ref().unwrap().clone(), 311 + not_found: true, 312 + }), 313 + parent: parent 314 + .cloned() 315 + .map(postview_to_replyref) 316 + .unwrap_or(ReplyRefPost::NotFound { 317 + uri: parent_uri.as_ref().unwrap().clone(), 318 + not_found: true, 319 + }), 320 + grandparent_author: None, 321 + }) 322 + } else { 323 + None 324 + }; 325 + 326 + Some(FeedViewPost { 327 + post, 328 + reply, 329 + reason: None, 330 + feed_context: None, 331 + }) 332 + } 333 + 334 + // and this one runs in getAuthorFeed when filter=PostsAndAuthorThreads 335 + fn compile_feed_authors_threads_only( 336 + uri: &String, 337 + posts: &mut HashMap<String, FeedViewPartData>, 338 + ) -> Option<FeedViewPost> { 339 + let (post, root_uri, parent_uri) = posts.get(uri)?.clone(); 340 + 341 + let root = root_uri.as_ref().and_then(|root| posts.get(root)); 342 + let parent = parent_uri.as_ref().and_then(|parent| posts.get(parent)); 343 + 344 + let reply = if parent_uri.is_some() && root_uri.is_some() { 345 + Some(ReplyRef { 346 + root: root 347 + .cloned() 348 + .map(|(post, _, _)| postview_to_replyref(post))?, 349 + parent: parent 350 + .cloned() 351 + .map(|(post, _, _)| postview_to_replyref(post))?, 352 + grandparent_author: None, 353 + }) 354 + } else { 355 + None 356 + }; 357 + 358 + Some(FeedViewPost { 359 + post, 360 + reply, 361 + reason: None, 362 + feed_context: None, 363 + }) 364 + }
+1 -1
parakeet/src/xrpc/app_bsky/feed/likes.rs
··· 62 62 .map(|(_, uri)| uri.clone()) 63 63 .collect::<Vec<_>>(); 64 64 65 - let mut posts = hyd.hydrate_feed_posts(at_uris).await; 65 + let mut posts = hyd.hydrate_feed_posts(at_uris, false).await; 66 66 67 67 let feed: Vec<_> = results 68 68 .into_iter()
+10 -11
parakeet/src/xrpc/app_bsky/feed/posts.rs
··· 123 123 }) 124 124 .collect::<Vec<_>>(); 125 125 126 - let mut posts = hyd.hydrate_feed_posts(at_uris).await; 126 + let mut posts = hyd.hydrate_feed_posts(at_uris, false).await; 127 127 let mut repost_data = get_skeleton_repost_data(&mut conn, &hyd, repost_skeleton).await; 128 128 129 129 let feed = skeleton ··· 152 152 })) 153 153 } 154 154 155 - #[derive(Debug, Deserialize)] 155 + #[derive(Debug, Default, Eq, PartialEq, Deserialize)] 156 156 #[serde(rename_all = "snake_case")] 157 157 pub enum GetAuthorFeedFilter { 158 + #[default] 158 159 PostsWithReplies, 159 160 PostsNoReplies, 160 161 PostsWithMedia, 161 162 PostsAndAuthorThreads, 162 163 PostsWithVideo, 163 - } 164 - 165 - impl Default for GetAuthorFeedFilter { 166 - fn default() -> Self { 167 - Self::PostsWithReplies 168 - } 169 164 } 170 165 171 166 #[derive(Debug, Deserialize)] ··· 227 222 posts_query = posts_query.filter(schema::author_feeds::sort_at.lt(cursor)); 228 223 } 229 224 225 + let author_threads_only = query.filter == GetAuthorFeedFilter::PostsAndAuthorThreads; 230 226 posts_query = match query.filter { 231 227 GetAuthorFeedFilter::PostsWithReplies => { 232 228 posts_query.filter(schema::author_feeds::typ.eq("post")) ··· 269 265 .collect::<Vec<_>>(); 270 266 271 267 // get the actor for if we have reposted 272 - let profile = hyd.hydrate_profile_basic(did).await.ok_or(Error::server_error(None))?; 268 + let profile = hyd 269 + .hydrate_profile_basic(did) 270 + .await 271 + .ok_or(Error::server_error(None))?; 273 272 274 - let mut posts = hyd.hydrate_feed_posts(at_uris).await; 273 + let mut posts = hyd.hydrate_feed_posts(at_uris, author_threads_only).await; 275 274 276 275 let mut feed: Vec<_> = results 277 276 .into_iter() ··· 348 347 .map(|(_, uri)| uri.clone()) 349 348 .collect::<Vec<_>>(); 350 349 351 - let mut posts = hyd.hydrate_feed_posts(at_uris).await; 350 + let mut posts = hyd.hydrate_feed_posts(at_uris, false).await; 352 351 353 352 let feed = results 354 353 .into_iter()