tangled
alpha
login
or
join now
parakeet.at
/
parakeet
62
fork
atom
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
62
fork
atom
overview
issues
12
pulls
pipelines
store if a post violates a threadgate
mia.omg.lol
5 months ago
20e03838
bc019eb8
verified
This commit was signed with the committer's
known signature
.
mia.omg.lol
SSH Key Fingerprint:
SHA256:eb+NhC0QEl+XKRuFP/97oH6LEz0TXTKPXGDIAI5y7CQ=
+144
-4
6 changed files
expand all
collapse all
unified
split
consumer
src
db
copy.rs
record.rs
sql
post_insert.sql
migrations
2025-09-27-171241_post-tweaks
down.sql
up.sql
parakeet-db
src
schema.rs
+26
consumer/src/db/copy.rs
reviewed
···
198
198
199
199
writer.finish().await?;
200
200
201
201
+
let threadgated: Vec<(String, String, DateTime<Utc>)> = conn
202
202
+
.query(
203
203
+
"SELECT root_uri, p.at_uri, p.created_at FROM posts_tmp p INNER JOIN threadgates t ON root_uri = post_uri WHERE t.allow IS NOT NULL",
204
204
+
&[],
205
205
+
)
206
206
+
.await?
207
207
+
.into_iter()
208
208
+
.map(|v| (v.get(0), v.get(1), v.get(2))).collect();
209
209
+
210
210
+
for (root, post, created_at) in dbg!(threadgated) {
211
211
+
match super::post_enforce_threadgate(conn, &root, did, created_at, true).await {
212
212
+
Ok(true) => {
213
213
+
conn.execute(
214
214
+
"UPDATE posts_tmp SET violates_threadgate=TRUE WHERE at_uri=$1",
215
215
+
&[&post],
216
216
+
)
217
217
+
.await?;
218
218
+
}
219
219
+
Ok(false) => continue,
220
220
+
Err(e) => {
221
221
+
tracing::error!("failed to check threadgate enforcement: {e}");
222
222
+
continue;
223
223
+
}
224
224
+
}
225
225
+
}
226
226
+
201
227
conn.execute("INSERT INTO posts (SELECT * FROM posts_tmp)", &[])
202
228
.await
203
229
}
+111
consumer/src/db/record.rs
reviewed
···
333
333
let embed = rec.embed.as_ref().map(|v| v.as_str());
334
334
let embed_subtype = rec.embed.as_ref().and_then(|v| v.subtype());
335
335
336
336
+
// if there is a root, we need to check for the presence of a threadgate.
337
337
+
let violates_threadgate = match &root_uri {
338
338
+
Some(root) => {
339
339
+
post_enforce_threadgate(conn, root, repo, rec.created_at, is_backfill).await?
340
340
+
}
341
341
+
None => false,
342
342
+
};
343
343
+
336
344
let tags = merge_tags(tags, rec.tags);
337
345
338
346
let count = conn
···
354
362
&embed,
355
363
&embed_subtype,
356
364
&mentions,
365
365
+
&violates_threadgate,
357
366
&rec.created_at,
358
367
],
359
368
)
···
369
378
pub async fn post_delete<C: GenericClient>(conn: &mut C, at_uri: &str) -> PgExecResult {
370
379
conn.execute("DELETE FROM posts WHERE at_uri=$1", &[&at_uri])
371
380
.await
381
381
+
}
382
382
+
383
383
+
pub async fn post_enforce_threadgate<C: GenericClient>(
384
384
+
conn: &mut C,
385
385
+
root: &str,
386
386
+
post_author: &str,
387
387
+
post_created_at: DateTime<Utc>,
388
388
+
is_backfill: bool,
389
389
+
) -> PgResult<bool> {
390
390
+
// check if the root and the current post are the same author
391
391
+
// strip "at://" then break into parts by '/'
392
392
+
let parts = root[5..].split('/').collect::<Vec<_>>();
393
393
+
let root_author = parts[0];
394
394
+
if root_author == post_author {
395
395
+
return Ok(false);
396
396
+
}
397
397
+
398
398
+
let tg_data = threadgate_get(conn, root).await?;
399
399
+
400
400
+
let Some((created_at, allow, allow_lists)) = tg_data else {
401
401
+
return Ok(false);
402
402
+
};
403
403
+
404
404
+
// when backfilling, there's no point continuing if the record is dated before the threadgate
405
405
+
if is_backfill && post_created_at < created_at {
406
406
+
return Ok(false);
407
407
+
}
408
408
+
409
409
+
if allow.is_empty() {
410
410
+
return Ok(true);
411
411
+
}
412
412
+
413
413
+
let allow: HashSet<String> = HashSet::from_iter(allow);
414
414
+
415
415
+
if allow.contains("app.bsky.feed.threadgate#followerRule")
416
416
+
|| allow.contains("app.bsky.feed.threadgate#followingRule")
417
417
+
{
418
418
+
let profile_state: Option<(bool, bool)> = conn
419
419
+
.query_opt(
420
420
+
"SELECT following IS NOT NULL, followed IS NOT NULL FROM profile_states WHERE did=$1 AND subject=$2",
421
421
+
&[&root_author, &post_author],
422
422
+
)
423
423
+
.await?
424
424
+
.map(|v| (v.get(0), v.get(1)));
425
425
+
426
426
+
if let Some((following, followed)) = profile_state {
427
427
+
if allow.contains("app.bsky.feed.threadgate#followerRule") && followed {
428
428
+
return Ok(false);
429
429
+
}
430
430
+
431
431
+
if allow.contains("app.bsky.feed.threadgate#followingRule") && following {
432
432
+
return Ok(false);
433
433
+
}
434
434
+
}
435
435
+
}
436
436
+
437
437
+
// check mentions
438
438
+
if allow.contains("app.bsky.feed.threadgate#mentionRule") {
439
439
+
let mentions: Vec<String> = conn
440
440
+
.query_opt("SELECT mentions FROM posts WHERE at_uri=$1", &[&root])
441
441
+
.await?
442
442
+
.map(|r| r.get(0))
443
443
+
.unwrap_or_default();
444
444
+
445
445
+
if mentions.contains(&post_author.to_owned()) {
446
446
+
return Ok(false);
447
447
+
}
448
448
+
}
449
449
+
450
450
+
if allow.contains("app.bsky.feed.threadgate#listRule") {
451
451
+
if allow_lists.is_empty() {
452
452
+
return Ok(true);
453
453
+
}
454
454
+
455
455
+
let count: i64 = conn
456
456
+
.query_one(
457
457
+
"SELECT count(*) FROM list_items WHERE list_uri=ANY($1) AND subject=$2",
458
458
+
&[&allow_lists, &post_author],
459
459
+
)
460
460
+
.await?
461
461
+
.get(0);
462
462
+
if count == 0 {
463
463
+
return Ok(true);
464
464
+
}
465
465
+
}
466
466
+
467
467
+
Ok(false)
372
468
}
373
469
374
470
pub async fn post_get_info_for_delete<C: GenericClient>(
···
729
825
pub async fn status_delete<C: GenericClient>(conn: &mut C, did: &str) -> PgExecResult {
730
826
conn.execute("DELETE FROM statuses WHERE did=$1", &[&did])
731
827
.await
828
828
+
}
829
829
+
830
830
+
async fn threadgate_get<C: GenericClient>(
831
831
+
conn: &mut C,
832
832
+
post: &str,
833
833
+
) -> PgOptResult<(DateTime<Utc>, Vec<String>, Vec<String>)> {
834
834
+
let res = conn
835
835
+
.query_opt(
836
836
+
"SELECT created_at, allow, allowed_lists FROM threadgates WHERE post_uri=$1 AND allow IS NOT NULL",
837
837
+
&[&post],
838
838
+
)
839
839
+
.await?
840
840
+
.map(|v| (v.get(0), v.get(1), v.get(2)));
841
841
+
842
842
+
Ok(res)
732
843
}
733
844
734
845
pub async fn threadgate_upsert<C: GenericClient>(
+2
-2
consumer/src/db/sql/post_insert.sql
reviewed
···
1
1
INSERT INTO posts (at_uri, did, cid, record, content, facets, languages, tags, parent_uri, parent_cid, root_uri,
2
2
-
root_cid, embed, embed_subtype, mentions, created_at)
3
3
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
2
2
+
root_cid, embed, embed_subtype, mentions, violates_threadgate, created_at)
3
3
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
4
4
ON CONFLICT DO NOTHING
+2
-1
migrations/2025-09-27-171241_post-tweaks/down.sql
reviewed
···
1
1
alter table posts
2
2
-
drop column mentions;
2
2
+
drop column mentions,
3
3
+
drop column violates_threadgate;
+2
-1
migrations/2025-09-27-171241_post-tweaks/up.sql
reviewed
···
1
1
alter table posts
2
2
-
add column mentions text[];
2
2
+
add column mentions text[],
3
3
+
add column violates_threadgate bool not null default false;
+1
parakeet-db/src/schema.rs
reviewed
···
285
285
created_at -> Timestamptz,
286
286
indexed_at -> Timestamp,
287
287
mentions -> Nullable<Array<Nullable<Text>>>,
288
288
+
violates_threadgate -> Bool,
288
289
}
289
290
}
290
291