this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 227 lines 6.6 kB view raw
1use anyhow::{anyhow, Context, Result}; 2use axum::{extract::State, response::IntoResponse, Json}; 3use axum_extra::extract::Query; 4use base64::{engine::general_purpose, Engine as _}; 5use chrono::Utc; 6use http::{HeaderMap, StatusCode}; 7use serde::{Deserialize, Serialize}; 8use serde_json::json; 9 10use crate::errors::SupercellError; 11use crate::storage::{verification_method_get, StoragePool}; 12 13use crate::crypto::{validate, JwtClaims, JwtHeader}; 14 15use super::context::WebContext; 16 17#[derive(Deserialize, Default)] 18pub struct FeedParams { 19 pub feed: Option<String>, 20 pub limit: Option<u16>, 21 pub cursor: Option<String>, 22} 23 24#[derive(Serialize)] 25pub struct FeedItemView { 26 pub post: String, 27} 28 29#[derive(Serialize)] 30pub struct FeedItemsView { 31 #[serde(skip_serializing_if = "Option::is_none")] 32 pub cursor: Option<String>, 33 pub feed: Vec<FeedItemView>, 34} 35 36pub async fn handle_get_feed_skeleton( 37 State(web_context): State<WebContext>, 38 Query(feed_params): Query<FeedParams>, 39 headers: HeaderMap, 40) -> Result<impl IntoResponse, SupercellError> { 41 if feed_params.feed.is_none() { 42 return Err(anyhow!("feed parameter is required").into()); 43 } 44 let feed_uri = feed_params.feed.unwrap(); 45 46 let feed_control = web_context.feeds.get(&feed_uri); 47 if feed_control.is_none() { 48 return Ok(( 49 StatusCode::BAD_REQUEST, 50 Json(json!({ 51 "error": "UnknownFeed", 52 "message": "unknown feed", 53 })), 54 ) 55 .into_response()); 56 } 57 58 let feed_control = feed_control.unwrap(); 59 60 if !feed_control.allowed.is_empty() { 61 let authorization = headers.get("Authorization").and_then(|value| { 62 value 63 .to_str() 64 .map(|inner_value| inner_value.to_string()) 65 .ok() 66 }); 67 68 let did = did_from_jwt(&web_context.pool, &web_context.external_base, authorization).await; 69 70 if let Err(err) = did { 71 tracing::info!(error = ?err, "failed to validate JWT"); 72 return Ok(Json(FeedItemsView { 73 cursor: None, 74 feed: feed_control 75 .deny 76 .as_ref() 77 .map(|value| { 78 vec![FeedItemView { 79 post: value.clone(), 80 }] 81 }) 82 .unwrap_or(vec![]), 83 }) 84 .into_response()); 85 } 86 87 let did = did.unwrap(); 88 89 if !feed_control.allowed.contains(&did) { 90 return Ok(Json(FeedItemsView { 91 cursor: None, 92 feed: feed_control 93 .deny 94 .as_ref() 95 .map(|value| { 96 vec![FeedItemView { 97 post: value.clone(), 98 }] 99 }) 100 .unwrap_or(vec![]), 101 }) 102 .into_response()); 103 } 104 } 105 106 let parsed_cursor = parse_cursor(feed_params.cursor) 107 .map(|value| value.clamp(0, 10000)) 108 .unwrap_or(0) as usize; 109 110 let posts = web_context.cache.get_posts(&feed_uri, parsed_cursor).await; 111 112 if posts.is_none() { 113 return Ok(( 114 StatusCode::BAD_REQUEST, 115 Json(json!({ 116 "error": "UnknownFeed", 117 "message": "unknown feed", 118 })), 119 ) 120 .into_response()); 121 } 122 let posts = posts.unwrap(); 123 124 let cursor = if posts.is_empty() { 125 Some(parsed_cursor.to_string()) 126 } else { 127 Some((parsed_cursor + 1).to_string()) 128 }; 129 130 let feed_item_views = posts 131 .iter() 132 .map(|feed_item| FeedItemView { 133 post: feed_item.clone(), 134 }) 135 .collect::<Vec<_>>(); 136 137 Ok(Json(FeedItemsView { 138 cursor, 139 feed: feed_item_views, 140 }) 141 .into_response()) 142} 143 144pub fn split_token(token: &str) -> Result<[&str; 3]> { 145 let mut components = token.split('.'); 146 let header = components.next().ok_or(anyhow!("missing header"))?; 147 let claims = components.next().ok_or(anyhow!("missing claims"))?; 148 let signature = components.next().ok_or(anyhow!("missing signature"))?; 149 150 if components.next().is_some() { 151 return Err(anyhow!("invalid token")); 152 } 153 154 Ok([header, claims, signature]) 155} 156 157async fn did_from_jwt( 158 pool: &StoragePool, 159 external_base: &str, 160 authorization: Option<String>, 161) -> Result<String> { 162 let jwt = authorization 163 .and_then(|value| { 164 value 165 .strip_prefix("Bearer ") 166 .map(|inner_value| inner_value.to_string()) 167 }) 168 .ok_or(anyhow!("missing authorization"))?; 169 let [header_part, claims_part, signature_part] = split_token(&jwt)?; 170 171 let header: JwtHeader = { 172 let content = general_purpose::URL_SAFE_NO_PAD 173 .decode(header_part) 174 .context("unable to base64 decode content")?; 175 serde_json::from_slice(&content).context("unable to deserialize object")? 176 }; 177 let claims: JwtClaims = { 178 let content = general_purpose::URL_SAFE_NO_PAD 179 .decode(claims_part) 180 .context("unable to base64 decode content")?; 181 serde_json::from_slice(&content).context("unable to deserialize object")? 182 }; 183 184 let now = Utc::now(); 185 let now = now.timestamp() as i32; 186 187 if header.alg != "ES256K" { 188 return Err(anyhow!("unsupported algorithm")); 189 } 190 if claims.lxm != "app.bsky.feed.getFeedSkeleton" { 191 return Err(anyhow!("invalid resource")); 192 } 193 if claims.aud != format!("did:web:{}", external_base) { 194 return Err(anyhow!("invalid audience")); 195 } 196 if claims.exp < now { 197 return Err(anyhow!("token expired")); 198 } 199 if claims.iat > now { 200 return Err(anyhow!("token issued in the future")); 201 } 202 203 let multibase = verification_method_get(pool, &claims.iss).await?; 204 if multibase.is_none() { 205 return Err(anyhow!("verification method not found")); 206 } 207 let multibase = multibase.unwrap(); 208 209 let signature = general_purpose::URL_SAFE_NO_PAD 210 .decode(signature_part) 211 .context("invalid signature")?; 212 let signature: &[u8] = &signature; 213 214 let content = format!("{}.{}", header_part, claims_part); 215 216 validate(&multibase, signature, &content)?; 217 218 Ok(claims.iss) 219} 220 221fn parse_cursor(value: Option<String>) -> Option<i64> { 222 let value = value.as_ref()?; 223 224 let parts = value.split(",").collect::<Vec<&str>>(); 225 226 parts.first().and_then(|value| value.parse::<i64>().ok()) 227}