+22
migrations/2025-08-03-125504_mutes/up.sql
+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
+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
+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
+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
parakeet/src/xrpc/app_bsky/graph/mod.rs
+143
parakeet/src/xrpc/app_bsky/graph/mutes.rs
+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
+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
+6
parakeet/src/xrpc/mod.rs