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}