Parakeet is a Rust-based Bluesky AppServer aiming to implement most of the functionality required to support the Bluesky client
appview atproto bluesky rust appserver
66
fork

Configure Feed

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

Merge branch 'feat-media' into 'main'

Working Media

See merge request parakeet-social/parakeet!15

Mia d35c4f64 e00e955f

+160 -82
+27
parakeet/src/config.rs
··· 21 21 #[serde(default)] 22 22 pub trusted_verifiers: Vec<String>, 23 23 pub plc_directory: Option<String>, 24 + #[serde(default)] 25 + pub cdn: ConfigCdn, 24 26 } 25 27 26 28 #[derive(Debug, Deserialize)] ··· 54 56 fn default_port() -> u16 { 55 57 6000 56 58 } 59 + 60 + #[derive(Debug, Deserialize)] 61 + pub struct ConfigCdn { 62 + #[serde(default = "default_cdn_base")] 63 + pub base: String, 64 + #[serde(default = "default_cdn_video_base")] 65 + pub video_base: String, 66 + } 67 + 68 + impl Default for ConfigCdn { 69 + fn default() -> Self { 70 + ConfigCdn { 71 + base: default_cdn_base(), 72 + video_base: default_cdn_video_base(), 73 + } 74 + } 75 + } 76 + 77 + fn default_cdn_base() -> String { 78 + "https://cdn.bsky.app".to_string() 79 + } 80 + 81 + fn default_cdn_video_base() -> String { 82 + "https://video.bsky.app".to_string() 83 + }
+14 -15
parakeet/src/hydration/embed.rs
··· 1 1 use crate::hydration::StatefulHydrator; 2 2 use crate::loaders::EmbedLoaderRet; 3 + use crate::xrpc::cdn::BskyCdn; 3 4 use itertools::Itertools; 4 5 use lexica::app_bsky::embed::{ 5 6 AspectRatio, Embed, External, ImageView, RecordView, RecordViewInner, RecordWrapper, VideoView, ··· 27 28 } 28 29 } 29 30 30 - fn build_embed(embed: EmbedLoaderRet) -> Embed { 31 + fn build_embed(embed: EmbedLoaderRet, did: &str, cdn: &BskyCdn) -> Embed { 31 32 match embed { 32 33 EmbedLoaderRet::Images(images) => Embed::Images { 33 34 images: images 34 35 .into_iter() 35 36 .map(|img| ImageView { 36 - thumb: format!("https://localhost/postimg_thumb/{}", img.cid), 37 - fullsize: format!("https://localhost/postimg/{}", img.cid), 37 + thumb: cdn.embed_thumb(did, &img.cid), 38 + fullsize: cdn.embed_fullsize(did, &img.cid), 38 39 alt: img.alt.unwrap_or_default(), 39 40 aspect_ratio: build_aspect_ratio(img.height, img.width), 40 41 }) 41 42 .collect(), 42 43 }, 43 44 EmbedLoaderRet::Video(video) => Embed::Video(VideoView { 44 - playlist: format!("https://localhost/video/{}", &video.cid), 45 - thumbnail: Some(format!("https://localhost/videothumb/{}", &video.cid)), 45 + playlist: cdn.video_playlist(did, &video.cid), 46 + thumbnail: Some(cdn.video_thumb(did, &video.cid)), 46 47 cid: video.cid, 47 48 alt: video.alt, 48 49 aspect_ratio: build_aspect_ratio(video.height, video.width), ··· 52 53 uri: external.uri, 53 54 title: external.title, 54 55 description: external.description, 55 - thumb: external 56 - .thumb_cid 57 - .map(|v| format!("https://localhost/embed/{v}")), 56 + thumb: external.thumb_cid.map(|cid| cdn.embed_thumb(did, &cid)), 58 57 }, 59 58 }, 60 59 _ => unreachable!(), ··· 178 177 } 179 178 180 179 pub async fn hydrate_embed(&self, post: String) -> Option<Embed> { 181 - let embed = self.loaders.embed.load(post).await?; 180 + let (embed, author) = self.loaders.embed.load(post).await?; 182 181 183 182 match embed { 184 183 EmbedLoaderRet::Record(record) => self ··· 190 189 .await 191 190 .map(|record| Embed::RecordWithMedia { 192 191 record: RecordWrapper { record }, 193 - media: Box::new(build_embed(*media)), 192 + media: Box::new(build_embed(*media, &author, &self.cdn)), 194 193 }), 195 - _ => Some(build_embed(embed)), 194 + _ => Some(build_embed(embed, &author, &self.cdn)), 196 195 } 197 196 } 198 197 ··· 201 200 202 201 let with_records = embeds 203 202 .values() 204 - .filter_map(|v| match v { 203 + .filter_map(|(v, _)| match v { 205 204 EmbedLoaderRet::Record(rec) => Some(rec), 206 205 EmbedLoaderRet::RecordWithMedia(rec, _) => Some(rec), 207 206 _ => None, ··· 212 211 213 212 embeds 214 213 .into_iter() 215 - .filter_map(|(k, v)| { 214 + .filter_map(|(k, (v, author))| { 216 215 let embed = match v { 217 216 EmbedLoaderRet::Record(record) => { 218 217 let record = records.get(&record.uri).cloned().unwrap_or( ··· 234 233 235 234 Some(Embed::RecordWithMedia { 236 235 record: RecordWrapper { record }, 237 - media: Box::new(build_embed(*media)), 236 + media: Box::new(build_embed(*media, &author, &self.cdn)), 238 237 }) 239 238 } 240 - _ => Some(build_embed(v)), 239 + _ => Some(build_embed(v, &author, &self.cdn)), 241 240 }?; 242 241 243 242 Some((k, embed))
+10 -5
parakeet/src/hydration/feedgen.rs
··· 1 1 use crate::hydration::map_labels; 2 + use crate::xrpc::cdn::BskyCdn; 2 3 use lexica::app_bsky::actor::ProfileView; 3 4 use lexica::app_bsky::feed::{GeneratorContentMode, GeneratorView}; 4 5 use parakeet_db::models; ··· 10 11 creator: ProfileView, 11 12 labels: Vec<models::Label>, 12 13 likes: Option<i32>, 14 + cdn: &BskyCdn, 13 15 ) -> GeneratorView { 14 16 let content_mode = feedgen 15 17 .content_mode ··· 18 20 let description_facets = feedgen 19 21 .description_facets 20 22 .and_then(|v| serde_json::from_value(v).ok()); 23 + 24 + let avatar = feedgen.avatar_cid.map(|cid| cdn.avatar(&creator.did, &cid)); 21 25 22 26 GeneratorView { 23 27 uri: feedgen.at_uri, ··· 27 31 display_name: feedgen.name, 28 32 description: feedgen.description, 29 33 description_facets, 30 - avatar: feedgen 31 - .avatar_cid 32 - .map(|v| format!("https://localhost/feedgen/{v}")), 34 + avatar, 33 35 like_count: likes.unwrap_or_default() as i64, 34 36 accepts_interactions: feedgen.accepts_interactions, 35 37 labels: map_labels(labels), ··· 44 46 let (feedgen, likes) = self.loaders.feedgen.load(feedgen).await?; 45 47 let profile = self.hydrate_profile(feedgen.owner.clone()).await?; 46 48 47 - Some(build_feedgen(feedgen, profile, labels, likes)) 49 + Some(build_feedgen(feedgen, profile, labels, likes, &self.cdn)) 48 50 } 49 51 50 52 pub async fn hydrate_feedgens(&self, feedgens: Vec<String>) -> HashMap<String, GeneratorView> { ··· 64 66 let creator = creators.get(&feedgen.owner).cloned()?; 65 67 let labels = labels.get(&uri).cloned().unwrap_or_default(); 66 68 67 - Some((uri, build_feedgen(feedgen, creator, labels, likes))) 69 + Some(( 70 + uri, 71 + build_feedgen(feedgen, creator, labels, likes, &self.cdn), 72 + )) 68 73 }) 69 74 .collect() 70 75 }
+11 -10
parakeet/src/hydration/list.rs
··· 1 1 use crate::hydration::{map_labels, StatefulHydrator}; 2 + use crate::xrpc::cdn::BskyCdn; 2 3 use lexica::app_bsky::actor::ProfileView; 3 4 use lexica::app_bsky::graph::{ListPurpose, ListView, ListViewBasic}; 4 5 use parakeet_db::models; ··· 9 10 list: models::List, 10 11 list_item_count: i64, 11 12 labels: Vec<models::Label>, 13 + cdn: &BskyCdn, 12 14 ) -> Option<ListViewBasic> { 13 15 let purpose = ListPurpose::from_str(&list.list_type).ok()?; 16 + let avatar = list.avatar_cid.map(|cid| cdn.avatar(&list.owner, &cid)); 14 17 15 18 Some(ListViewBasic { 16 19 uri: list.at_uri, 17 20 cid: list.cid, 18 21 name: list.name, 19 22 purpose, 20 - avatar: list 21 - .avatar_cid 22 - .map(|v| format!("https://localhost/list/{v}")), 23 + avatar, 23 24 list_item_count, 24 25 labels: map_labels(labels), 25 26 indexed_at: list.indexed_at, ··· 31 32 list_item_count: i64, 32 33 creator: ProfileView, 33 34 labels: Vec<models::Label>, 35 + cdn: &BskyCdn, 34 36 ) -> Option<ListView> { 35 37 let purpose = ListPurpose::from_str(&list.list_type).ok()?; 38 + let avatar = list.avatar_cid.map(|cid| cdn.avatar(&list.owner, &cid)); 36 39 37 40 let description_facets = list 38 41 .description_facets ··· 46 49 purpose, 47 50 description: list.description, 48 51 description_facets, 49 - avatar: list 50 - .avatar_cid 51 - .map(|v| format!("https://localhost/list/{v}")), 52 + avatar, 52 53 list_item_count, 53 54 labels: map_labels(labels), 54 55 indexed_at: list.indexed_at, ··· 60 61 let labels = self.get_label(&list).await; 61 62 let (list, count) = self.loaders.list.load(list).await?; 62 63 63 - build_basic(list, count, labels) 64 + build_basic(list, count, labels, &self.cdn) 64 65 } 65 66 66 67 pub async fn hydrate_lists_basic(&self, lists: Vec<String>) -> HashMap<String, ListViewBasic> { ··· 72 73 .filter_map(|(uri, (list, count))| { 73 74 let labels = labels.get(&uri).cloned().unwrap_or_default(); 74 75 75 - build_basic(list, count, labels).map(|v| (uri, v)) 76 + build_basic(list, count, labels, &self.cdn).map(|v| (uri, v)) 76 77 }) 77 78 .collect() 78 79 } ··· 82 83 let (list, count) = self.loaders.list.load(list).await?; 83 84 let profile = self.hydrate_profile(list.owner.clone()).await?; 84 85 85 - build_listview(list, count, profile, labels) 86 + build_listview(list, count, profile, labels, &self.cdn) 86 87 } 87 88 88 89 pub async fn hydrate_lists(&self, lists: Vec<String>) -> HashMap<String, ListView> { ··· 98 99 let creator = creators.get(&list.owner)?; 99 100 let labels = labels.get(&uri).cloned().unwrap_or_default(); 100 101 101 - build_listview(list, count, creator.to_owned(), labels).map(|v| (uri, v)) 102 + build_listview(list, count, creator.to_owned(), labels, &self.cdn).map(|v| (uri, v)) 102 103 }) 103 104 .collect() 104 105 }
+4
parakeet/src/hydration/mod.rs
··· 1 1 #![allow(dead_code)] 2 2 3 3 use crate::loaders::Dataloaders; 4 + use crate::xrpc::cdn::BskyCdn; 4 5 use crate::xrpc::extract::LabelConfigItem; 5 6 use std::collections::HashMap; 6 7 use std::sync::Arc; ··· 39 40 loaders: Arc<Dataloaders>, 40 41 accept_labelers: &'a [LabelConfigItem], 41 42 current_actor: Option<String>, 43 + cdn: Arc<BskyCdn>, 42 44 } 43 45 44 46 impl StatefulHydrator<'_> { 45 47 pub fn new<'a>( 46 48 loaders: &Arc<Dataloaders>, 49 + cdn: &Arc<BskyCdn>, 47 50 accept_labelers: &'a [LabelConfigItem], 48 51 current_actor: Option<crate::xrpc::extract::AtpAuth>, 49 52 ) -> StatefulHydrator<'a> { ··· 51 54 loaders: loaders.clone(), 52 55 accept_labelers, 53 56 current_actor: current_actor.map(|v| v.0), 57 + cdn: cdn.clone(), 54 58 } 55 59 } 56 60
+23 -23
parakeet/src/hydration/profile.rs
··· 1 1 use crate::hydration::map_labels; 2 2 use crate::loaders::ProfileLoaderRet; 3 + use crate::xrpc::cdn::BskyCdn; 3 4 use chrono::prelude::*; 4 5 use chrono::TimeDelta; 5 6 use lexica::app_bsky::actor::*; ··· 114 115 } 115 116 } 116 117 117 - fn build_status(status: models::Status) -> Option<StatusView> { 118 + fn build_status(status: models::Status, cdn: &BskyCdn) -> Option<StatusView> { 118 119 let s = Status::from_str(&status.status).ok()?; 119 120 let embed = status 120 121 .embed_uri ··· 127 128 description, 128 129 thumb: status 129 130 .thumb_cid 130 - .map(|v| format!("https://localhost/embed/{v}")), 131 + .map(|cid| cdn.embed_thumb(&status.did, &cid)), 131 132 }, 132 133 }); 133 134 ··· 150 151 (handle, profile, chat_decl, is_labeler, stats, status): ProfileLoaderRet, 151 152 labels: Vec<models::Label>, 152 153 verifications: Option<Vec<models::VerificationEntry>>, 154 + cdn: &BskyCdn, 153 155 ) -> ProfileViewBasic { 154 156 let associated = build_associated(chat_decl, is_labeler, stats); 155 157 let verification = build_verification(&profile, &handle, verifications); 156 - let status = status.and_then(build_status); 158 + let status = status.and_then(|status| build_status(status, cdn)); 159 + let avatar = profile.avatar_cid.map(|cid| cdn.avatar(&profile.did, &cid)); 157 160 158 161 ProfileViewBasic { 159 162 did: profile.did, 160 163 handle: handle.unwrap_or("handle.invalid".to_string()), 161 164 display_name: profile.display_name, 162 - avatar: profile 163 - .avatar_cid 164 - .map(|v| format!("https://localhost/avatar/{v}")), 165 + avatar, 165 166 associated, 166 167 labels: map_labels(labels), 167 168 verification, ··· 174 175 (handle, profile, chat_decl, is_labeler, stats, status): ProfileLoaderRet, 175 176 labels: Vec<models::Label>, 176 177 verifications: Option<Vec<models::VerificationEntry>>, 178 + cdn: &BskyCdn, 177 179 ) -> ProfileView { 178 180 let associated = build_associated(chat_decl, is_labeler, stats); 179 181 let verification = build_verification(&profile, &handle, verifications); 180 - let status = status.and_then(build_status); 182 + let status = status.and_then(|status| build_status(status, cdn)); 183 + let avatar = profile.avatar_cid.map(|cid| cdn.avatar(&profile.did, &cid)); 181 184 182 185 ProfileView { 183 186 did: profile.did, 184 187 handle: handle.unwrap_or("handle.invalid".to_string()), 185 188 display_name: profile.display_name, 186 189 description: profile.description, 187 - avatar: profile 188 - .avatar_cid 189 - .map(|v| format!("https://localhost/avatar/{v}")), 190 + avatar, 190 191 associated, 191 192 labels: map_labels(labels), 192 193 verification, ··· 200 201 (handle, profile, chat_decl, is_labeler, stats, status): ProfileLoaderRet, 201 202 labels: Vec<models::Label>, 202 203 verifications: Option<Vec<models::VerificationEntry>>, 204 + cdn: &BskyCdn, 203 205 ) -> ProfileViewDetailed { 204 206 let associated = build_associated(chat_decl, is_labeler, stats); 205 207 let verification = build_verification(&profile, &handle, verifications); 206 - let status = status.and_then(build_status); 208 + let status = status.and_then(|status| build_status(status, cdn)); 209 + let avatar = profile.avatar_cid.map(|cid| cdn.avatar(&profile.did, &cid)); 210 + let banner = profile.banner_cid.map(|cid| cdn.banner(&profile.did, &cid)); 207 211 208 212 ProfileViewDetailed { 209 213 did: profile.did, 210 214 handle: handle.unwrap_or("handle.invalid".to_string()), 211 215 display_name: profile.display_name, 212 216 description: profile.description, 213 - avatar: profile 214 - .avatar_cid 215 - .map(|v| format!("https://localhost/avatar/{v}")), 216 - banner: profile 217 - .banner_cid 218 - .map(|v| format!("https://localhost/banner/{v}")), 217 + avatar, 218 + banner, 219 219 followers_count: stats.map(|v| v.followers as i64).unwrap_or_default(), 220 220 follows_count: stats.map(|v| v.following as i64).unwrap_or_default(), 221 221 associated, ··· 233 233 let verif = self.loaders.verification.load(did.clone()).await; 234 234 let profile_info = self.loaders.profile.load(did).await?; 235 235 236 - Some(build_basic(profile_info, labels, verif)) 236 + Some(build_basic(profile_info, labels, verif, &self.cdn)) 237 237 } 238 238 239 239 pub async fn hydrate_profiles_basic( ··· 250 250 let labels = labels.get(&k).cloned().unwrap_or_default(); 251 251 let verif = verif.get(&k).cloned(); 252 252 253 - let v = build_basic(profile_info, labels, verif); 253 + let v = build_basic(profile_info, labels, verif, &self.cdn); 254 254 (k, v) 255 255 }) 256 256 .collect() ··· 262 262 let verif = self.loaders.verification.load(did.clone()).await; 263 263 let profile_info = self.loaders.profile.load(did).await?; 264 264 265 - Some(build_profile(profile_info, labels, verif)) 265 + Some(build_profile(profile_info, labels, verif, &self.cdn)) 266 266 } 267 267 268 268 pub async fn hydrate_profiles(&self, dids: Vec<String>) -> HashMap<String, ProfileView> { ··· 276 276 let labels = labels.get(&k).cloned().unwrap_or_default(); 277 277 let verif = verif.get(&k).cloned(); 278 278 279 - let v = build_profile(profile_info, labels, verif); 279 + let v = build_profile(profile_info, labels, verif, &self.cdn); 280 280 (k, v) 281 281 }) 282 282 .collect() ··· 288 288 let verif = self.loaders.verification.load(did.clone()).await; 289 289 let profile_info = self.loaders.profile.load(did).await?; 290 290 291 - Some(build_detailed(profile_info, labels, verif)) 291 + Some(build_detailed(profile_info, labels, verif, &self.cdn)) 292 292 } 293 293 294 294 pub async fn hydrate_profiles_detailed( ··· 305 305 let labels = labels.get(&k).cloned().unwrap_or_default(); 306 306 let verif = verif.get(&k).cloned(); 307 307 308 - let v = build_detailed(profile_info, labels, verif); 308 + let v = build_detailed(profile_info, labels, verif, &self.cdn); 309 309 (k, v) 310 310 }) 311 311 .collect()
+10 -8
parakeet/src/loaders.rs
··· 11 11 use std::str::FromStr; 12 12 13 13 pub struct Dataloaders { 14 - pub embed: Loader<String, EmbedLoaderRet, EmbedLoader>, 14 + pub embed: Loader<String, (EmbedLoaderRet, String), EmbedLoader>, 15 15 pub feedgen: Loader<String, FeedGenLoaderRet, FeedGenLoader>, 16 16 pub handle: Loader<String, String, HandleLoader>, 17 17 pub label: LabelLoader, ··· 263 263 Record(models::PostEmbedRecord), 264 264 RecordWithMedia(models::PostEmbedRecord, Box<EmbedLoaderRet>), 265 265 } 266 - impl BatchFn<String, EmbedLoaderRet> for EmbedLoader { 267 - async fn load(&mut self, keys: &[String]) -> HashMap<String, EmbedLoaderRet> { 266 + impl BatchFn<String, (EmbedLoaderRet, String)> for EmbedLoader { 267 + async fn load(&mut self, keys: &[String]) -> HashMap<String, (EmbedLoaderRet, String)> { 268 268 let mut conn = self.0.get().await.unwrap(); 269 269 270 270 let res = schema::posts::table ··· 273 273 .left_join(schema::post_embed_record::table) 274 274 .select(( 275 275 schema::posts::at_uri, 276 + schema::posts::did, 276 277 schema::posts::embed.assume_not_null(), 277 278 schema::posts::embed_subtype, 278 279 Option::<models::PostEmbedVideo>::as_select(), ··· 287 288 .load::<( 288 289 String, 289 290 String, 291 + String, 290 292 Option<String>, 291 293 Option<models::PostEmbedVideo>, 292 294 Option<models::PostEmbedExt>, ··· 297 299 298 300 let image_post_uris = res 299 301 .iter() 300 - .filter_map(|(uri, embed, subtype, _, _, _)| { 302 + .filter_map(|(uri, _, embed, subtype, _, _, _)| { 301 303 (subtype.as_ref().unwrap_or(embed).as_str() == "app.bsky.embed.images") 302 304 .then_some(uri.clone()) 303 305 }) ··· 313 315 .into_group_map_by(|v| v.post_uri.clone()); 314 316 315 317 HashMap::from_iter(res.into_iter().filter_map( 316 - |(uri, embed, subtype, video, external, record)| { 318 + |(uri, did, embed, subtype, video, external, record)| { 317 319 let embed = match embed.as_str() { 318 320 "app.bsky.embed.images" => image_posts.remove(&uri).map(EmbedLoaderRet::Images), 319 - "app.bsky.embed.videos" => video.map(EmbedLoaderRet::Video), 321 + "app.bsky.embed.video" => video.map(EmbedLoaderRet::Video), 320 322 "app.bsky.embed.external" => external.map(EmbedLoaderRet::External), 321 323 "app.bsky.embed.record" => record.map(EmbedLoaderRet::Record), 322 324 "app.bsky.embed.recordWithMedia" => { ··· 325 327 "app.bsky.embed.images" => { 326 328 image_posts.remove(&uri).map(EmbedLoaderRet::Images) 327 329 } 328 - "app.bsky.embed.videos" => video.map(EmbedLoaderRet::Video), 330 + "app.bsky.embed.video" => video.map(EmbedLoaderRet::Video), 329 331 "app.bsky.embed.external" => external.map(EmbedLoaderRet::External), 330 332 _ => None, 331 333 } ··· 335 337 _ => None, 336 338 }?; 337 339 338 - Some((uri, embed)) 340 + Some((uri, (embed, did))) 339 341 }, 340 342 )) 341 343 }
+4
parakeet/src/main.rs
··· 17 17 pub dataloaders: Arc<loaders::Dataloaders>, 18 18 pub index_client: parakeet_index::Client, 19 19 pub jwt: Arc<xrpc::jwt::JwtVerifier>, 20 + pub cdn: Arc<xrpc::cdn::BskyCdn>, 20 21 } 21 22 22 23 #[tokio::main] ··· 43 44 resolver, 44 45 )); 45 46 47 + let cdn = Arc::new(xrpc::cdn::BskyCdn::new(conf.cdn.base, conf.cdn.video_base)); 48 + 46 49 #[allow(unused)] 47 50 hydration::TRUSTED_VERIFIERS.set(conf.trusted_verifiers); 48 51 ··· 65 68 dataloaders, 66 69 index_client, 67 70 jwt, 71 + cdn, 68 72 }); 69 73 70 74 let addr = std::net::SocketAddr::new(conf.server.bind_address.parse()?, conf.server.port);
+2 -2
parakeet/src/xrpc/app_bsky/actor.rs
··· 20 20 maybe_auth: Option<AtpAuth>, 21 21 Query(query): Query<ActorQuery>, 22 22 ) -> XrpcResult<Json<ProfileViewDetailed>> { 23 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 23 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 24 24 25 25 let did = get_actor_did(&state.dataloaders, query.actor).await?; 26 26 ··· 51 51 maybe_auth: Option<AtpAuth>, 52 52 ExtraQuery(query): ExtraQuery<ActorsQuery>, 53 53 ) -> XrpcResult<Json<GetProfilesRes>> { 54 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 54 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 55 55 56 56 let dids = get_actor_dids(&state.dataloaders, query.actors).await; 57 57
+3 -3
parakeet/src/xrpc/app_bsky/feed/feedgen.rs
··· 26 26 Query(query): Query<ActorWithCursorQuery>, 27 27 ) -> XrpcResult<Json<GetActorFeedRes>> { 28 28 let mut conn = state.pool.get().await?; 29 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 29 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 30 30 31 31 let did = get_actor_did(&state.dataloaders, query.actor).await?; 32 32 ··· 84 84 maybe_auth: Option<AtpAuth>, 85 85 Query(query): Query<GetFeedGeneratorQuery>, 86 86 ) -> XrpcResult<Json<GetFeedGeneratorRes>> { 87 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 87 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 88 88 89 89 let Some(view) = hyd.hydrate_feedgen(query.feed).await else { 90 90 return Err(Error::not_found()); ··· 114 114 maybe_auth: Option<AtpAuth>, 115 115 ExtraQuery(query): ExtraQuery<FeedsQuery>, 116 116 ) -> XrpcResult<Json<GetFeedGeneratorsRes>> { 117 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 117 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 118 118 119 119 let feeds = hyd 120 120 .hydrate_feedgens(query.feeds)
+2 -2
parakeet/src/xrpc/app_bsky/feed/likes.rs
··· 30 30 return Err(Error::not_found()); 31 31 } 32 32 33 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, Some(auth)); 33 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, Some(auth)); 34 34 35 35 let limit = query.limit.unwrap_or(50).clamp(1, 100); 36 36 ··· 97 97 Query(query): Query<GetLikesQuery>, 98 98 ) -> XrpcResult<Json<GetLikesRes>> { 99 99 let mut conn = state.pool.get().await?; 100 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 100 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 101 101 102 102 let uri = normalise_at_uri(&state.dataloaders, &query.uri).await?; 103 103
+6 -6
parakeet/src/xrpc/app_bsky/feed/posts.rs
··· 61 61 Query(query): Query<GetAuthorFeedQuery>, 62 62 ) -> XrpcResult<Json<FeedRes>> { 63 63 let mut conn = state.pool.get().await?; 64 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 64 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 65 65 66 66 let did = get_actor_did(&state.dataloaders, query.actor.clone()).await?; 67 67 ··· 136 136 Query(query): Query<ListWithCursorQuery>, 137 137 ) -> XrpcResult<Json<FeedRes>> { 138 138 let mut conn = state.pool.get().await?; 139 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 139 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 140 140 141 141 let limit = query.limit.unwrap_or(50).clamp(1, 100); 142 142 ··· 215 215 Query(query): Query<GetPostThreadQuery>, 216 216 ) -> XrpcResult<Json<GetPostThreadRes>> { 217 217 let mut conn = state.pool.get().await?; 218 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 218 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 219 219 220 220 let uri = normalise_at_uri(&state.dataloaders, &query.uri).await?; 221 221 let depth = query.depth.unwrap_or(6).clamp(0, 1000); ··· 313 313 maybe_auth: Option<AtpAuth>, 314 314 ExtraQuery(query): ExtraQuery<PostsQuery>, 315 315 ) -> XrpcResult<Json<PostsRes>> { 316 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 316 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 317 317 let posts = hyd.hydrate_posts(query.uris).await; 318 318 319 319 Ok(Json(PostsRes { ··· 346 346 Query(query): Query<GetQuotesQuery>, 347 347 ) -> XrpcResult<Json<GetQuotesRes>> { 348 348 let mut conn = state.pool.get().await?; 349 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 349 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 350 350 351 351 let limit = query.limit.unwrap_or(50).clamp(1, 100); 352 352 ··· 411 411 Query(query): Query<GetRepostedByQuery>, 412 412 ) -> XrpcResult<Json<GetRepostedByRes>> { 413 413 let mut conn = state.pool.get().await?; 414 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 414 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 415 415 416 416 let uri = normalise_at_uri(&state.dataloaders, &query.uri).await?; 417 417
+2 -2
parakeet/src/xrpc/app_bsky/graph/lists.rs
··· 32 32 Query(query): Query<ActorWithCursorQuery>, 33 33 ) -> XrpcResult<Json<AppBskyGraphGetListsRes>> { 34 34 let mut conn = state.pool.get().await?; 35 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 35 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 36 36 37 37 let did = get_actor_did(&state.dataloaders, query.actor).await?; 38 38 ··· 86 86 Query(query): Query<ListWithCursorQuery>, 87 87 ) -> XrpcResult<Json<AppBskyGraphGetListRes>> { 88 88 let mut conn = state.pool.get().await?; 89 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 89 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 90 90 91 91 let Some(list) = hyd.hydrate_list(query.list).await else { 92 92 return Err(Error::not_found());
+2 -2
parakeet/src/xrpc/app_bsky/graph/relations.rs
··· 26 26 Query(query): Query<ActorWithCursorQuery>, 27 27 ) -> XrpcResult<Json<AppBskyGraphGetFollowersRes>> { 28 28 let mut conn = state.pool.get().await?; 29 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 29 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 30 30 31 31 let subj_did = get_actor_did(&state.dataloaders, query.actor).await?; 32 32 ··· 86 86 Query(query): Query<ActorWithCursorQuery>, 87 87 ) -> XrpcResult<Json<AppBskyGraphGetFollowsRes>> { 88 88 let mut conn = state.pool.get().await?; 89 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 89 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 90 90 91 91 let subj_did = get_actor_did(&state.dataloaders, query.actor).await?; 92 92
+3 -3
parakeet/src/xrpc/app_bsky/graph/starter_packs.rs
··· 27 27 Query(query): Query<ActorWithCursorQuery>, 28 28 ) -> XrpcResult<Json<StarterPacksRes>> { 29 29 let mut conn = state.pool.get().await?; 30 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 30 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 31 31 32 32 let subj_did = get_actor_did(&state.dataloaders, query.actor).await?; 33 33 ··· 90 90 maybe_auth: Option<AtpAuth>, 91 91 Query(query): Query<GetStarterPackQuery>, 92 92 ) -> XrpcResult<Json<GetStarterPackRes>> { 93 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 93 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 94 94 95 95 let Some(starter_pack) = hyd.hydrate_starterpack(query.starter_pack).await else { 96 96 return Err(Error::not_found()); ··· 110 110 maybe_auth: Option<AtpAuth>, 111 111 ExtraQuery(query): ExtraQuery<GetStarterPacksQuery>, 112 112 ) -> XrpcResult<Json<StarterPacksRes>> { 113 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 113 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 114 114 115 115 let starter_packs = hyd 116 116 .hydrate_starterpacks_basic(query.uris)
+1 -1
parakeet/src/xrpc/app_bsky/labeler.rs
··· 35 35 maybe_auth: Option<AtpAuth>, 36 36 ExtraQuery(query): ExtraQuery<GetServicesQuery>, 37 37 ) -> XrpcResult<Json<GetServicesRes>> { 38 - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); 38 + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); 39 39 40 40 let views = match query.detailed { 41 41 true => hyd
+35
parakeet/src/xrpc/cdn.rs
··· 1 + /// For a CDN that uses paths identically to Bluesky 2 + pub struct BskyCdn { 3 + cdn_base: String, 4 + video_base: String, 5 + } 6 + 7 + impl BskyCdn { 8 + pub fn new(cdn_base: String, video_base: String) -> Self { 9 + BskyCdn {cdn_base, video_base} 10 + } 11 + 12 + pub fn avatar(&self, did: &str, cid: &str) -> String { 13 + format!("{}/img/avatar/plain/{did}/{cid}@jpeg", self.cdn_base) 14 + } 15 + 16 + pub fn banner(&self, did: &str, cid: &str) -> String { 17 + format!("{}/img/banner/plain/{did}/{cid}@jpeg", self.cdn_base) 18 + } 19 + 20 + pub fn embed_thumb(&self, did: &str, cid: &str) -> String { 21 + format!("{}/img/feed_thumbnail/plain/{did}/{cid}@jpeg", self.cdn_base) 22 + } 23 + 24 + pub fn embed_fullsize(&self, did: &str, cid: &str) -> String { 25 + format!("{}/img/feed_fullsize/plain/{did}/{cid}@jpeg", self.cdn_base) 26 + } 27 + 28 + pub fn video_thumb(&self, did: &str, cid: &str) -> String { 29 + format!("{}/watch/{did}/{cid}/thumbnail.jpg", self.video_base) 30 + } 31 + 32 + pub fn video_playlist(&self, did: &str, cid: &str) -> String { 33 + format!("{}/watch/{did}/{cid}/playlist.m3u8", self.video_base) 34 + } 35 + }
+1
parakeet/src/xrpc/mod.rs
··· 6 6 use std::str::FromStr; 7 7 8 8 mod app_bsky; 9 + pub mod cdn; 9 10 mod com_atproto; 10 11 mod error; 11 12 pub mod extract;