ceres: a small planet in a giant solar system
33
fork

Configure Feed

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

i think that's the rest of the views with media

+227 -34
+227 -34
src/server/xrpc/app_bsky_feed.rs
··· 2 2 use crate::state::AppState; 3 3 use axum::{Json, Router, extract::State}; 4 4 use jacquard::{ 5 + IntoStatic, 6 + client::AgentSessionExt, 7 + deps::fluent_uri::Uri, 5 8 prelude::{HttpClient, XrpcClient}, 6 - types::{datetime::Datetime, ident::AtIdentifier, string::Nsid}, 9 + types::{datetime::Datetime, did::Did, ident::AtIdentifier, string::Nsid, uri::UriValue}, 7 10 }; 8 11 use jacquard_api::app_bsky::actor::ProfileViewBasic; 12 + use jacquard_api::app_bsky::embed::record::{View as RecordView, ViewRecord, ViewUnionRecord}; 13 + use jacquard_api::app_bsky::embed::record_with_media::{self, RecordWithMediaMedia, ViewMedia}; 14 + use jacquard_api::app_bsky::embed::{external, images, video}; 9 15 use jacquard_api::app_bsky::feed::get_author_feed::{GetAuthorFeedOutput, GetAuthorFeedRequest}; 10 - use jacquard_api::app_bsky::feed::{FeedViewPost, PostView}; 16 + use jacquard_api::app_bsky::feed::post::{Post, PostEmbed}; 17 + use jacquard_api::app_bsky::feed::{FeedViewPost, PostView, PostViewEmbed}; 11 18 use jacquard_api::com_atproto::repo::list_records::{ListRecords, ListRecordsResponse}; 19 + use jacquard_api::com_atproto::repo::strong_ref::StrongRef; 12 20 use jacquard_axum::{ExtractXrpc, IntoRouter, service_auth::ExtractOptionalServiceAuth}; 13 21 use jacquard_common::xrpc; 14 22 use log::{debug, info}; 15 23 24 + fn blob_url(pds_url: &Uri<String>, did: &Did<'_>, cid: &str) -> Option<UriValue<'static>> { 25 + UriValue::new_owned(format!( 26 + "{pds_url}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}" 27 + )) 28 + .ok() 29 + } 30 + 31 + fn images_view( 32 + images_embed: images::Images<'static>, 33 + pds_url: &Uri<String>, 34 + did: &Did<'_>, 35 + ) -> images::View<'static> { 36 + let view_images = images_embed 37 + .images 38 + .into_iter() 39 + .filter_map(|img| { 40 + let uri = blob_url(pds_url, did, img.image.blob().cid().as_ref())?; 41 + Some(images::ViewImage { 42 + alt: img.alt, 43 + aspect_ratio: img.aspect_ratio, 44 + fullsize: uri.clone(), 45 + thumb: uri, 46 + extra_data: Default::default(), 47 + }) 48 + }) 49 + .collect(); 50 + images::View { 51 + images: view_images, 52 + extra_data: Default::default(), 53 + } 54 + } 55 + 56 + fn video_view( 57 + video_embed: video::Video<'static>, 58 + pds_url: &Uri<String>, 59 + did: &Did<'_>, 60 + ) -> Option<video::View<'static>> { 61 + let cid = video_embed.video.blob().cid().clone(); 62 + let playlist = blob_url(pds_url, did, cid.as_ref())?; 63 + let presentation = video_embed 64 + .presentation 65 + .map(|p| video::ViewPresentation::from(p.as_str().to_string())); 66 + Some(video::View { 67 + alt: video_embed.alt, 68 + aspect_ratio: video_embed.aspect_ratio, 69 + cid, 70 + playlist, 71 + presentation, 72 + thumbnail: None, 73 + extra_data: Default::default(), 74 + }) 75 + } 76 + 77 + fn external_view( 78 + source: external::External<'static>, 79 + pds_url: &Uri<String>, 80 + did: &Did<'_>, 81 + ) -> external::View<'static> { 82 + let thumb = source 83 + .thumb 84 + .as_ref() 85 + .and_then(|b| blob_url(pds_url, did, b.blob().cid().as_ref())); 86 + external::View { 87 + external: external::ViewExternal { 88 + description: source.description, 89 + thumb, 90 + title: source.title, 91 + uri: source.uri, 92 + extra_data: Default::default(), 93 + }, 94 + extra_data: Default::default(), 95 + } 96 + } 97 + 98 + fn media_to_view( 99 + media: RecordWithMediaMedia<'static>, 100 + pds_url: &Uri<String>, 101 + did: &Did<'_>, 102 + ) -> Option<ViewMedia<'static>> { 103 + match media { 104 + RecordWithMediaMedia::Images(images_embed) => Some(ViewMedia::ImagesView(Box::new( 105 + images_view(*images_embed, pds_url, did), 106 + ))), 107 + RecordWithMediaMedia::Video(video_embed) => { 108 + video_view(*video_embed, pds_url, did).map(|v| ViewMedia::VideoView(Box::new(v))) 109 + } 110 + RecordWithMediaMedia::External(external_embed) => Some(ViewMedia::ExternalView(Box::new( 111 + external_view(external_embed.external, pds_url, did), 112 + ))), 113 + RecordWithMediaMedia::Unknown(_) => None, 114 + } 115 + } 116 + 117 + async fn fetch_view_record( 118 + state: &AppState, 119 + strong_ref: StrongRef<'static>, 120 + ) -> Option<ViewRecord<'static>> { 121 + let fetched = match state.agent.fetch_record_slingshot(&strong_ref.uri).await { 122 + Ok(f) => f, 123 + Err(err) => { 124 + log::warn!("fetch quoted record {}: {err:#}", strong_ref.uri); 125 + return None; 126 + } 127 + }; 128 + 129 + let author_id = strong_ref.uri.authority().clone().into_static(); 130 + let ctx = resolve_author_context(state, author_id).await.ok()?; 131 + let author = ProfileViewBasic { 132 + associated: None, 133 + avatar: ctx.avatar_uri, 134 + created_at: ctx.profile.created_at, 135 + debug: None, 136 + did: ctx.did, 137 + display_name: ctx.profile.display_name, 138 + handle: ctx.handle, 139 + labels: None, 140 + pronouns: ctx.profile.pronouns, 141 + status: None, 142 + verification: None, 143 + viewer: None, 144 + extra_data: Default::default(), 145 + }; 146 + 147 + let indexed_at = jacquard::common::from_data_owned::<Post<'static>>(fetched.value.clone()) 148 + .ok() 149 + .map(|p| p.created_at) 150 + .unwrap_or_else(Datetime::now); 151 + 152 + Some(ViewRecord { 153 + author, 154 + cid: fetched.cid.unwrap_or(strong_ref.cid), 155 + embeds: None, 156 + indexed_at, 157 + labels: None, 158 + like_count: None, 159 + quote_count: None, 160 + reply_count: None, 161 + repost_count: None, 162 + uri: fetched.uri, 163 + value: fetched.value, 164 + extra_data: Default::default(), 165 + }) 166 + } 167 + 168 + async fn post_embed_to_view( 169 + embed: PostEmbed<'static>, 170 + state: &AppState, 171 + pds_url: &Uri<String>, 172 + did: &Did<'_>, 173 + ) -> Option<PostViewEmbed<'static>> { 174 + match embed { 175 + PostEmbed::Images(images_embed) => Some(PostViewEmbed::ImagesView(Box::new(images_view( 176 + *images_embed, 177 + pds_url, 178 + did, 179 + )))), 180 + PostEmbed::Video(video_embed) => { 181 + video_view(*video_embed, pds_url, did).map(|v| PostViewEmbed::VideoView(Box::new(v))) 182 + } 183 + PostEmbed::External(external_embed) => Some(PostViewEmbed::ExternalView(Box::new( 184 + external_view(external_embed.external, pds_url, did), 185 + ))), 186 + PostEmbed::RecordWithMedia(record_with_media) => { 187 + let media = media_to_view(record_with_media.media, pds_url, did)?; 188 + let view_record = fetch_view_record(state, record_with_media.record.record).await?; 189 + let record = RecordView { 190 + record: ViewUnionRecord::ViewRecord(Box::new(view_record)), 191 + extra_data: Default::default(), 192 + }; 193 + Some(PostViewEmbed::RecordWithMediaView(Box::new( 194 + record_with_media::View { 195 + media, 196 + record, 197 + extra_data: Default::default(), 198 + }, 199 + ))) 200 + } 201 + PostEmbed::Record(_) | PostEmbed::Unknown(_) => None, 202 + } 203 + } 204 + 16 205 pub async fn get_author_feed( 17 206 State(state): State<AppState>, 18 207 ExtractOptionalServiceAuth(_auth): ExtractOptionalServiceAuth, ··· 74 263 XrpcErrorResponse::internal_server_error() 75 264 })?; 76 265 77 - let feed = output 78 - .records 79 - .into_iter() 80 - .map(|record| { 81 - let post = PostView { 82 - author: author.clone(), 83 - bookmark_count: None, 84 - cid: record.cid, 85 - debug: None, 86 - embed: None, 87 - indexed_at: Datetime::now(), 88 - labels: None, 89 - like_count: None, 90 - quote_count: None, 91 - record: record.value, 92 - reply_count: None, 93 - repost_count: None, 94 - threadgate: None, 95 - uri: record.uri, 96 - viewer: None, 97 - extra_data: Default::default(), 98 - }; 99 - FeedViewPost { 100 - feed_context: None, 101 - post, 102 - reason: None, 103 - reply: None, 104 - req_id: None, 105 - extra_data: Default::default(), 106 - } 107 - }) 108 - .collect(); 266 + let mut feed = Vec::with_capacity(output.records.len()); 267 + for record in output.records { 268 + let embed_source = jacquard::common::from_data_owned::<Post<'static>>(record.value.clone()) 269 + .ok() 270 + .and_then(|post| post.embed); 271 + let embed = match embed_source { 272 + Some(e) => post_embed_to_view(e, &state, &ctx.pds_url, &ctx.did).await, 273 + None => None, 274 + }; 275 + let post = PostView { 276 + author: author.clone(), 277 + bookmark_count: None, 278 + cid: record.cid, 279 + debug: None, 280 + embed, 281 + indexed_at: Datetime::now(), 282 + labels: None, 283 + like_count: None, 284 + quote_count: None, 285 + record: record.value, 286 + reply_count: None, 287 + repost_count: None, 288 + threadgate: None, 289 + uri: record.uri, 290 + viewer: None, 291 + extra_data: Default::default(), 292 + }; 293 + feed.push(FeedViewPost { 294 + feed_context: None, 295 + post, 296 + reason: None, 297 + reply: None, 298 + req_id: None, 299 + extra_data: Default::default(), 300 + }); 301 + } 109 302 110 303 Ok(Json(GetAuthorFeedOutput { 111 304 cursor: output.cursor,