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
69
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 {