···11+use super::{PgExecResult, PgResult};
22+use chrono::prelude::*;
33+use chrono::{DateTime, Utc};
44+use deadpool_postgres::GenericClient;
55+use std::collections::HashSet;
66+77+pub async fn post_enforce_threadgate<C: GenericClient>(
88+ conn: &mut C,
99+ root: &str,
1010+ post_author: &str,
1111+ post_created_at: DateTime<Utc>,
1212+ is_backfill: bool,
1313+) -> PgResult<bool> {
1414+ // check if the root and the current post are the same author
1515+ // strip "at://" then break into parts by '/'
1616+ let parts = root[5..].split('/').collect::<Vec<_>>();
1717+ let root_author = parts[0];
1818+ if root_author == post_author {
1919+ return Ok(false);
2020+ }
2121+2222+ let tg_data = super::threadgate_get(conn, root).await?;
2323+2424+ let Some((created_at, allow, allow_lists)) = tg_data else {
2525+ return Ok(false);
2626+ };
2727+2828+ // when backfilling, there's no point continuing if the record is dated before the threadgate
2929+ if is_backfill && post_created_at < created_at {
3030+ return Ok(false);
3131+ }
3232+3333+ if allow.is_empty() {
3434+ return Ok(true);
3535+ }
3636+3737+ let allow: HashSet<String> = HashSet::from_iter(allow);
3838+3939+ if allow.contains("app.bsky.feed.threadgate#followerRule")
4040+ || allow.contains("app.bsky.feed.threadgate#followingRule")
4141+ {
4242+ let profile_state: Option<(bool, bool)> = conn
4343+ .query_opt(
4444+ "SELECT following IS NOT NULL, followed IS NOT NULL FROM profile_states WHERE did=$1 AND subject=$2",
4545+ &[&root_author, &post_author],
4646+ )
4747+ .await?
4848+ .map(|v| (v.get(0), v.get(1)));
4949+5050+ if let Some((following, followed)) = profile_state {
5151+ if allow.contains("app.bsky.feed.threadgate#followerRule") && followed {
5252+ return Ok(false);
5353+ }
5454+5555+ if allow.contains("app.bsky.feed.threadgate#followingRule") && following {
5656+ return Ok(false);
5757+ }
5858+ }
5959+ }
6060+6161+ // check mentions
6262+ if allow.contains("app.bsky.feed.threadgate#mentionRule") {
6363+ let mentions: Vec<String> = conn
6464+ .query_opt("SELECT mentions FROM posts WHERE at_uri=$1", &[&root])
6565+ .await?
6666+ .map(|r| r.get(0))
6767+ .unwrap_or_default();
6868+6969+ if mentions.contains(&post_author.to_owned()) {
7070+ return Ok(false);
7171+ }
7272+ }
7373+7474+ if allow.contains("app.bsky.feed.threadgate#listRule") {
7575+ if allow_lists.is_empty() {
7676+ return Ok(true);
7777+ }
7878+7979+ let count: i64 = conn
8080+ .query_one(
8181+ "SELECT count(*) FROM list_items WHERE list_uri=ANY($1) AND subject=$2",
8282+ &[&allow_lists, &post_author],
8383+ )
8484+ .await?
8585+ .get(0);
8686+ if count == 0 {
8787+ return Ok(true);
8888+ }
8989+ }
9090+9191+ Ok(false)
9292+}
9393+9494+pub async fn postgate_maintain_detaches<C: GenericClient>(
9595+ conn: &mut C,
9696+ post: &str,
9797+ detached: &[String],
9898+ disable_effective: Option<NaiveDateTime>,
9999+) -> PgExecResult {
100100+ conn.execute(
101101+ "SELECT maintain_postgates($1, $2, $3)",
102102+ &[&post, &detached, &disable_effective],
103103+ )
104104+ .await
105105+}
+2
consumer/src/db/mod.rs
···77mod actor;
88mod backfill;
99pub mod copy;
1010+mod gates;
1011mod labels;
1112mod record;
12131314pub use actor::*;
1415pub use backfill::*;
1616+pub use gates::*;
1517pub use labels::*;
1618pub use record::*;
+2-102
consumer/src/db/record.rs
···336336 // if there is a root, we need to check for the presence of a threadgate.
337337 let violates_threadgate = match &root_uri {
338338 Some(root) => {
339339- post_enforce_threadgate(conn, root, repo, rec.created_at, is_backfill).await?
339339+ super::post_enforce_threadgate(conn, root, repo, rec.created_at, is_backfill).await?
340340 }
341341 None => false,
342342 };
···380380 .await
381381}
382382383383-pub async fn post_enforce_threadgate<C: GenericClient>(
384384- conn: &mut C,
385385- root: &str,
386386- post_author: &str,
387387- post_created_at: DateTime<Utc>,
388388- is_backfill: bool,
389389-) -> PgResult<bool> {
390390- // check if the root and the current post are the same author
391391- // strip "at://" then break into parts by '/'
392392- let parts = root[5..].split('/').collect::<Vec<_>>();
393393- let root_author = parts[0];
394394- if root_author == post_author {
395395- return Ok(false);
396396- }
397397-398398- let tg_data = threadgate_get(conn, root).await?;
399399-400400- let Some((created_at, allow, allow_lists)) = tg_data else {
401401- return Ok(false);
402402- };
403403-404404- // when backfilling, there's no point continuing if the record is dated before the threadgate
405405- if is_backfill && post_created_at < created_at {
406406- return Ok(false);
407407- }
408408-409409- if allow.is_empty() {
410410- return Ok(true);
411411- }
412412-413413- let allow: HashSet<String> = HashSet::from_iter(allow);
414414-415415- if allow.contains("app.bsky.feed.threadgate#followerRule")
416416- || allow.contains("app.bsky.feed.threadgate#followingRule")
417417- {
418418- let profile_state: Option<(bool, bool)> = conn
419419- .query_opt(
420420- "SELECT following IS NOT NULL, followed IS NOT NULL FROM profile_states WHERE did=$1 AND subject=$2",
421421- &[&root_author, &post_author],
422422- )
423423- .await?
424424- .map(|v| (v.get(0), v.get(1)));
425425-426426- if let Some((following, followed)) = profile_state {
427427- if allow.contains("app.bsky.feed.threadgate#followerRule") && followed {
428428- return Ok(false);
429429- }
430430-431431- if allow.contains("app.bsky.feed.threadgate#followingRule") && following {
432432- return Ok(false);
433433- }
434434- }
435435- }
436436-437437- // check mentions
438438- if allow.contains("app.bsky.feed.threadgate#mentionRule") {
439439- let mentions: Vec<String> = conn
440440- .query_opt("SELECT mentions FROM posts WHERE at_uri=$1", &[&root])
441441- .await?
442442- .map(|r| r.get(0))
443443- .unwrap_or_default();
444444-445445- if mentions.contains(&post_author.to_owned()) {
446446- return Ok(false);
447447- }
448448- }
449449-450450- if allow.contains("app.bsky.feed.threadgate#listRule") {
451451- if allow_lists.is_empty() {
452452- return Ok(true);
453453- }
454454-455455- let count: i64 = conn
456456- .query_one(
457457- "SELECT count(*) FROM list_items WHERE list_uri=ANY($1) AND subject=$2",
458458- &[&allow_lists, &post_author],
459459- )
460460- .await?
461461- .get(0);
462462- if count == 0 {
463463- return Ok(true);
464464- }
465465- }
466466-467467- Ok(false)
468468-}
469469-470383pub async fn post_get_info_for_delete<C: GenericClient>(
471384 conn: &mut C,
472385 at_uri: &str,
···665578 .await
666579}
667580668668-pub async fn postgate_maintain_detaches<C: GenericClient>(
669669- conn: &mut C,
670670- post: &str,
671671- detached: &[String],
672672- disable_effective: Option<NaiveDateTime>,
673673-) -> PgExecResult {
674674- conn.execute(
675675- "SELECT maintain_postgates($1, $2, $3)",
676676- &[&post, &detached, &disable_effective],
677677- )
678678- .await
679679-}
680680-681581pub async fn profile_upsert<C: GenericClient>(
682582 conn: &mut C,
683583 repo: &str,
···827727 .await
828728}
829729830830-async fn threadgate_get<C: GenericClient>(
730730+pub async fn threadgate_get<C: GenericClient>(
831731 conn: &mut C,
832732 post: &str,
833733) -> PgOptResult<(DateTime<Utc>, Vec<String>, Vec<String>)> {