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

show reposts in author feeds

mia.omg.lol f652576d 80379191

verified
+153 -22
+13 -1
migrations/2025-09-27-171241_post-tweaks/down.sql
··· 1 1 alter table posts 2 2 drop column mentions, 3 - drop column violates_threadgate; 3 + drop column violates_threadgate; 4 + 5 + drop trigger t_author_feed_ins_post on posts; 6 + drop trigger t_author_feed_del_post on posts; 7 + drop trigger t_author_feed_ins_repost on reposts; 8 + drop trigger t_author_feed_del_repost on reposts; 9 + 10 + drop function f_author_feed_ins_post; 11 + drop function f_author_feed_del_post; 12 + drop function f_author_feed_ins_repost; 13 + drop function f_author_feed_del_repost; 14 + 15 + drop table author_feeds;
+77 -1
migrations/2025-09-27-171241_post-tweaks/up.sql
··· 1 1 alter table posts 2 2 add column mentions text[], 3 - add column violates_threadgate bool not null default false; 3 + add column violates_threadgate bool not null default false; 4 + 5 + create table author_feeds 6 + ( 7 + uri text primary key, 8 + cid text not null, 9 + post text not null, 10 + did text not null, 11 + typ text not null, 12 + sort_at timestamptz not null 13 + ); 14 + 15 + -- author_feeds post triggers 16 + create function f_author_feed_ins_post() returns trigger 17 + language plpgsql as 18 + $$ 19 + begin 20 + insert into author_feeds (uri, cid, post, did, typ, sort_at) 21 + VALUES (NEW.at_uri, NEW.cid, NEW.at_uri, NEW.did, 'post', NEW.created_at) 22 + on conflict do nothing; 23 + return NEW; 24 + end; 25 + $$; 26 + 27 + create trigger t_author_feed_ins_post 28 + before insert 29 + on posts 30 + for each row 31 + execute procedure f_author_feed_ins_post(); 32 + 33 + create function f_author_feed_del_post() returns trigger 34 + language plpgsql as 35 + $$ 36 + begin 37 + delete from author_feeds where did = OLD.did and item = OLD.at_uri and typ = 'post'; 38 + return OLD; 39 + end; 40 + $$; 41 + 42 + create trigger t_author_feed_del_post 43 + before delete 44 + on posts 45 + for each row 46 + execute procedure f_author_feed_del_post(); 47 + 48 + -- author_feeds repost triggers 49 + create function f_author_feed_ins_repost() returns trigger 50 + language plpgsql as 51 + $$ 52 + begin 53 + insert into author_feeds (uri, cid, post, did, typ, sort_at) 54 + VALUES ('at://' || NEW.did || 'app.bsky.feed.repost' || NEW.rkey, NEW.post_cid, NEW.post, NEW.did, 'repost', NEW.created_at) 55 + on conflict do nothing; 56 + return NEW; 57 + end; 58 + $$; 59 + 60 + create trigger t_author_feed_ins_repost 61 + before insert 62 + on reposts 63 + for each row 64 + execute procedure f_author_feed_ins_repost(); 65 + 66 + create function f_author_feed_del_repost() returns trigger 67 + language plpgsql as 68 + $$ 69 + begin 70 + delete from author_feeds where did = OLD.did and item = OLD.post and typ = 'repost'; 71 + return OLD; 72 + end; 73 + $$; 74 + 75 + create trigger t_author_feed_del_repost 76 + before delete 77 + on reposts 78 + for each row 79 + execute procedure f_author_feed_del_repost();
+13
parakeet-db/src/models.rs
··· 417 417 pub subject_type: &'a str, 418 418 pub tags: Vec<String>, 419 419 } 420 + 421 + #[derive(Debug, Queryable, Selectable, Identifiable)] 422 + #[diesel(table_name = crate::schema::author_feeds)] 423 + #[diesel(primary_key(uri))] 424 + #[diesel(check_for_backend(diesel::pg::Pg))] 425 + pub struct AuthorFeedItem { 426 + pub uri: String, 427 + pub cid: String, 428 + pub post: String, 429 + pub did: String, 430 + pub typ: String, 431 + pub sort_at: DateTime<Utc>, 432 + }
+12
parakeet-db/src/schema.rs
··· 13 13 } 14 14 15 15 diesel::table! { 16 + author_feeds (uri) { 17 + uri -> Text, 18 + cid -> Text, 19 + post -> Text, 20 + did -> Text, 21 + typ -> Text, 22 + sort_at -> Timestamptz, 23 + } 24 + } 25 + 26 + diesel::table! { 16 27 backfill (repo, repo_ver) { 17 28 repo -> Text, 18 29 repo_ver -> Text, ··· 431 442 432 443 diesel::allow_tables_to_appear_in_same_query!( 433 444 actors, 445 + author_feeds, 434 446 backfill, 435 447 backfill_jobs, 436 448 blocks,
+38 -20
parakeet/src/xrpc/app_bsky/feed/posts.rs
··· 19 19 BlockedAuthor, FeedReasonRepost, FeedSkeletonResponse, FeedViewPost, FeedViewPostReason, 20 20 PostView, SkeletonReason, ThreadViewPost, ThreadViewPostType, ThreadgateView, 21 21 }; 22 - use parakeet_db::schema; 22 + use parakeet_db::{models, schema}; 23 23 use reqwest::Url; 24 24 use serde::{Deserialize, Serialize}; 25 25 use std::collections::HashMap; ··· 217 217 218 218 let limit = query.limit.unwrap_or(50).clamp(1, 100); 219 219 220 - let mut posts_query = schema::posts::table 221 - .select((schema::posts::created_at, schema::posts::at_uri)) 222 - .filter(schema::posts::did.eq(did)) 220 + let mut posts_query = schema::author_feeds::table 221 + .select(models::AuthorFeedItem::as_select()) 222 + .left_join(schema::posts::table.on(schema::posts::at_uri.eq(schema::author_feeds::post))) 223 + .filter(schema::author_feeds::did.eq(&did)) 223 224 .into_boxed(); 224 225 225 226 if let Some(cursor) = datetime_cursor(query.cursor.as_ref()) { 226 - posts_query = posts_query.filter(schema::posts::created_at.lt(cursor)); 227 + posts_query = posts_query.filter(schema::author_feeds::sort_at.lt(cursor)); 227 228 } 228 229 229 230 posts_query = match query.filter { 230 - GetAuthorFeedFilter::PostsWithReplies => posts_query, 231 + GetAuthorFeedFilter::PostsWithReplies => { 232 + posts_query.filter(schema::author_feeds::typ.eq("post")) 233 + } 231 234 GetAuthorFeedFilter::PostsNoReplies => { 232 235 posts_query.filter(schema::posts::parent_uri.is_null()) 233 236 } 234 - GetAuthorFeedFilter::PostsWithMedia => posts_query.filter(embed_type_filter(&[ 235 - "app.bsky.embed.video", 236 - "app.bsky.embed.images", 237 - ])), 237 + GetAuthorFeedFilter::PostsWithMedia => posts_query.filter( 238 + embed_type_filter(&["app.bsky.embed.video", "app.bsky.embed.images"]) 239 + .and(schema::author_feeds::typ.eq("post")), 240 + ), 238 241 GetAuthorFeedFilter::PostsAndAuthorThreads => posts_query.filter( 239 242 (schema::posts::parent_uri 240 - .like(format!("at://{}/%", &query.actor)) 243 + .like(format!("at://{did}/%")) 241 244 .or(schema::posts::parent_uri.is_null())) 242 245 .and( 243 246 schema::posts::root_uri 244 - .like(format!("at://{}/%", &query.actor)) 247 + .like(format!("at://{did}/%")) 245 248 .or(schema::posts::root_uri.is_null()), 246 249 ), 247 250 ), 248 - GetAuthorFeedFilter::PostsWithVideo => { 249 - posts_query.filter(embed_type_filter(&["app.bsky.embed.video"])) 250 - } 251 + GetAuthorFeedFilter::PostsWithVideo => posts_query.filter( 252 + embed_type_filter(&["app.bsky.embed.video"]).and(schema::author_feeds::typ.eq("post")), 253 + ), 251 254 }; 252 255 253 256 let results = posts_query 254 - .order(schema::posts::created_at.desc()) 257 + .order(schema::author_feeds::sort_at.desc()) 255 258 .limit(limit as i64) 256 - .load::<(chrono::DateTime<chrono::Utc>, String)>(&mut conn) 259 + .load(&mut conn) 257 260 .await?; 258 261 259 262 let cursor = results 260 263 .last() 261 - .map(|(last, _)| last.timestamp_millis().to_string()); 264 + .map(|item| item.sort_at.timestamp_millis().to_string()); 262 265 263 266 let at_uris = results 264 267 .iter() 265 - .map(|(_, uri)| uri.clone()) 268 + .map(|item| item.post.clone()) 266 269 .collect::<Vec<_>>(); 270 + 271 + // get the actor for if we have reposted 272 + let profile = hyd.hydrate_profile_basic(did).await.ok_or(Error::server_error(None))?; 267 273 268 274 let mut posts = hyd.hydrate_feed_posts(at_uris).await; 269 275 270 276 let mut feed: Vec<_> = results 271 277 .into_iter() 272 - .filter_map(|(_, uri)| posts.remove(&uri)) 278 + .filter_map(|item| { 279 + posts.remove(&item.post).map(|mut fvp| { 280 + if item.typ == "repost" { 281 + fvp.reason = Some(FeedViewPostReason::Repost(FeedReasonRepost { 282 + by: profile.clone(), 283 + uri: Some(item.uri), 284 + cid: Some(item.cid), 285 + indexed_at: Default::default(), 286 + })) 287 + } 288 + fvp 289 + }) 290 + }) 273 291 .collect(); 274 292 275 293 if let Some(post) = pin {