1use crate::error::Result;
2use deadpool_postgres::GenericClient;
3
4pub mod actor;
5pub mod bulk_copy;
6pub mod bulk_resolve;
7pub mod composite_builders;
8pub mod gates;
9pub mod id_resolution;
10pub mod labels;
11pub mod operations;
12pub mod record_exists;
13pub mod workers;
14
15pub use actor::*;
16pub use gates::*;
17pub use labels::*;
18pub use operations::*;
19pub use record_exists::record_exists;
20
21/// Check if a recipient has muted a thread
22///
23/// # Arguments
24///
25/// * `recipient_actor_id` - The actor_id of the potential recipient
26/// * `root_post_actor_id` - The actor_id of the root post author
27/// * `root_post_rkey` - The rkey (as i64) of the root post
28///
29/// # Returns
30///
31/// `true` if the recipient has muted this thread, `false` otherwise
32pub async fn is_thread_muted<C: GenericClient>(
33 conn: &C,
34 recipient_actor_id: i32,
35 root_post_actor_id: i32,
36 root_post_rkey: i64,
37) -> Result<bool> {
38 let row = conn
39 .query_opt(
40 "SELECT 1 FROM actors, unnest(COALESCE(thread_mutes, ARRAY[]::thread_mute_record[])) AS tm
41 WHERE id = $1
42 AND (tm).root_post_actor_id = $2
43 AND (tm).root_post_rkey = $3",
44 &[&recipient_actor_id, &root_post_actor_id, &root_post_rkey],
45 )
46 .await?;
47 Ok(row.is_some())
48}
49
50/// Walk up the reply chain to get the parent post's author actor_id and parent URI
51///
52/// This is used by NotifyReplyChain to traverse the reply chain and notify ancestors.
53/// Returns (author_actor_id, parent_uri) if the post exists, None if not found.
54pub async fn get_reply_chain_parent<C: GenericClient>(
55 conn: &C,
56 post_uri: &str,
57) -> Result<Option<(i32, Option<String>)>> {
58 let row_opt = conn
59 .query_opt(
60 "WITH post_parts AS (
61 SELECT
62 SPLIT_PART(SUBSTRING($1 FROM 6), '/', 1) as did,
63 SPLIT_PART(SUBSTRING($1 FROM 6), '/', 3) as rkey
64 ),
65 post_lookup AS (
66 SELECT p.actor_id, p.parent_post_actor_id, p.parent_post_rkey
67 FROM post_parts pp
68 INNER JOIN actors a ON a.did = pp.did
69 INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64(pp.rkey)
70 )
71 SELECT pl.actor_id,
72 CASE WHEN pl.parent_post_actor_id IS NOT NULL THEN
73 'at://' || pa.did || '/app.bsky.feed.post/' || i64_to_tid(pl.parent_post_rkey)
74 ELSE NULL END as parent_uri
75 FROM post_lookup pl
76 LEFT JOIN actors pa ON pa.id = pl.parent_post_actor_id",
77 &[&post_uri],
78 )
79 .await?;
80
81 Ok(row_opt.map(|row| {
82 let author_actor_id: i32 = row.get(0);
83 let parent_uri: Option<String> = row.get(1);
84 (author_actor_id, parent_uri)
85 }))
86}
87
88/// Check if a post exists
89///
90/// # Arguments
91///
92/// * `actor_id` - The actor_id of the post author
93/// * `rkey` - The rkey (as i64) of the post
94///
95/// # Returns
96///
97/// `true` if the post exists, `false` otherwise
98///
99/// This is a simple lookup helper to consolidate the many inline EXISTS queries
100/// scattered throughout the codebase.
101pub async fn post_exists<C: GenericClient>(
102 conn: &C,
103 actor_id: i32,
104 rkey: i64,
105) -> Result<bool> {
106 let exists: bool = conn
107 .query_one(
108 "SELECT EXISTS(SELECT 1 FROM posts WHERE actor_id = $1 AND rkey = $2)",
109 &[&actor_id, &rkey],
110 )
111 .await?
112 .get(0);
113 Ok(exists)
114}