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

Configure Feed

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

feat: thread mutes

Mia 9a73bc9c 447ed52b

+93 -17
+8
crates/parakeet-db/src/models.rs
··· 471 471 pub threadgate_allowed_lists: Option<Vec<String>>, 472 472 } 473 473 474 + #[derive(Debug, Insertable, AsChangeset)] 475 + #[diesel(table_name = crate::schema::thread_mutes)] 476 + #[diesel(check_for_backend(diesel::pg::Pg))] 477 + pub struct NewThreadMute<'a> { 478 + pub did: &'a str, 479 + pub thread: &'a str, 480 + } 481 + 474 482 pub use not_null_vec::TextArray; 475 483 mod not_null_vec { 476 484 use diesel::deserialize::FromSql;
+10
crates/parakeet-db/src/schema.rs
··· 405 405 } 406 406 407 407 diesel::table! { 408 + thread_mutes (did, thread) { 409 + did -> Text, 410 + thread -> Text, 411 + created_at -> Timestamptz, 412 + } 413 + } 414 + 415 + diesel::table! { 408 416 threadgates (at_uri) { 409 417 at_uri -> Text, 410 418 cid -> Text, ··· 457 465 diesel::joinable!(reposts -> actors (did)); 458 466 diesel::joinable!(starterpacks -> actors (owner)); 459 467 diesel::joinable!(statuses -> actors (did)); 468 + diesel::joinable!(thread_mutes -> actors (did)); 460 469 diesel::joinable!(threadgates -> posts (post_uri)); 461 470 diesel::joinable!(verification -> actors (verifier)); 462 471 ··· 494 503 reposts, 495 504 starterpacks, 496 505 statuses, 506 + thread_mutes, 497 507 threadgates, 498 508 verification, 499 509 );
+2 -2
crates/parakeet/src/db.rs
··· 83 83 pub repost_rkey: Option<String>, 84 84 #[diesel(sql_type = diesel::sql_types::Bool)] 85 85 pub bookmarked: bool, 86 - // #[diesel(sql_type = diesel::sql_types::Bool)] 87 - // pub muted: bool, 86 + #[diesel(sql_type = diesel::sql_types::Bool)] 87 + pub thread_muted: bool, 88 88 #[diesel(sql_type = diesel::sql_types::Bool)] 89 89 pub embed_disabled: bool, 90 90 #[diesel(sql_type = diesel::sql_types::Bool)]
+1 -1
crates/parakeet/src/hydration/posts.rs
··· 28 28 repost, 29 29 like, 30 30 bookmarked: data.bookmarked, 31 - thread_muted: false, // todo when we have thread mutes 31 + thread_muted: data.thread_muted, 32 32 reply_disabled: false, 33 33 embedding_disabled: data.embed_disabled && !is_me, // poster can always bypass embed disabled. 34 34 pinned: data.pinned,
+4 -2
crates/parakeet/src/sql/post_state.sql
··· 5 5 l.rkey as like_rkey, 6 6 r.rkey as repost_rkey, 7 7 b.did is not null as bookmarked, 8 + tm.did is not null as thread_muted, 8 9 coalesce(pg.rules && ARRAY ['app.bsky.feed.postgate#disableRule'], false) as embed_disabled 9 10 from posts p 10 11 left join likes l on l.subject = p.at_uri and l.did = $1 11 12 left join reposts r on r.post = p.at_uri and r.did = $1 12 13 left join bookmarks b on b.subject = p.at_uri and b.did = $1 13 14 left join postgates pg on pg.post_uri = p.at_uri 15 + left join thread_mutes tm on tm.thread = coalesce(p.root_uri, p.at_uri) 14 16 where p.at_uri = any ($2) 15 - and (l.rkey is not null or r.rkey is not null or b.did is not null or pg.rules is not null)) bq, 16 - (select pinned_uri, pinned_cid from profiles where did = $1) pp; 17 + and (l.rkey is not null or r.rkey is not null or b.did is not null or tm.did is not null or pg.rules is not null)) bq, 18 + (select pinned_uri, pinned_cid from profiles where did = $1) pp;
+50 -5
crates/parakeet/src/xrpc/app_bsky/graph/mutes.rs
··· 62 62 pub actor: String, 63 63 } 64 64 65 - #[derive(Debug, Deserialize)] 66 - pub struct MuteActorListReq { 67 - pub list: String, 68 - } 69 - 70 65 pub async fn mute_actor( 71 66 State(state): State<GlobalState>, 72 67 auth: AtpAuth, ··· 88 83 Ok(()) 89 84 } 90 85 86 + #[derive(Debug, Deserialize)] 87 + pub struct MuteActorListReq { 88 + pub list: String, 89 + } 90 + 91 91 pub async fn mute_actor_list( 92 92 State(state): State<GlobalState>, 93 93 auth: AtpAuth, ··· 109 109 Ok(()) 110 110 } 111 111 112 + #[derive(Debug, Deserialize)] 113 + pub struct MuteThreadReq { 114 + pub root: String, 115 + } 116 + 117 + pub async fn mute_thread( 118 + State(state): State<GlobalState>, 119 + auth: AtpAuth, 120 + Json(form): Json<MuteThreadReq>, 121 + ) -> XrpcResult<()> { 122 + let mut conn = state.pool.get().await?; 123 + 124 + let data = models::NewThreadMute { 125 + did: &auth.0, 126 + thread: &form.root, 127 + }; 128 + 129 + diesel::insert_into(schema::thread_mutes::table) 130 + .values(&data) 131 + .on_conflict_do_nothing() 132 + .execute(&mut conn) 133 + .await?; 134 + 135 + Ok(()) 136 + } 137 + 112 138 pub async fn unmute_actor( 113 139 State(state): State<GlobalState>, 114 140 auth: AtpAuth, ··· 146 172 147 173 Ok(()) 148 174 } 175 + 176 + pub async fn unmute_thread( 177 + State(state): State<GlobalState>, 178 + auth: AtpAuth, 179 + Json(form): Json<MuteThreadReq>, 180 + ) -> XrpcResult<()> { 181 + let mut conn = state.pool.get().await?; 182 + 183 + diesel::delete(schema::thread_mutes::table) 184 + .filter( 185 + schema::thread_mutes::did 186 + .eq(&auth.0) 187 + .and(schema::thread_mutes::thread.eq(&form.root)), 188 + ) 189 + .execute(&mut conn) 190 + .await?; 191 + 192 + Ok(()) 193 + }
+2 -2
crates/parakeet/src/xrpc/app_bsky/mod.rs
··· 57 57 // TODO: app.bsky.graph.getSuggestedFollows (recs) 58 58 .route("/app.bsky.graph.muteActor", post(graph::mutes::mute_actor)) 59 59 .route("/app.bsky.graph.muteActorList", post(graph::mutes::mute_actor_list)) 60 - // TODO: app.bsky.graph.muteThread (notifs) 60 + .route("/app.bsky.graph.muteThread", post(graph::mutes::mute_thread)) 61 61 // TODO: app.bsky.graph.searchStarterPacks (search) 62 62 .route("/app.bsky.graph.unmuteActor", post(graph::mutes::unmute_actor)) 63 63 .route("/app.bsky.graph.unmuteActorList", post(graph::mutes::unmute_actor_list)) 64 - // TODO: app.bsky.graph.unmuteThread (notifs) 64 + .route("/app.bsky.graph.unmuteThread", post(graph::mutes::unmute_thread)) 65 65 .route("/app.bsky.labeler.getServices", get(labeler::get_services)) 66 66 // TODO: app.bsky.notification.getPreferences 67 67 // TODO: app.bsky.notification.getUnreadCount
+5 -5
fk.patch
··· 1 - diff --git a/parakeet-db/src/schema.rs b/parakeet-db/src/schema.rs 2 - index 59f65d9..a4219d5 100644 3 - --- a/parakeet-db/src/schema.rs 4 - +++ b/parakeet-db/src/schema.rs 5 - @@ -364,11 +364,13 @@ diesel::joinable!(post_embed_images -> posts (post_uri)); 1 + diff --git a/crates/parakeet-db/src/schema.rs b/crates/parakeet-db/src/schema.rs 2 + --- a/crates/parakeet-db/src/schema.rs 3 + +++ b/crates/parakeet-db/src/schema.rs 4 + @@ -459,12 +459,14 @@ 6 5 diesel::joinable!(post_embed_record -> posts (post_uri)); 7 6 diesel::joinable!(post_embed_video -> posts (post_uri)); 8 7 diesel::joinable!(post_embed_video_captions -> posts (post_uri)); ··· 12 11 diesel::joinable!(reposts -> actors (did)); 13 12 diesel::joinable!(starterpacks -> actors (owner)); 14 13 diesel::joinable!(statuses -> actors (did)); 14 + diesel::joinable!(thread_mutes -> actors (did)); 15 15 +diesel::joinable!(threadgates -> posts (post_uri)); 16 16 diesel::joinable!(verification -> actors (verifier)); 17 17
+1
migrations/2026-04-28-191914-0000_thread-mutes/down.sql
··· 1 + drop table thread_mutes;
+10
migrations/2026-04-28-191914-0000_thread-mutes/up.sql
··· 1 + create table thread_mutes 2 + ( 3 + did text not null references actors (did), 4 + thread text not null, 5 + created_at timestamptz not null default now(), 6 + 7 + primary key (did, thread) 8 + ); 9 + 10 + create index threadmutes_list_index on thread_mutes (thread);