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

Configure Feed

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

Merge branch 'feat-mutes' into 'main'

Mutes

See merge request parakeet-social/parakeet!20

Mia 4766bf0f 9c38127c

+264 -2
+2
migrations/2025-08-03-125504_mutes/down.sql
··· 1 + drop table list_mutes; 2 + drop table mutes;
+22
migrations/2025-08-03-125504_mutes/up.sql
··· 1 + create table list_mutes 2 + ( 3 + did text not null references actors (did), 4 + list_uri text not null, 5 + created_at timestamptz not null default now(), 6 + 7 + primary key (did, list_uri) 8 + ); 9 + 10 + create index listmutes_list_index on list_mutes using hash (list_uri); 11 + create index listmutes_did_index on list_mutes using hash (did); 12 + 13 + create table mutes 14 + ( 15 + did text not null references actors (did), 16 + subject text not null, 17 + created_at timestamptz not null default now(), 18 + 19 + primary key (did, subject) 20 + ); 21 + 22 + create index mutes_subject_index on mutes (subject);
+16
parakeet-db/src/models.rs
··· 367 367 pub created_at: DateTime<Utc>, 368 368 pub indexed_at: NaiveDateTime, 369 369 } 370 + 371 + #[derive(Debug, Insertable, AsChangeset)] 372 + #[diesel(table_name = crate::schema::mutes)] 373 + #[diesel(check_for_backend(diesel::pg::Pg))] 374 + pub struct NewMute<'a> { 375 + pub did: &'a str, 376 + pub subject: &'a str, 377 + } 378 + 379 + #[derive(Debug, Insertable, AsChangeset)] 380 + #[diesel(table_name = crate::schema::list_mutes)] 381 + #[diesel(check_for_backend(diesel::pg::Pg))] 382 + pub struct NewListMute<'a> { 383 + pub did: &'a str, 384 + pub list_uri: &'a str, 385 + }
+20
parakeet-db/src/schema.rs
··· 151 151 } 152 152 153 153 diesel::table! { 154 + list_mutes (did, list_uri) { 155 + did -> Text, 156 + list_uri -> Text, 157 + created_at -> Timestamptz, 158 + } 159 + } 160 + 161 + diesel::table! { 154 162 lists (at_uri) { 155 163 at_uri -> Text, 156 164 owner -> Text, ··· 162 170 avatar_cid -> Nullable<Text>, 163 171 created_at -> Timestamptz, 164 172 indexed_at -> Timestamp, 173 + } 174 + } 175 + 176 + diesel::table! { 177 + mutes (did, subject) { 178 + did -> Text, 179 + subject -> Text, 180 + created_at -> Timestamptz, 165 181 } 166 182 } 167 183 ··· 366 382 diesel::joinable!(labelers -> actors (did)); 367 383 diesel::joinable!(likes -> actors (did)); 368 384 diesel::joinable!(list_blocks -> actors (did)); 385 + diesel::joinable!(list_mutes -> actors (did)); 369 386 diesel::joinable!(lists -> actors (owner)); 387 + diesel::joinable!(mutes -> actors (did)); 370 388 diesel::joinable!(notif_decl -> actors (did)); 371 389 diesel::joinable!(post_embed_ext -> posts (post_uri)); 372 390 diesel::joinable!(post_embed_images -> posts (post_uri)); ··· 396 414 likes, 397 415 list_blocks, 398 416 list_items, 417 + list_mutes, 399 418 lists, 419 + mutes, 400 420 notif_decl, 401 421 post_embed_ext, 402 422 post_embed_images,
+47 -1
parakeet/src/xrpc/app_bsky/graph/lists.rs
··· 1 1 use crate::hydration::StatefulHydrator; 2 2 use crate::xrpc::error::{Error, XrpcResult}; 3 3 use crate::xrpc::extract::{AtpAcceptLabelers, AtpAuth}; 4 - use crate::xrpc::{check_actor_status, datetime_cursor, get_actor_did, ActorWithCursorQuery}; 4 + use crate::xrpc::{ 5 + check_actor_status, datetime_cursor, get_actor_did, ActorWithCursorQuery, CursorQuery, 6 + }; 5 7 use crate::GlobalState; 6 8 use axum::extract::{Query, State}; 7 9 use axum::Json; ··· 135 137 items, 136 138 })) 137 139 } 140 + 141 + #[derive(Debug, Serialize)] 142 + pub struct GetListMutesRes { 143 + #[serde(skip_serializing_if = "Option::is_none")] 144 + cursor: Option<String>, 145 + lists: Vec<ListView>, 146 + } 147 + 148 + pub async fn get_list_mutes( 149 + State(state): State<GlobalState>, 150 + AtpAcceptLabelers(labelers): AtpAcceptLabelers, 151 + auth: AtpAuth, 152 + Query(query): Query<CursorQuery>, 153 + ) -> XrpcResult<Json<GetListMutesRes>> { 154 + let mut conn = state.pool.get().await?; 155 + let did = auth.0.clone(); 156 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, Some(auth)); 157 + 158 + let limit = query.limit.unwrap_or(50).clamp(1, 100); 159 + 160 + let mut mutes_query = schema::list_mutes::table 161 + .select(schema::list_mutes::list_uri) 162 + .filter(schema::list_mutes::did.eq(did)) 163 + .into_boxed(); 164 + 165 + if let Some(cursor) = query.cursor { 166 + mutes_query = mutes_query.filter(schema::list_mutes::list_uri.lt(cursor)); 167 + } 168 + 169 + let mutes = mutes_query 170 + .order(schema::list_mutes::list_uri.desc()) 171 + .limit(limit as i64) 172 + .load(&mut conn) 173 + .await?; 174 + 175 + let lists = hyd.hydrate_lists(mutes).await; 176 + let mutes = lists.into_values().collect::<Vec<_>>(); 177 + let cursor = mutes.last().map(|v| v.uri.clone()); 178 + 179 + Ok(Json(GetListMutesRes { 180 + cursor, 181 + lists: mutes, 182 + })) 183 + }
+1
parakeet/src/xrpc/app_bsky/graph/mod.rs
··· 1 1 pub mod lists; 2 + pub mod mutes; 2 3 pub mod relations; 3 4 pub mod starter_packs;
+143
parakeet/src/xrpc/app_bsky/graph/mutes.rs
··· 1 + use crate::hydration::StatefulHydrator; 2 + use crate::xrpc::error::XrpcResult; 3 + use crate::xrpc::extract::{AtpAcceptLabelers, AtpAuth}; 4 + use crate::xrpc::CursorQuery; 5 + use crate::GlobalState; 6 + use axum::extract::{Query, State}; 7 + use axum::Json; 8 + use diesel::prelude::*; 9 + use diesel_async::RunQueryDsl; 10 + use lexica::app_bsky::actor::ProfileView; 11 + use parakeet_db::{models, schema}; 12 + use serde::{Deserialize, Serialize}; 13 + 14 + #[derive(Debug, Serialize)] 15 + pub struct GetMutesRes { 16 + #[serde(skip_serializing_if = "Option::is_none")] 17 + cursor: Option<String>, 18 + mutes: Vec<ProfileView>, 19 + } 20 + 21 + pub async fn get_mutes( 22 + State(state): State<GlobalState>, 23 + AtpAcceptLabelers(labelers): AtpAcceptLabelers, 24 + auth: AtpAuth, 25 + Query(query): Query<CursorQuery>, 26 + ) -> XrpcResult<Json<GetMutesRes>> { 27 + let mut conn = state.pool.get().await?; 28 + let did = auth.0.clone(); 29 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, Some(auth)); 30 + 31 + let limit = query.limit.unwrap_or(50).clamp(1, 100); 32 + 33 + let mut muted_query = schema::mutes::table 34 + .select(schema::mutes::subject) 35 + .filter(schema::mutes::did.eq(did)) 36 + .into_boxed(); 37 + 38 + if let Some(cursor) = query.cursor { 39 + muted_query = muted_query.filter(schema::mutes::subject.lt(cursor)); 40 + } 41 + 42 + let muted = muted_query 43 + .order(schema::mutes::subject.desc()) 44 + .limit(limit as i64) 45 + .load(&mut conn) 46 + .await?; 47 + 48 + let profiles = hyd.hydrate_profiles(muted).await; 49 + let mutes = profiles.into_values().collect::<Vec<_>>(); 50 + let cursor = mutes.last().map(|v| v.did.clone()); 51 + 52 + Ok(Json(GetMutesRes { cursor, mutes })) 53 + } 54 + 55 + #[derive(Debug, Deserialize)] 56 + pub struct MuteActorReq { 57 + pub actor: String, 58 + } 59 + 60 + #[derive(Debug, Deserialize)] 61 + pub struct MuteActorListReq { 62 + pub list: String, 63 + } 64 + 65 + pub async fn mute_actor( 66 + State(state): State<GlobalState>, 67 + auth: AtpAuth, 68 + Json(form): Json<MuteActorReq>, 69 + ) -> XrpcResult<()> { 70 + let mut conn = state.pool.get().await?; 71 + 72 + let data = models::NewMute { 73 + did: &auth.0, 74 + subject: &form.actor, 75 + }; 76 + 77 + diesel::insert_into(schema::mutes::table) 78 + .values(&data) 79 + .on_conflict_do_nothing() 80 + .execute(&mut conn) 81 + .await?; 82 + 83 + Ok(()) 84 + } 85 + 86 + pub async fn mute_actor_list( 87 + State(state): State<GlobalState>, 88 + auth: AtpAuth, 89 + Json(form): Json<MuteActorListReq>, 90 + ) -> XrpcResult<()> { 91 + let mut conn = state.pool.get().await?; 92 + 93 + let data = models::NewListMute { 94 + did: &auth.0, 95 + list_uri: &form.list, 96 + }; 97 + 98 + diesel::insert_into(schema::list_mutes::table) 99 + .values(&data) 100 + .on_conflict_do_nothing() 101 + .execute(&mut conn) 102 + .await?; 103 + 104 + Ok(()) 105 + } 106 + 107 + pub async fn unmute_actor( 108 + State(state): State<GlobalState>, 109 + auth: AtpAuth, 110 + Json(form): Json<MuteActorReq>, 111 + ) -> XrpcResult<()> { 112 + let mut conn = state.pool.get().await?; 113 + 114 + diesel::delete(schema::mutes::table) 115 + .filter( 116 + schema::mutes::did 117 + .eq(&auth.0) 118 + .and(schema::mutes::subject.eq(&form.actor)), 119 + ) 120 + .execute(&mut conn) 121 + .await?; 122 + 123 + Ok(()) 124 + } 125 + 126 + pub async fn unmute_actor_list( 127 + State(state): State<GlobalState>, 128 + auth: AtpAuth, 129 + Json(form): Json<MuteActorListReq>, 130 + ) -> XrpcResult<()> { 131 + let mut conn = state.pool.get().await?; 132 + 133 + diesel::delete(schema::list_mutes::table) 134 + .filter( 135 + schema::list_mutes::did 136 + .eq(&auth.0) 137 + .and(schema::list_mutes::list_uri.eq(&form.list)), 138 + ) 139 + .execute(&mut conn) 140 + .await?; 141 + 142 + Ok(()) 143 + }
+7 -1
parakeet/src/xrpc/app_bsky/mod.rs
··· 1 - use axum::routing::get; 1 + use axum::routing::{get, post}; 2 2 use axum::Router; 3 3 4 4 mod actor; ··· 27 27 .route("/app.bsky.graph.getFollowers", get(graph::relations::get_followers)) 28 28 .route("/app.bsky.graph.getFollows", get(graph::relations::get_follows)) 29 29 .route("/app.bsky.graph.getList", get(graph::lists::get_list)) 30 + .route("/app.bsky.graph.getListMutes", get(graph::lists::get_list_mutes)) 30 31 .route("/app.bsky.graph.getLists", get(graph::lists::get_lists)) 32 + .route("/app.bsky.graph.getMutes", get(graph::mutes::get_mutes)) 31 33 .route("/app.bsky.graph.getStarterPack", get(graph::starter_packs::get_starter_pack)) 32 34 .route("/app.bsky.graph.getStarterPacks", get(graph::starter_packs::get_starter_packs)) 35 + .route("/app.bsky.graph.muteActor", post(graph::mutes::mute_actor)) 36 + .route("/app.bsky.graph.muteActorList", post(graph::mutes::mute_actor_list)) 37 + .route("/app.bsky.graph.unmuteActor", post(graph::mutes::unmute_actor)) 38 + .route("/app.bsky.graph.unmuteActorList", post(graph::mutes::unmute_actor_list)) 33 39 .route("/app.bsky.labeler.getServices", get(labeler::get_services)) 34 40 }
+6
parakeet/src/xrpc/mod.rs
··· 95 95 } 96 96 97 97 #[derive(Debug, Deserialize)] 98 + pub struct CursorQuery { 99 + pub limit: Option<u8>, 100 + pub cursor: Option<String>, 101 + } 102 + 103 + #[derive(Debug, Deserialize)] 98 104 pub struct ActorWithCursorQuery { 99 105 pub actor: String, 100 106 pub limit: Option<u8>,