From 7cdac0f098cad26f8428ac2bccb9fdae9fb19e79 Mon Sep 17 00:00:00 2001 From: Mia Date: Sun, 3 Aug 2025 13:49:10 +0000 Subject: [PATCH] feat: mutes --- migrations/2025-08-03-125504_mutes/down.sql | 2 + migrations/2025-08-03-125504_mutes/up.sql | 22 +++ parakeet-db/src/models.rs | 16 +++ parakeet-db/src/schema.rs | 20 +++ parakeet/src/xrpc/app_bsky/graph/lists.rs | 48 ++++++- parakeet/src/xrpc/app_bsky/graph/mod.rs | 1 + parakeet/src/xrpc/app_bsky/graph/mutes.rs | 143 ++++++++++++++++++++ parakeet/src/xrpc/app_bsky/mod.rs | 8 +- parakeet/src/xrpc/mod.rs | 6 + 9 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 migrations/2025-08-03-125504_mutes/down.sql create mode 100644 migrations/2025-08-03-125504_mutes/up.sql create mode 100644 parakeet/src/xrpc/app_bsky/graph/mutes.rs diff --git a/migrations/2025-08-03-125504_mutes/down.sql b/migrations/2025-08-03-125504_mutes/down.sql new file mode 100644 index 00000000..059a77a1 --- /dev/null +++ b/migrations/2025-08-03-125504_mutes/down.sql @@ -0,0 +1,2 @@ +drop table list_mutes; +drop table mutes; \ No newline at end of file diff --git a/migrations/2025-08-03-125504_mutes/up.sql b/migrations/2025-08-03-125504_mutes/up.sql new file mode 100644 index 00000000..de0f6bfa --- /dev/null +++ b/migrations/2025-08-03-125504_mutes/up.sql @@ -0,0 +1,22 @@ +create table list_mutes +( + did text not null references actors (did), + list_uri text not null, + created_at timestamptz not null default now(), + + primary key (did, list_uri) +); + +create index listmutes_list_index on list_mutes using hash (list_uri); +create index listmutes_did_index on list_mutes using hash (did); + +create table mutes +( + did text not null references actors (did), + subject text not null, + created_at timestamptz not null default now(), + + primary key (did, subject) +); + +create index mutes_subject_index on mutes (subject); diff --git a/parakeet-db/src/models.rs b/parakeet-db/src/models.rs index 1689e5bd..1cbb2a9a 100644 --- a/parakeet-db/src/models.rs +++ b/parakeet-db/src/models.rs @@ -367,3 +367,19 @@ pub struct Status { pub created_at: DateTime, pub indexed_at: NaiveDateTime, } + +#[derive(Debug, Insertable, AsChangeset)] +#[diesel(table_name = crate::schema::mutes)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct NewMute<'a> { + pub did: &'a str, + pub subject: &'a str, +} + +#[derive(Debug, Insertable, AsChangeset)] +#[diesel(table_name = crate::schema::list_mutes)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct NewListMute<'a> { + pub did: &'a str, + pub list_uri: &'a str, +} diff --git a/parakeet-db/src/schema.rs b/parakeet-db/src/schema.rs index a95d91b8..7153a336 100644 --- a/parakeet-db/src/schema.rs +++ b/parakeet-db/src/schema.rs @@ -150,6 +150,14 @@ diesel::table! { } } +diesel::table! { + list_mutes (did, list_uri) { + did -> Text, + list_uri -> Text, + created_at -> Timestamptz, + } +} + diesel::table! { lists (at_uri) { at_uri -> Text, @@ -165,6 +173,14 @@ diesel::table! { } } +diesel::table! { + mutes (did, subject) { + did -> Text, + subject -> Text, + created_at -> Timestamptz, + } +} + diesel::table! { notif_decl (did) { did -> Text, @@ -366,7 +382,9 @@ diesel::joinable!(labeler_defs -> labelers (labeler)); diesel::joinable!(labelers -> actors (did)); diesel::joinable!(likes -> actors (did)); diesel::joinable!(list_blocks -> actors (did)); +diesel::joinable!(list_mutes -> actors (did)); diesel::joinable!(lists -> actors (owner)); +diesel::joinable!(mutes -> actors (did)); diesel::joinable!(notif_decl -> actors (did)); diesel::joinable!(post_embed_ext -> posts (post_uri)); diesel::joinable!(post_embed_images -> posts (post_uri)); @@ -396,7 +414,9 @@ diesel::allow_tables_to_appear_in_same_query!( likes, list_blocks, list_items, + list_mutes, lists, + mutes, notif_decl, post_embed_ext, post_embed_images, diff --git a/parakeet/src/xrpc/app_bsky/graph/lists.rs b/parakeet/src/xrpc/app_bsky/graph/lists.rs index 137c955f..c1904c5e 100644 --- a/parakeet/src/xrpc/app_bsky/graph/lists.rs +++ b/parakeet/src/xrpc/app_bsky/graph/lists.rs @@ -1,7 +1,9 @@ use crate::hydration::StatefulHydrator; use crate::xrpc::error::{Error, XrpcResult}; use crate::xrpc::extract::{AtpAcceptLabelers, AtpAuth}; -use crate::xrpc::{check_actor_status, datetime_cursor, get_actor_did, ActorWithCursorQuery}; +use crate::xrpc::{ + check_actor_status, datetime_cursor, get_actor_did, ActorWithCursorQuery, CursorQuery, +}; use crate::GlobalState; use axum::extract::{Query, State}; use axum::Json; @@ -135,3 +137,47 @@ pub async fn get_list( items, })) } + +#[derive(Debug, Serialize)] +pub struct GetListMutesRes { + #[serde(skip_serializing_if = "Option::is_none")] + cursor: Option, + lists: Vec, +} + +pub async fn get_list_mutes( + State(state): State, + AtpAcceptLabelers(labelers): AtpAcceptLabelers, + auth: AtpAuth, + Query(query): Query, +) -> XrpcResult> { + let mut conn = state.pool.get().await?; + let did = auth.0.clone(); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, Some(auth)); + + let limit = query.limit.unwrap_or(50).clamp(1, 100); + + let mut mutes_query = schema::list_mutes::table + .select(schema::list_mutes::list_uri) + .filter(schema::list_mutes::did.eq(did)) + .into_boxed(); + + if let Some(cursor) = query.cursor { + mutes_query = mutes_query.filter(schema::list_mutes::list_uri.lt(cursor)); + } + + let mutes = mutes_query + .order(schema::list_mutes::list_uri.desc()) + .limit(limit as i64) + .load(&mut conn) + .await?; + + let lists = hyd.hydrate_lists(mutes).await; + let mutes = lists.into_values().collect::>(); + let cursor = mutes.last().map(|v| v.uri.clone()); + + Ok(Json(GetListMutesRes { + cursor, + lists: mutes, + })) +} diff --git a/parakeet/src/xrpc/app_bsky/graph/mod.rs b/parakeet/src/xrpc/app_bsky/graph/mod.rs index 3b553cc6..b5afa3c3 100644 --- a/parakeet/src/xrpc/app_bsky/graph/mod.rs +++ b/parakeet/src/xrpc/app_bsky/graph/mod.rs @@ -1,3 +1,4 @@ pub mod lists; +pub mod mutes; pub mod relations; pub mod starter_packs; diff --git a/parakeet/src/xrpc/app_bsky/graph/mutes.rs b/parakeet/src/xrpc/app_bsky/graph/mutes.rs new file mode 100644 index 00000000..693c2c2c --- /dev/null +++ b/parakeet/src/xrpc/app_bsky/graph/mutes.rs @@ -0,0 +1,143 @@ +use crate::hydration::StatefulHydrator; +use crate::xrpc::error::XrpcResult; +use crate::xrpc::extract::{AtpAcceptLabelers, AtpAuth}; +use crate::xrpc::CursorQuery; +use crate::GlobalState; +use axum::extract::{Query, State}; +use axum::Json; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use lexica::app_bsky::actor::ProfileView; +use parakeet_db::{models, schema}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +pub struct GetMutesRes { + #[serde(skip_serializing_if = "Option::is_none")] + cursor: Option, + mutes: Vec, +} + +pub async fn get_mutes( + State(state): State, + AtpAcceptLabelers(labelers): AtpAcceptLabelers, + auth: AtpAuth, + Query(query): Query, +) -> XrpcResult> { + let mut conn = state.pool.get().await?; + let did = auth.0.clone(); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, Some(auth)); + + let limit = query.limit.unwrap_or(50).clamp(1, 100); + + let mut muted_query = schema::mutes::table + .select(schema::mutes::subject) + .filter(schema::mutes::did.eq(did)) + .into_boxed(); + + if let Some(cursor) = query.cursor { + muted_query = muted_query.filter(schema::mutes::subject.lt(cursor)); + } + + let muted = muted_query + .order(schema::mutes::subject.desc()) + .limit(limit as i64) + .load(&mut conn) + .await?; + + let profiles = hyd.hydrate_profiles(muted).await; + let mutes = profiles.into_values().collect::>(); + let cursor = mutes.last().map(|v| v.did.clone()); + + Ok(Json(GetMutesRes { cursor, mutes })) +} + +#[derive(Debug, Deserialize)] +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, + Json(form): Json, +) -> XrpcResult<()> { + let mut conn = state.pool.get().await?; + + let data = models::NewMute { + did: &auth.0, + subject: &form.actor, + }; + + diesel::insert_into(schema::mutes::table) + .values(&data) + .on_conflict_do_nothing() + .execute(&mut conn) + .await?; + + Ok(()) +} + +pub async fn mute_actor_list( + State(state): State, + auth: AtpAuth, + Json(form): Json, +) -> XrpcResult<()> { + let mut conn = state.pool.get().await?; + + let data = models::NewListMute { + did: &auth.0, + list_uri: &form.list, + }; + + diesel::insert_into(schema::list_mutes::table) + .values(&data) + .on_conflict_do_nothing() + .execute(&mut conn) + .await?; + + Ok(()) +} + +pub async fn unmute_actor( + State(state): State, + auth: AtpAuth, + Json(form): Json, +) -> XrpcResult<()> { + let mut conn = state.pool.get().await?; + + diesel::delete(schema::mutes::table) + .filter( + schema::mutes::did + .eq(&auth.0) + .and(schema::mutes::subject.eq(&form.actor)), + ) + .execute(&mut conn) + .await?; + + Ok(()) +} + +pub async fn unmute_actor_list( + State(state): State, + auth: AtpAuth, + Json(form): Json, +) -> XrpcResult<()> { + let mut conn = state.pool.get().await?; + + diesel::delete(schema::list_mutes::table) + .filter( + schema::list_mutes::did + .eq(&auth.0) + .and(schema::list_mutes::list_uri.eq(&form.list)), + ) + .execute(&mut conn) + .await?; + + Ok(()) +} diff --git a/parakeet/src/xrpc/app_bsky/mod.rs b/parakeet/src/xrpc/app_bsky/mod.rs index 101810e6..dadcb0e6 100644 --- a/parakeet/src/xrpc/app_bsky/mod.rs +++ b/parakeet/src/xrpc/app_bsky/mod.rs @@ -1,4 +1,4 @@ -use axum::routing::get; +use axum::routing::{get, post}; use axum::Router; mod actor; @@ -27,8 +27,14 @@ pub fn routes() -> Router { .route("/app.bsky.graph.getFollowers", get(graph::relations::get_followers)) .route("/app.bsky.graph.getFollows", get(graph::relations::get_follows)) .route("/app.bsky.graph.getList", get(graph::lists::get_list)) + .route("/app.bsky.graph.getListMutes", get(graph::lists::get_list_mutes)) .route("/app.bsky.graph.getLists", get(graph::lists::get_lists)) + .route("/app.bsky.graph.getMutes", get(graph::mutes::get_mutes)) .route("/app.bsky.graph.getStarterPack", get(graph::starter_packs::get_starter_pack)) .route("/app.bsky.graph.getStarterPacks", get(graph::starter_packs::get_starter_packs)) + .route("/app.bsky.graph.muteActor", post(graph::mutes::mute_actor)) + .route("/app.bsky.graph.muteActorList", post(graph::mutes::mute_actor_list)) + .route("/app.bsky.graph.unmuteActor", post(graph::mutes::unmute_actor)) + .route("/app.bsky.graph.unmuteActorList", post(graph::mutes::unmute_actor_list)) .route("/app.bsky.labeler.getServices", get(labeler::get_services)) } diff --git a/parakeet/src/xrpc/mod.rs b/parakeet/src/xrpc/mod.rs index 8af323a0..70af1cc4 100644 --- a/parakeet/src/xrpc/mod.rs +++ b/parakeet/src/xrpc/mod.rs @@ -94,6 +94,12 @@ async fn check_actor_status( } } +#[derive(Debug, Deserialize)] +pub struct CursorQuery { + pub limit: Option, + pub cursor: Option, +} + #[derive(Debug, Deserialize)] pub struct ActorWithCursorQuery { pub actor: String, -- 2.51.2