this repo has no description
1// Yes, I know, this endpoint is an appview one, not for PDS. Who cares!!
2// Yes, this only gets posts that our DB/instance knows about. Who cares!!!
3
4use crate::state::AppState;
5use axum::{
6 Json,
7 extract::State,
8 http::StatusCode,
9 response::{IntoResponse, Response},
10};
11use jacquard_repo::storage::BlockStore;
12use serde::Serialize;
13use serde_json::{Value, json};
14use tracing::error;
15
16#[derive(Serialize)]
17pub struct TimelineOutput {
18 pub feed: Vec<FeedViewPost>,
19 pub cursor: Option<String>,
20}
21
22#[derive(Serialize)]
23pub struct FeedViewPost {
24 pub post: PostView,
25}
26
27#[derive(Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct PostView {
30 pub uri: String,
31 pub cid: String,
32 pub author: AuthorView,
33 pub record: Value,
34 pub indexed_at: String,
35}
36
37#[derive(Serialize)]
38pub struct AuthorView {
39 pub did: String,
40 pub handle: String,
41}
42
43pub async fn get_timeline(
44 State(state): State<AppState>,
45 headers: axum::http::HeaderMap,
46) -> Response {
47 let token = match crate::auth::extract_bearer_token_from_header(
48 headers.get("Authorization").and_then(|h| h.to_str().ok())
49 ) {
50 Some(t) => t,
51 None => {
52 return (
53 StatusCode::UNAUTHORIZED,
54 Json(json!({"error": "AuthenticationRequired"})),
55 )
56 .into_response();
57 }
58 };
59
60 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
61 Ok(user) => user,
62 Err(_) => {
63 return (
64 StatusCode::UNAUTHORIZED,
65 Json(json!({"error": "AuthenticationFailed"})),
66 )
67 .into_response();
68 }
69 };
70
71 let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", auth_user.did)
72 .fetch_optional(&state.db)
73 .await;
74
75 let user_id = match user_query {
76 Ok(Some(row)) => row.id,
77 _ => {
78 return (
79 StatusCode::INTERNAL_SERVER_ERROR,
80 Json(json!({"error": "InternalError", "message": "User not found"})),
81 )
82 .into_response();
83 }
84 };
85
86 let follows_query = sqlx::query!(
87 "SELECT record_cid FROM records WHERE repo_id = $1 AND collection = 'app.bsky.graph.follow'",
88 user_id
89 )
90 .fetch_all(&state.db)
91 .await;
92
93 let follow_cids: Vec<String> = match follows_query {
94 Ok(rows) => rows.iter().map(|r| r.record_cid.clone()).collect(),
95 Err(e) => {
96 error!("Failed to get follows: {:?}", e);
97 return (
98 StatusCode::INTERNAL_SERVER_ERROR,
99 Json(json!({"error": "InternalError"})),
100 )
101 .into_response();
102 }
103 };
104
105 let mut followed_dids: Vec<String> = Vec::new();
106 for cid_str in follow_cids {
107 let cid = match cid_str.parse::<cid::Cid>() {
108 Ok(c) => c,
109 Err(_) => continue,
110 };
111
112 let block_bytes = match state.block_store.get(&cid).await {
113 Ok(Some(b)) => b,
114 _ => continue,
115 };
116
117 let record: Value = match serde_ipld_dagcbor::from_slice(&block_bytes) {
118 Ok(v) => v,
119 Err(_) => continue,
120 };
121
122 if let Some(subject) = record.get("subject").and_then(|s| s.as_str()) {
123 followed_dids.push(subject.to_string());
124 }
125 }
126
127 if followed_dids.is_empty() {
128 return (
129 StatusCode::OK,
130 Json(TimelineOutput {
131 feed: vec![],
132 cursor: None,
133 }),
134 )
135 .into_response();
136 }
137
138 let posts_result = sqlx::query!(
139 "SELECT r.record_cid, r.rkey, r.created_at, u.did, u.handle
140 FROM records r
141 JOIN repos rp ON r.repo_id = rp.user_id
142 JOIN users u ON rp.user_id = u.id
143 WHERE u.did = ANY($1) AND r.collection = 'app.bsky.feed.post'
144 ORDER BY r.created_at DESC
145 LIMIT 50",
146 &followed_dids
147 )
148 .fetch_all(&state.db)
149 .await;
150
151 let posts = match posts_result {
152 Ok(rows) => rows,
153 Err(e) => {
154 error!("Failed to get posts: {:?}", e);
155 return (
156 StatusCode::INTERNAL_SERVER_ERROR,
157 Json(json!({"error": "InternalError"})),
158 )
159 .into_response();
160 }
161 };
162
163 let mut feed: Vec<FeedViewPost> = Vec::new();
164
165 for row in posts {
166 let record_cid: String = row.record_cid;
167 let rkey: String = row.rkey;
168 let created_at: chrono::DateTime<chrono::Utc> = row.created_at;
169 let author_did: String = row.did;
170 let author_handle: String = row.handle;
171
172 let cid = match record_cid.parse::<cid::Cid>() {
173 Ok(c) => c,
174 Err(_) => continue,
175 };
176
177 let block_bytes = match state.block_store.get(&cid).await {
178 Ok(Some(b)) => b,
179 _ => continue,
180 };
181
182 let record: Value = match serde_ipld_dagcbor::from_slice(&block_bytes) {
183 Ok(v) => v,
184 Err(_) => continue,
185 };
186
187 let uri = format!("at://{}/app.bsky.feed.post/{}", author_did, rkey);
188
189 feed.push(FeedViewPost {
190 post: PostView {
191 uri,
192 cid: record_cid,
193 author: AuthorView {
194 did: author_did,
195 handle: author_handle,
196 },
197 record,
198 indexed_at: created_at.to_rfc3339(),
199 },
200 });
201 }
202
203 (StatusCode::OK, Json(TimelineOutput { feed, cursor: None })).into_response()
204}