From 9c826c07290e1ff74694bb772fe8aee774c6b6d3 Mon Sep 17 00:00:00 2001 From: Mia Date: Sat, 21 Jun 2025 17:31:20 +0000 Subject: [PATCH] feat(parakeet): bsky media CDN --- parakeet/src/config.rs | 27 +++++++++++ parakeet/src/hydration/embed.rs | 29 ++++++------ parakeet/src/hydration/feedgen.rs | 15 ++++-- parakeet/src/hydration/list.rs | 21 +++++---- parakeet/src/hydration/mod.rs | 4 ++ parakeet/src/hydration/profile.rs | 46 +++++++++---------- parakeet/src/loaders.rs | 18 ++++---- parakeet/src/main.rs | 4 ++ parakeet/src/xrpc/app_bsky/actor.rs | 4 +- parakeet/src/xrpc/app_bsky/feed/feedgen.rs | 6 +-- parakeet/src/xrpc/app_bsky/feed/likes.rs | 4 +- parakeet/src/xrpc/app_bsky/feed/posts.rs | 12 ++--- parakeet/src/xrpc/app_bsky/graph/lists.rs | 4 +- parakeet/src/xrpc/app_bsky/graph/relations.rs | 4 +- .../src/xrpc/app_bsky/graph/starter_packs.rs | 6 +-- parakeet/src/xrpc/app_bsky/labeler.rs | 2 +- parakeet/src/xrpc/cdn.rs | 35 ++++++++++++++ parakeet/src/xrpc/mod.rs | 1 + 18 files changed, 160 insertions(+), 82 deletions(-) create mode 100644 parakeet/src/xrpc/cdn.rs diff --git a/parakeet/src/config.rs b/parakeet/src/config.rs index 91ef65aa..32492d0e 100644 --- a/parakeet/src/config.rs +++ b/parakeet/src/config.rs @@ -21,6 +21,8 @@ pub struct Config { #[serde(default)] pub trusted_verifiers: Vec, pub plc_directory: Option, + #[serde(default)] + pub cdn: ConfigCdn, } #[derive(Debug, Deserialize)] @@ -54,3 +56,28 @@ fn default_bind_address() -> String { fn default_port() -> u16 { 6000 } + +#[derive(Debug, Deserialize)] +pub struct ConfigCdn { + #[serde(default = "default_cdn_base")] + pub base: String, + #[serde(default = "default_cdn_video_base")] + pub video_base: String, +} + +impl Default for ConfigCdn { + fn default() -> Self { + ConfigCdn { + base: default_cdn_base(), + video_base: default_cdn_video_base(), + } + } +} + +fn default_cdn_base() -> String { + "https://cdn.bsky.app".to_string() +} + +fn default_cdn_video_base() -> String { + "https://video.bsky.app".to_string() +} diff --git a/parakeet/src/hydration/embed.rs b/parakeet/src/hydration/embed.rs index 43807fdf..40f5789b 100644 --- a/parakeet/src/hydration/embed.rs +++ b/parakeet/src/hydration/embed.rs @@ -1,5 +1,6 @@ use crate::hydration::StatefulHydrator; use crate::loaders::EmbedLoaderRet; +use crate::xrpc::cdn::BskyCdn; use itertools::Itertools; use lexica::app_bsky::embed::{ AspectRatio, Embed, External, ImageView, RecordView, RecordViewInner, RecordWrapper, VideoView, @@ -27,22 +28,22 @@ fn build_record_view(post: PostView) -> RecordView { } } -fn build_embed(embed: EmbedLoaderRet) -> Embed { +fn build_embed(embed: EmbedLoaderRet, did: &str, cdn: &BskyCdn) -> Embed { match embed { EmbedLoaderRet::Images(images) => Embed::Images { images: images .into_iter() .map(|img| ImageView { - thumb: format!("https://localhost/postimg_thumb/{}", img.cid), - fullsize: format!("https://localhost/postimg/{}", img.cid), + thumb: cdn.embed_thumb(did, &img.cid), + fullsize: cdn.embed_fullsize(did, &img.cid), alt: img.alt.unwrap_or_default(), aspect_ratio: build_aspect_ratio(img.height, img.width), }) .collect(), }, EmbedLoaderRet::Video(video) => Embed::Video(VideoView { - playlist: format!("https://localhost/video/{}", &video.cid), - thumbnail: Some(format!("https://localhost/videothumb/{}", &video.cid)), + playlist: cdn.video_playlist(did, &video.cid), + thumbnail: Some(cdn.video_thumb(did, &video.cid)), cid: video.cid, alt: video.alt, aspect_ratio: build_aspect_ratio(video.height, video.width), @@ -52,9 +53,7 @@ fn build_embed(embed: EmbedLoaderRet) -> Embed { uri: external.uri, title: external.title, description: external.description, - thumb: external - .thumb_cid - .map(|v| format!("https://localhost/embed/{v}")), + thumb: external.thumb_cid.map(|cid| cdn.embed_thumb(did, &cid)), }, }, _ => unreachable!(), @@ -178,7 +177,7 @@ impl StatefulHydrator<'_> { } pub async fn hydrate_embed(&self, post: String) -> Option { - let embed = self.loaders.embed.load(post).await?; + let (embed, author) = self.loaders.embed.load(post).await?; match embed { EmbedLoaderRet::Record(record) => self @@ -190,9 +189,9 @@ impl StatefulHydrator<'_> { .await .map(|record| Embed::RecordWithMedia { record: RecordWrapper { record }, - media: Box::new(build_embed(*media)), + media: Box::new(build_embed(*media, &author, &self.cdn)), }), - _ => Some(build_embed(embed)), + _ => Some(build_embed(embed, &author, &self.cdn)), } } @@ -201,7 +200,7 @@ impl StatefulHydrator<'_> { let with_records = embeds .values() - .filter_map(|v| match v { + .filter_map(|(v, _)| match v { EmbedLoaderRet::Record(rec) => Some(rec), EmbedLoaderRet::RecordWithMedia(rec, _) => Some(rec), _ => None, @@ -212,7 +211,7 @@ impl StatefulHydrator<'_> { embeds .into_iter() - .filter_map(|(k, v)| { + .filter_map(|(k, (v, author))| { let embed = match v { EmbedLoaderRet::Record(record) => { let record = records.get(&record.uri).cloned().unwrap_or( @@ -234,10 +233,10 @@ impl StatefulHydrator<'_> { Some(Embed::RecordWithMedia { record: RecordWrapper { record }, - media: Box::new(build_embed(*media)), + media: Box::new(build_embed(*media, &author, &self.cdn)), }) } - _ => Some(build_embed(v)), + _ => Some(build_embed(v, &author, &self.cdn)), }?; Some((k, embed)) diff --git a/parakeet/src/hydration/feedgen.rs b/parakeet/src/hydration/feedgen.rs index 2818dbf9..0f61698a 100644 --- a/parakeet/src/hydration/feedgen.rs +++ b/parakeet/src/hydration/feedgen.rs @@ -1,4 +1,5 @@ use crate::hydration::map_labels; +use crate::xrpc::cdn::BskyCdn; use lexica::app_bsky::actor::ProfileView; use lexica::app_bsky::feed::{GeneratorContentMode, GeneratorView}; use parakeet_db::models; @@ -10,6 +11,7 @@ fn build_feedgen( creator: ProfileView, labels: Vec, likes: Option, + cdn: &BskyCdn, ) -> GeneratorView { let content_mode = feedgen .content_mode @@ -19,6 +21,8 @@ fn build_feedgen( .description_facets .and_then(|v| serde_json::from_value(v).ok()); + let avatar = feedgen.avatar_cid.map(|cid| cdn.avatar(&creator.did, &cid)); + GeneratorView { uri: feedgen.at_uri, cid: feedgen.cid, @@ -27,9 +31,7 @@ fn build_feedgen( display_name: feedgen.name, description: feedgen.description, description_facets, - avatar: feedgen - .avatar_cid - .map(|v| format!("https://localhost/feedgen/{v}")), + avatar, like_count: likes.unwrap_or_default() as i64, accepts_interactions: feedgen.accepts_interactions, labels: map_labels(labels), @@ -44,7 +46,7 @@ impl super::StatefulHydrator<'_> { let (feedgen, likes) = self.loaders.feedgen.load(feedgen).await?; let profile = self.hydrate_profile(feedgen.owner.clone()).await?; - Some(build_feedgen(feedgen, profile, labels, likes)) + Some(build_feedgen(feedgen, profile, labels, likes, &self.cdn)) } pub async fn hydrate_feedgens(&self, feedgens: Vec) -> HashMap { @@ -64,7 +66,10 @@ impl super::StatefulHydrator<'_> { let creator = creators.get(&feedgen.owner).cloned()?; let labels = labels.get(&uri).cloned().unwrap_or_default(); - Some((uri, build_feedgen(feedgen, creator, labels, likes))) + Some(( + uri, + build_feedgen(feedgen, creator, labels, likes, &self.cdn), + )) }) .collect() } diff --git a/parakeet/src/hydration/list.rs b/parakeet/src/hydration/list.rs index 409abf32..149b2869 100644 --- a/parakeet/src/hydration/list.rs +++ b/parakeet/src/hydration/list.rs @@ -1,4 +1,5 @@ use crate::hydration::{map_labels, StatefulHydrator}; +use crate::xrpc::cdn::BskyCdn; use lexica::app_bsky::actor::ProfileView; use lexica::app_bsky::graph::{ListPurpose, ListView, ListViewBasic}; use parakeet_db::models; @@ -9,17 +10,17 @@ fn build_basic( list: models::List, list_item_count: i64, labels: Vec, + cdn: &BskyCdn, ) -> Option { let purpose = ListPurpose::from_str(&list.list_type).ok()?; + let avatar = list.avatar_cid.map(|cid| cdn.avatar(&list.owner, &cid)); Some(ListViewBasic { uri: list.at_uri, cid: list.cid, name: list.name, purpose, - avatar: list - .avatar_cid - .map(|v| format!("https://localhost/list/{v}")), + avatar, list_item_count, labels: map_labels(labels), indexed_at: list.indexed_at, @@ -31,8 +32,10 @@ fn build_listview( list_item_count: i64, creator: ProfileView, labels: Vec, + cdn: &BskyCdn, ) -> Option { let purpose = ListPurpose::from_str(&list.list_type).ok()?; + let avatar = list.avatar_cid.map(|cid| cdn.avatar(&list.owner, &cid)); let description_facets = list .description_facets @@ -46,9 +49,7 @@ fn build_listview( purpose, description: list.description, description_facets, - avatar: list - .avatar_cid - .map(|v| format!("https://localhost/list/{v}")), + avatar, list_item_count, labels: map_labels(labels), indexed_at: list.indexed_at, @@ -60,7 +61,7 @@ impl StatefulHydrator<'_> { let labels = self.get_label(&list).await; let (list, count) = self.loaders.list.load(list).await?; - build_basic(list, count, labels) + build_basic(list, count, labels, &self.cdn) } pub async fn hydrate_lists_basic(&self, lists: Vec) -> HashMap { @@ -72,7 +73,7 @@ impl StatefulHydrator<'_> { .filter_map(|(uri, (list, count))| { let labels = labels.get(&uri).cloned().unwrap_or_default(); - build_basic(list, count, labels).map(|v| (uri, v)) + build_basic(list, count, labels, &self.cdn).map(|v| (uri, v)) }) .collect() } @@ -82,7 +83,7 @@ impl StatefulHydrator<'_> { let (list, count) = self.loaders.list.load(list).await?; let profile = self.hydrate_profile(list.owner.clone()).await?; - build_listview(list, count, profile, labels) + build_listview(list, count, profile, labels, &self.cdn) } pub async fn hydrate_lists(&self, lists: Vec) -> HashMap { @@ -98,7 +99,7 @@ impl StatefulHydrator<'_> { let creator = creators.get(&list.owner)?; let labels = labels.get(&uri).cloned().unwrap_or_default(); - build_listview(list, count, creator.to_owned(), labels).map(|v| (uri, v)) + build_listview(list, count, creator.to_owned(), labels, &self.cdn).map(|v| (uri, v)) }) .collect() } diff --git a/parakeet/src/hydration/mod.rs b/parakeet/src/hydration/mod.rs index 81d4faaf..e2f8f641 100644 --- a/parakeet/src/hydration/mod.rs +++ b/parakeet/src/hydration/mod.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] use crate::loaders::Dataloaders; +use crate::xrpc::cdn::BskyCdn; use crate::xrpc::extract::LabelConfigItem; use std::collections::HashMap; use std::sync::Arc; @@ -39,11 +40,13 @@ pub struct StatefulHydrator<'a> { loaders: Arc, accept_labelers: &'a [LabelConfigItem], current_actor: Option, + cdn: Arc, } impl StatefulHydrator<'_> { pub fn new<'a>( loaders: &Arc, + cdn: &Arc, accept_labelers: &'a [LabelConfigItem], current_actor: Option, ) -> StatefulHydrator<'a> { @@ -51,6 +54,7 @@ impl StatefulHydrator<'_> { loaders: loaders.clone(), accept_labelers, current_actor: current_actor.map(|v| v.0), + cdn: cdn.clone(), } } diff --git a/parakeet/src/hydration/profile.rs b/parakeet/src/hydration/profile.rs index d4133e02..3be5487b 100644 --- a/parakeet/src/hydration/profile.rs +++ b/parakeet/src/hydration/profile.rs @@ -1,5 +1,6 @@ use crate::hydration::map_labels; use crate::loaders::ProfileLoaderRet; +use crate::xrpc::cdn::BskyCdn; use chrono::prelude::*; use chrono::TimeDelta; use lexica::app_bsky::actor::*; @@ -114,7 +115,7 @@ fn build_verification( } } -fn build_status(status: models::Status) -> Option { +fn build_status(status: models::Status, cdn: &BskyCdn) -> Option { let s = Status::from_str(&status.status).ok()?; let embed = status .embed_uri @@ -127,7 +128,7 @@ fn build_status(status: models::Status) -> Option { description, thumb: status .thumb_cid - .map(|v| format!("https://localhost/embed/{v}")), + .map(|cid| cdn.embed_thumb(&status.did, &cid)), }, }); @@ -150,18 +151,18 @@ fn build_basic( (handle, profile, chat_decl, is_labeler, stats, status): ProfileLoaderRet, labels: Vec, verifications: Option>, + cdn: &BskyCdn, ) -> ProfileViewBasic { let associated = build_associated(chat_decl, is_labeler, stats); let verification = build_verification(&profile, &handle, verifications); - let status = status.and_then(build_status); + let status = status.and_then(|status| build_status(status, cdn)); + let avatar = profile.avatar_cid.map(|cid| cdn.avatar(&profile.did, &cid)); ProfileViewBasic { did: profile.did, handle: handle.unwrap_or("handle.invalid".to_string()), display_name: profile.display_name, - avatar: profile - .avatar_cid - .map(|v| format!("https://localhost/avatar/{v}")), + avatar, associated, labels: map_labels(labels), verification, @@ -174,19 +175,19 @@ fn build_profile( (handle, profile, chat_decl, is_labeler, stats, status): ProfileLoaderRet, labels: Vec, verifications: Option>, + cdn: &BskyCdn, ) -> ProfileView { let associated = build_associated(chat_decl, is_labeler, stats); let verification = build_verification(&profile, &handle, verifications); - let status = status.and_then(build_status); + let status = status.and_then(|status| build_status(status, cdn)); + let avatar = profile.avatar_cid.map(|cid| cdn.avatar(&profile.did, &cid)); ProfileView { did: profile.did, handle: handle.unwrap_or("handle.invalid".to_string()), display_name: profile.display_name, description: profile.description, - avatar: profile - .avatar_cid - .map(|v| format!("https://localhost/avatar/{v}")), + avatar, associated, labels: map_labels(labels), verification, @@ -200,22 +201,21 @@ fn build_detailed( (handle, profile, chat_decl, is_labeler, stats, status): ProfileLoaderRet, labels: Vec, verifications: Option>, + cdn: &BskyCdn, ) -> ProfileViewDetailed { let associated = build_associated(chat_decl, is_labeler, stats); let verification = build_verification(&profile, &handle, verifications); - let status = status.and_then(build_status); + let status = status.and_then(|status| build_status(status, cdn)); + let avatar = profile.avatar_cid.map(|cid| cdn.avatar(&profile.did, &cid)); + let banner = profile.banner_cid.map(|cid| cdn.banner(&profile.did, &cid)); ProfileViewDetailed { did: profile.did, handle: handle.unwrap_or("handle.invalid".to_string()), display_name: profile.display_name, description: profile.description, - avatar: profile - .avatar_cid - .map(|v| format!("https://localhost/avatar/{v}")), - banner: profile - .banner_cid - .map(|v| format!("https://localhost/banner/{v}")), + avatar, + banner, followers_count: stats.map(|v| v.followers as i64).unwrap_or_default(), follows_count: stats.map(|v| v.following as i64).unwrap_or_default(), associated, @@ -233,7 +233,7 @@ impl super::StatefulHydrator<'_> { let verif = self.loaders.verification.load(did.clone()).await; let profile_info = self.loaders.profile.load(did).await?; - Some(build_basic(profile_info, labels, verif)) + Some(build_basic(profile_info, labels, verif, &self.cdn)) } pub async fn hydrate_profiles_basic( @@ -250,7 +250,7 @@ impl super::StatefulHydrator<'_> { let labels = labels.get(&k).cloned().unwrap_or_default(); let verif = verif.get(&k).cloned(); - let v = build_basic(profile_info, labels, verif); + let v = build_basic(profile_info, labels, verif, &self.cdn); (k, v) }) .collect() @@ -262,7 +262,7 @@ impl super::StatefulHydrator<'_> { let verif = self.loaders.verification.load(did.clone()).await; let profile_info = self.loaders.profile.load(did).await?; - Some(build_profile(profile_info, labels, verif)) + Some(build_profile(profile_info, labels, verif, &self.cdn)) } pub async fn hydrate_profiles(&self, dids: Vec) -> HashMap { @@ -276,7 +276,7 @@ impl super::StatefulHydrator<'_> { let labels = labels.get(&k).cloned().unwrap_or_default(); let verif = verif.get(&k).cloned(); - let v = build_profile(profile_info, labels, verif); + let v = build_profile(profile_info, labels, verif, &self.cdn); (k, v) }) .collect() @@ -288,7 +288,7 @@ impl super::StatefulHydrator<'_> { let verif = self.loaders.verification.load(did.clone()).await; let profile_info = self.loaders.profile.load(did).await?; - Some(build_detailed(profile_info, labels, verif)) + Some(build_detailed(profile_info, labels, verif, &self.cdn)) } pub async fn hydrate_profiles_detailed( @@ -305,7 +305,7 @@ impl super::StatefulHydrator<'_> { let labels = labels.get(&k).cloned().unwrap_or_default(); let verif = verif.get(&k).cloned(); - let v = build_detailed(profile_info, labels, verif); + let v = build_detailed(profile_info, labels, verif, &self.cdn); (k, v) }) .collect() diff --git a/parakeet/src/loaders.rs b/parakeet/src/loaders.rs index 73cb067e..15fdb190 100644 --- a/parakeet/src/loaders.rs +++ b/parakeet/src/loaders.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use std::str::FromStr; pub struct Dataloaders { - pub embed: Loader, + pub embed: Loader, pub feedgen: Loader, pub handle: Loader, pub label: LabelLoader, @@ -263,8 +263,8 @@ pub enum EmbedLoaderRet { Record(models::PostEmbedRecord), RecordWithMedia(models::PostEmbedRecord, Box), } -impl BatchFn for EmbedLoader { - async fn load(&mut self, keys: &[String]) -> HashMap { +impl BatchFn for EmbedLoader { + async fn load(&mut self, keys: &[String]) -> HashMap { let mut conn = self.0.get().await.unwrap(); let res = schema::posts::table @@ -273,6 +273,7 @@ impl BatchFn for EmbedLoader { .left_join(schema::post_embed_record::table) .select(( schema::posts::at_uri, + schema::posts::did, schema::posts::embed.assume_not_null(), schema::posts::embed_subtype, Option::::as_select(), @@ -285,6 +286,7 @@ impl BatchFn for EmbedLoader { .and(schema::posts::embed.is_not_null()), ) .load::<( + String, String, String, Option, @@ -297,7 +299,7 @@ impl BatchFn for EmbedLoader { let image_post_uris = res .iter() - .filter_map(|(uri, embed, subtype, _, _, _)| { + .filter_map(|(uri, _, embed, subtype, _, _, _)| { (subtype.as_ref().unwrap_or(embed).as_str() == "app.bsky.embed.images") .then_some(uri.clone()) }) @@ -313,10 +315,10 @@ impl BatchFn for EmbedLoader { .into_group_map_by(|v| v.post_uri.clone()); HashMap::from_iter(res.into_iter().filter_map( - |(uri, embed, subtype, video, external, record)| { + |(uri, did, embed, subtype, video, external, record)| { let embed = match embed.as_str() { "app.bsky.embed.images" => image_posts.remove(&uri).map(EmbedLoaderRet::Images), - "app.bsky.embed.videos" => video.map(EmbedLoaderRet::Video), + "app.bsky.embed.video" => video.map(EmbedLoaderRet::Video), "app.bsky.embed.external" => external.map(EmbedLoaderRet::External), "app.bsky.embed.record" => record.map(EmbedLoaderRet::Record), "app.bsky.embed.recordWithMedia" => { @@ -325,7 +327,7 @@ impl BatchFn for EmbedLoader { "app.bsky.embed.images" => { image_posts.remove(&uri).map(EmbedLoaderRet::Images) } - "app.bsky.embed.videos" => video.map(EmbedLoaderRet::Video), + "app.bsky.embed.video" => video.map(EmbedLoaderRet::Video), "app.bsky.embed.external" => external.map(EmbedLoaderRet::External), _ => None, } @@ -335,7 +337,7 @@ impl BatchFn for EmbedLoader { _ => None, }?; - Some((uri, embed)) + Some((uri, (embed, did))) }, )) } diff --git a/parakeet/src/main.rs b/parakeet/src/main.rs index 270e4b7e..81c910d4 100644 --- a/parakeet/src/main.rs +++ b/parakeet/src/main.rs @@ -17,6 +17,7 @@ pub struct GlobalState { pub dataloaders: Arc, pub index_client: parakeet_index::Client, pub jwt: Arc, + pub cdn: Arc, } #[tokio::main] @@ -43,6 +44,8 @@ async fn main() -> eyre::Result<()> { resolver, )); + let cdn = Arc::new(xrpc::cdn::BskyCdn::new(conf.cdn.base, conf.cdn.video_base)); + #[allow(unused)] hydration::TRUSTED_VERIFIERS.set(conf.trusted_verifiers); @@ -65,6 +68,7 @@ async fn main() -> eyre::Result<()> { dataloaders, index_client, jwt, + cdn, }); let addr = std::net::SocketAddr::new(conf.server.bind_address.parse()?, conf.server.port); diff --git a/parakeet/src/xrpc/app_bsky/actor.rs b/parakeet/src/xrpc/app_bsky/actor.rs index 597f5703..806cb9dd 100644 --- a/parakeet/src/xrpc/app_bsky/actor.rs +++ b/parakeet/src/xrpc/app_bsky/actor.rs @@ -20,7 +20,7 @@ pub async fn get_profile( maybe_auth: Option, Query(query): Query, ) -> XrpcResult> { - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let did = get_actor_did(&state.dataloaders, query.actor).await?; @@ -51,7 +51,7 @@ pub async fn get_profiles( maybe_auth: Option, ExtraQuery(query): ExtraQuery, ) -> XrpcResult> { - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let dids = get_actor_dids(&state.dataloaders, query.actors).await; diff --git a/parakeet/src/xrpc/app_bsky/feed/feedgen.rs b/parakeet/src/xrpc/app_bsky/feed/feedgen.rs index cab3e096..b34a9090 100644 --- a/parakeet/src/xrpc/app_bsky/feed/feedgen.rs +++ b/parakeet/src/xrpc/app_bsky/feed/feedgen.rs @@ -26,7 +26,7 @@ pub async fn get_actor_feeds( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let did = get_actor_did(&state.dataloaders, query.actor).await?; @@ -84,7 +84,7 @@ pub async fn get_feed_generator( maybe_auth: Option, Query(query): Query, ) -> XrpcResult> { - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let Some(view) = hyd.hydrate_feedgen(query.feed).await else { return Err(Error::not_found()); @@ -114,7 +114,7 @@ pub async fn get_feed_generators( maybe_auth: Option, ExtraQuery(query): ExtraQuery, ) -> XrpcResult> { - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let feeds = hyd .hydrate_feedgens(query.feeds) diff --git a/parakeet/src/xrpc/app_bsky/feed/likes.rs b/parakeet/src/xrpc/app_bsky/feed/likes.rs index 2dd83423..87ac4b79 100644 --- a/parakeet/src/xrpc/app_bsky/feed/likes.rs +++ b/parakeet/src/xrpc/app_bsky/feed/likes.rs @@ -30,7 +30,7 @@ pub async fn get_actor_likes( return Err(Error::not_found()); } - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, Some(auth)); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, Some(auth)); let limit = query.limit.unwrap_or(50).clamp(1, 100); @@ -97,7 +97,7 @@ pub async fn get_likes( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let uri = normalise_at_uri(&state.dataloaders, &query.uri).await?; diff --git a/parakeet/src/xrpc/app_bsky/feed/posts.rs b/parakeet/src/xrpc/app_bsky/feed/posts.rs index 5b2ba132..57469eb9 100644 --- a/parakeet/src/xrpc/app_bsky/feed/posts.rs +++ b/parakeet/src/xrpc/app_bsky/feed/posts.rs @@ -61,7 +61,7 @@ pub async fn get_author_feed( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let did = get_actor_did(&state.dataloaders, query.actor.clone()).await?; @@ -136,7 +136,7 @@ pub async fn get_list_feed( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let limit = query.limit.unwrap_or(50).clamp(1, 100); @@ -215,7 +215,7 @@ pub async fn get_post_thread( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let uri = normalise_at_uri(&state.dataloaders, &query.uri).await?; let depth = query.depth.unwrap_or(6).clamp(0, 1000); @@ -313,7 +313,7 @@ pub async fn get_posts( maybe_auth: Option, ExtraQuery(query): ExtraQuery, ) -> XrpcResult> { - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let posts = hyd.hydrate_posts(query.uris).await; Ok(Json(PostsRes { @@ -346,7 +346,7 @@ pub async fn get_quotes( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let limit = query.limit.unwrap_or(50).clamp(1, 100); @@ -411,7 +411,7 @@ pub async fn get_reposted_by( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let uri = normalise_at_uri(&state.dataloaders, &query.uri).await?; diff --git a/parakeet/src/xrpc/app_bsky/graph/lists.rs b/parakeet/src/xrpc/app_bsky/graph/lists.rs index c4e3d3dd..07577999 100644 --- a/parakeet/src/xrpc/app_bsky/graph/lists.rs +++ b/parakeet/src/xrpc/app_bsky/graph/lists.rs @@ -32,7 +32,7 @@ pub async fn get_lists( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let did = get_actor_did(&state.dataloaders, query.actor).await?; @@ -86,7 +86,7 @@ pub async fn get_list( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let Some(list) = hyd.hydrate_list(query.list).await else { return Err(Error::not_found()); diff --git a/parakeet/src/xrpc/app_bsky/graph/relations.rs b/parakeet/src/xrpc/app_bsky/graph/relations.rs index 11fdf54b..eef45c4d 100644 --- a/parakeet/src/xrpc/app_bsky/graph/relations.rs +++ b/parakeet/src/xrpc/app_bsky/graph/relations.rs @@ -26,7 +26,7 @@ pub async fn get_followers( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let subj_did = get_actor_did(&state.dataloaders, query.actor).await?; @@ -86,7 +86,7 @@ pub async fn get_follows( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let subj_did = get_actor_did(&state.dataloaders, query.actor).await?; diff --git a/parakeet/src/xrpc/app_bsky/graph/starter_packs.rs b/parakeet/src/xrpc/app_bsky/graph/starter_packs.rs index 1fa19038..82d91d0c 100644 --- a/parakeet/src/xrpc/app_bsky/graph/starter_packs.rs +++ b/parakeet/src/xrpc/app_bsky/graph/starter_packs.rs @@ -27,7 +27,7 @@ pub async fn get_actor_starter_packs( Query(query): Query, ) -> XrpcResult> { let mut conn = state.pool.get().await?; - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let subj_did = get_actor_did(&state.dataloaders, query.actor).await?; @@ -90,7 +90,7 @@ pub async fn get_starter_pack( maybe_auth: Option, Query(query): Query, ) -> XrpcResult> { - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let Some(starter_pack) = hyd.hydrate_starterpack(query.starter_pack).await else { return Err(Error::not_found()); @@ -110,7 +110,7 @@ pub async fn get_starter_packs( maybe_auth: Option, ExtraQuery(query): ExtraQuery, ) -> XrpcResult> { - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let starter_packs = hyd .hydrate_starterpacks_basic(query.uris) diff --git a/parakeet/src/xrpc/app_bsky/labeler.rs b/parakeet/src/xrpc/app_bsky/labeler.rs index 17e83d3d..ee139573 100644 --- a/parakeet/src/xrpc/app_bsky/labeler.rs +++ b/parakeet/src/xrpc/app_bsky/labeler.rs @@ -35,7 +35,7 @@ pub async fn get_services( maybe_auth: Option, ExtraQuery(query): ExtraQuery, ) -> XrpcResult> { - let hyd = StatefulHydrator::new(&state.dataloaders, &labelers, maybe_auth); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); let views = match query.detailed { true => hyd diff --git a/parakeet/src/xrpc/cdn.rs b/parakeet/src/xrpc/cdn.rs new file mode 100644 index 00000000..e87c9615 --- /dev/null +++ b/parakeet/src/xrpc/cdn.rs @@ -0,0 +1,35 @@ +/// For a CDN that uses paths identically to Bluesky +pub struct BskyCdn { + cdn_base: String, + video_base: String, +} + +impl BskyCdn { + pub fn new(cdn_base: String, video_base: String) -> Self { + BskyCdn {cdn_base, video_base} + } + + pub fn avatar(&self, did: &str, cid: &str) -> String { + format!("{}/img/avatar/plain/{did}/{cid}@jpeg", self.cdn_base) + } + + pub fn banner(&self, did: &str, cid: &str) -> String { + format!("{}/img/banner/plain/{did}/{cid}@jpeg", self.cdn_base) + } + + pub fn embed_thumb(&self, did: &str, cid: &str) -> String { + format!("{}/img/feed_thumbnail/plain/{did}/{cid}@jpeg", self.cdn_base) + } + + pub fn embed_fullsize(&self, did: &str, cid: &str) -> String { + format!("{}/img/feed_fullsize/plain/{did}/{cid}@jpeg", self.cdn_base) + } + + pub fn video_thumb(&self, did: &str, cid: &str) -> String { + format!("{}/watch/{did}/{cid}/thumbnail.jpg", self.video_base) + } + + pub fn video_playlist(&self, did: &str, cid: &str) -> String { + format!("{}/watch/{did}/{cid}/playlist.m3u8", self.video_base) + } +} diff --git a/parakeet/src/xrpc/mod.rs b/parakeet/src/xrpc/mod.rs index a88ab1d4..8af323a0 100644 --- a/parakeet/src/xrpc/mod.rs +++ b/parakeet/src/xrpc/mod.rs @@ -6,6 +6,7 @@ use serde::Deserialize; use std::str::FromStr; mod app_bsky; +pub mod cdn; mod com_atproto; mod error; pub mod extract; -- 2.51.2