From 9a73bc9cb9c3bb79df8ff15d968cee910b6735b8 Mon Sep 17 00:00:00 2001 From: Mia Date: Tue, 28 Apr 2026 20:00:36 +0100 Subject: [PATCH] feat: thread mutes --- crates/parakeet-db/src/models.rs | 8 +++ crates/parakeet-db/src/schema.rs | 10 ++++ crates/parakeet/src/db.rs | 4 +- crates/parakeet/src/hydration/posts.rs | 2 +- crates/parakeet/src/sql/post_state.sql | 6 +- .../parakeet/src/xrpc/app_bsky/graph/mutes.rs | 55 +++++++++++++++++-- crates/parakeet/src/xrpc/app_bsky/mod.rs | 4 +- fk.patch | 10 ++-- .../down.sql | 1 + .../up.sql | 10 ++++ 10 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 migrations/2026-04-28-191914-0000_thread-mutes/down.sql create mode 100644 migrations/2026-04-28-191914-0000_thread-mutes/up.sql diff --git a/crates/parakeet-db/src/models.rs b/crates/parakeet-db/src/models.rs index dd018ae..59bd615 100644 --- a/crates/parakeet-db/src/models.rs +++ b/crates/parakeet-db/src/models.rs @@ -471,6 +471,14 @@ pub struct NewDraft<'a> { pub threadgate_allowed_lists: Option>, } +#[derive(Debug, Insertable, AsChangeset)] +#[diesel(table_name = crate::schema::thread_mutes)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct NewThreadMute<'a> { + pub did: &'a str, + pub thread: &'a str, +} + pub use not_null_vec::TextArray; mod not_null_vec { use diesel::deserialize::FromSql; diff --git a/crates/parakeet-db/src/schema.rs b/crates/parakeet-db/src/schema.rs index eac7251..6602cf8 100644 --- a/crates/parakeet-db/src/schema.rs +++ b/crates/parakeet-db/src/schema.rs @@ -404,6 +404,14 @@ diesel::table! { } } +diesel::table! { + thread_mutes (did, thread) { + did -> Text, + thread -> Text, + created_at -> Timestamptz, + } +} + diesel::table! { threadgates (at_uri) { at_uri -> Text, @@ -457,6 +465,7 @@ diesel::joinable!(profiles -> actors (did)); diesel::joinable!(reposts -> actors (did)); diesel::joinable!(starterpacks -> actors (owner)); diesel::joinable!(statuses -> actors (did)); +diesel::joinable!(thread_mutes -> actors (did)); diesel::joinable!(threadgates -> posts (post_uri)); diesel::joinable!(verification -> actors (verifier)); @@ -494,6 +503,7 @@ diesel::allow_tables_to_appear_in_same_query!( reposts, starterpacks, statuses, + thread_mutes, threadgates, verification, ); diff --git a/crates/parakeet/src/db.rs b/crates/parakeet/src/db.rs index 8f9cf30..f153cd8 100644 --- a/crates/parakeet/src/db.rs +++ b/crates/parakeet/src/db.rs @@ -83,8 +83,8 @@ pub struct PostStateRet { pub repost_rkey: Option, #[diesel(sql_type = diesel::sql_types::Bool)] pub bookmarked: bool, - // #[diesel(sql_type = diesel::sql_types::Bool)] - // pub muted: bool, + #[diesel(sql_type = diesel::sql_types::Bool)] + pub thread_muted: bool, #[diesel(sql_type = diesel::sql_types::Bool)] pub embed_disabled: bool, #[diesel(sql_type = diesel::sql_types::Bool)] diff --git a/crates/parakeet/src/hydration/posts.rs b/crates/parakeet/src/hydration/posts.rs index 42b348a..3eb5696 100644 --- a/crates/parakeet/src/hydration/posts.rs +++ b/crates/parakeet/src/hydration/posts.rs @@ -28,7 +28,7 @@ fn build_viewer(did: &str, data: PostStateRet) -> PostViewerState { repost, like, bookmarked: data.bookmarked, - thread_muted: false, // todo when we have thread mutes + thread_muted: data.thread_muted, reply_disabled: false, embedding_disabled: data.embed_disabled && !is_me, // poster can always bypass embed disabled. pinned: data.pinned, diff --git a/crates/parakeet/src/sql/post_state.sql b/crates/parakeet/src/sql/post_state.sql index 967f947..77642c0 100644 --- a/crates/parakeet/src/sql/post_state.sql +++ b/crates/parakeet/src/sql/post_state.sql @@ -5,12 +5,14 @@ from (select p.at_uri, l.rkey as like_rkey, r.rkey as repost_rkey, b.did is not null as bookmarked, + tm.did is not null as thread_muted, coalesce(pg.rules && ARRAY ['app.bsky.feed.postgate#disableRule'], false) as embed_disabled from posts p left join likes l on l.subject = p.at_uri and l.did = $1 left join reposts r on r.post = p.at_uri and r.did = $1 left join bookmarks b on b.subject = p.at_uri and b.did = $1 left join postgates pg on pg.post_uri = p.at_uri + left join thread_mutes tm on tm.thread = coalesce(p.root_uri, p.at_uri) where p.at_uri = any ($2) - 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, - (select pinned_uri, pinned_cid from profiles where did = $1) pp; \ No newline at end of file + 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, + (select pinned_uri, pinned_cid from profiles where did = $1) pp; diff --git a/crates/parakeet/src/xrpc/app_bsky/graph/mutes.rs b/crates/parakeet/src/xrpc/app_bsky/graph/mutes.rs index f0ef748..7f0320a 100644 --- a/crates/parakeet/src/xrpc/app_bsky/graph/mutes.rs +++ b/crates/parakeet/src/xrpc/app_bsky/graph/mutes.rs @@ -62,11 +62,6 @@ pub struct MuteActorReq { pub actor: String, } -#[derive(Debug, Deserialize)] -pub struct MuteActorListReq { - pub list: String, -} - pub async fn mute_actor( State(state): State, auth: AtpAuth, @@ -88,6 +83,11 @@ pub async fn mute_actor( Ok(()) } +#[derive(Debug, Deserialize)] +pub struct MuteActorListReq { + pub list: String, +} + pub async fn mute_actor_list( State(state): State, auth: AtpAuth, @@ -109,6 +109,32 @@ pub async fn mute_actor_list( Ok(()) } +#[derive(Debug, Deserialize)] +pub struct MuteThreadReq { + pub root: String, +} + +pub async fn mute_thread( + State(state): State, + auth: AtpAuth, + Json(form): Json, +) -> XrpcResult<()> { + let mut conn = state.pool.get().await?; + + let data = models::NewThreadMute { + did: &auth.0, + thread: &form.root, + }; + + diesel::insert_into(schema::thread_mutes::table) + .values(&data) + .on_conflict_do_nothing() + .execute(&mut conn) + .await?; + + Ok(()) +} + pub async fn unmute_actor( State(state): State, auth: AtpAuth, @@ -146,3 +172,22 @@ pub async fn unmute_actor_list( Ok(()) } + +pub async fn unmute_thread( + State(state): State, + auth: AtpAuth, + Json(form): Json, +) -> XrpcResult<()> { + let mut conn = state.pool.get().await?; + + diesel::delete(schema::thread_mutes::table) + .filter( + schema::thread_mutes::did + .eq(&auth.0) + .and(schema::thread_mutes::thread.eq(&form.root)), + ) + .execute(&mut conn) + .await?; + + Ok(()) +} diff --git a/crates/parakeet/src/xrpc/app_bsky/mod.rs b/crates/parakeet/src/xrpc/app_bsky/mod.rs index f3bc4c3..87f1d6d 100644 --- a/crates/parakeet/src/xrpc/app_bsky/mod.rs +++ b/crates/parakeet/src/xrpc/app_bsky/mod.rs @@ -57,11 +57,11 @@ pub fn routes() -> Router { // TODO: app.bsky.graph.getSuggestedFollows (recs) .route("/app.bsky.graph.muteActor", post(graph::mutes::mute_actor)) .route("/app.bsky.graph.muteActorList", post(graph::mutes::mute_actor_list)) - // TODO: app.bsky.graph.muteThread (notifs) + .route("/app.bsky.graph.muteThread", post(graph::mutes::mute_thread)) // TODO: app.bsky.graph.searchStarterPacks (search) .route("/app.bsky.graph.unmuteActor", post(graph::mutes::unmute_actor)) .route("/app.bsky.graph.unmuteActorList", post(graph::mutes::unmute_actor_list)) - // TODO: app.bsky.graph.unmuteThread (notifs) + .route("/app.bsky.graph.unmuteThread", post(graph::mutes::unmute_thread)) .route("/app.bsky.labeler.getServices", get(labeler::get_services)) // TODO: app.bsky.notification.getPreferences // TODO: app.bsky.notification.getUnreadCount diff --git a/fk.patch b/fk.patch index 6ab63ef..49ed200 100644 --- a/fk.patch +++ b/fk.patch @@ -1,8 +1,7 @@ -diff --git a/parakeet-db/src/schema.rs b/parakeet-db/src/schema.rs -index 59f65d9..a4219d5 100644 ---- a/parakeet-db/src/schema.rs -+++ b/parakeet-db/src/schema.rs -@@ -364,11 +364,13 @@ diesel::joinable!(post_embed_images -> posts (post_uri)); +diff --git a/crates/parakeet-db/src/schema.rs b/crates/parakeet-db/src/schema.rs +--- a/crates/parakeet-db/src/schema.rs ++++ b/crates/parakeet-db/src/schema.rs +@@ -459,12 +459,14 @@ diesel::joinable!(post_embed_record -> posts (post_uri)); diesel::joinable!(post_embed_video -> posts (post_uri)); diesel::joinable!(post_embed_video_captions -> posts (post_uri)); @@ -12,6 +11,7 @@ index 59f65d9..a4219d5 100644 diesel::joinable!(reposts -> actors (did)); diesel::joinable!(starterpacks -> actors (owner)); diesel::joinable!(statuses -> actors (did)); + diesel::joinable!(thread_mutes -> actors (did)); +diesel::joinable!(threadgates -> posts (post_uri)); diesel::joinable!(verification -> actors (verifier)); diff --git a/migrations/2026-04-28-191914-0000_thread-mutes/down.sql b/migrations/2026-04-28-191914-0000_thread-mutes/down.sql new file mode 100644 index 0000000..457ee68 --- /dev/null +++ b/migrations/2026-04-28-191914-0000_thread-mutes/down.sql @@ -0,0 +1 @@ +drop table thread_mutes; diff --git a/migrations/2026-04-28-191914-0000_thread-mutes/up.sql b/migrations/2026-04-28-191914-0000_thread-mutes/up.sql new file mode 100644 index 0000000..8833e53 --- /dev/null +++ b/migrations/2026-04-28-191914-0000_thread-mutes/up.sql @@ -0,0 +1,10 @@ +create table thread_mutes +( + did text not null references actors (did), + thread text not null, + created_at timestamptz not null default now(), + + primary key (did, thread) +); + +create index threadmutes_list_index on thread_mutes (thread); -- 2.51.2