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.

GetAuthor feed start

+258 -100
+1
src/server/mod.rs
··· 21 21 let app: Router = Router::new() 22 22 .route("/.well-known/did.json", get(well_known::did_document)) 23 23 .merge(xrpc::app_bsky_actor::routes()) 24 + .merge(xrpc::app_bsky_feed::routes()) 24 25 .fallback(fallback::log_request) 25 26 .layer(CorsLayer::permissive()) 26 27 .with_state(state);
+14 -100
src/server/xrpc/app_bsky_actor.rs
··· 1 1 use crate::constellation; 2 - use crate::server::xrpc::XrpcErrorResponse; 2 + use crate::server::xrpc::{XrpcErrorResponse, resolve_author_context}; 3 3 use crate::state::AppState; 4 4 use crate::storage; 5 5 use axum::{Json, Router, extract::State}; 6 - use jacquard::client::AgentSessionExt; 7 - use jacquard::types::aturi::AtUri; 8 6 use jacquard::{ 9 7 IntoStatic, 10 - prelude::IdentityResolver, 11 - types::{datetime::Datetime, did::Did, ident::AtIdentifier, uri::UriValue}, 8 + types::{datetime::Datetime, did::Did, ident::AtIdentifier}, 12 9 }; 13 10 use jacquard_api::app_bsky::actor::get_profiles::{GetProfilesOutput, GetProfilesRequest}; 14 11 use jacquard_api::app_bsky::actor::{ 15 12 PreferencesItem, ProfileViewDetailed, 16 13 get_preferences::{GetPreferencesOutput, GetPreferencesRequest}, 17 14 get_profile::{GetProfileOutput, GetProfileRequest}, 18 - profile::Profile, 19 15 put_preferences::PutPreferencesRequest, 20 16 }; 21 17 use jacquard_axum::{ExtractXrpc, IntoRouter, service_auth::ExtractOptionalServiceAuth}; 22 18 use log::info; 23 - use std::str::FromStr; 24 19 25 20 async fn fetch_followers_count(state: &AppState, did: &Did<'_>) -> Option<i64> { 26 21 match constellation::get_backlink_dids( ··· 45 40 state: &AppState, 46 41 actor: AtIdentifier<'_>, 47 42 ) -> Result<ProfileViewDetailed<'static>, XrpcErrorResponse> { 48 - let did = match actor { 49 - AtIdentifier::Did(did) => did.into_static(), 50 - AtIdentifier::Handle(handle) => { 51 - state 52 - .resolver 53 - .resolve_handle(&handle) 54 - .await 55 - .map_err(|err| { 56 - log::error!("Error resolving the handle to a did for: {handle}"); 57 - log::error!("{err}"); 58 - XrpcErrorResponse::internal_server_error() 59 - })? 60 - } 61 - }; 62 - 63 - let requested_did_doc = state.resolver.resolve_did_doc(&did).await.map_err(|err| { 64 - log::error!("{err}"); 65 - XrpcErrorResponse::internal_server_error() 66 - })?; 67 - 68 - let did_doc = requested_did_doc.parse().map_err(|err| { 69 - log::error!("{err}"); 70 - XrpcErrorResponse::internal_server_error() 71 - })?; 72 - 73 - let handle = did_doc.handles().get(0).cloned().ok_or_else(|| { 74 - log::error!("No handle found in DID document"); 75 - XrpcErrorResponse::internal_server_error() 76 - })?; 77 - 78 - let pds_url = did_doc.pds_endpoint().ok_or_else(|| { 79 - log::error!("No PDS URL found in DID document"); 80 - XrpcErrorResponse::internal_server_error() 81 - })?; 82 - 83 - let at_uri = AtUri::from_str(format!("at://{did}/app.bsky.actor.profile/self").as_str()) 84 - .map_err(|err| { 85 - log::error!("{err}"); 86 - XrpcErrorResponse::internal_server_error() 87 - })?; 88 - let result = state 89 - .agent 90 - .fetch_record_slingshot(&at_uri) 91 - .await 92 - .map_err(|err| { 93 - log::error!("{err}"); 94 - XrpcErrorResponse::internal_server_error() 95 - })?; 96 - let profile_record: Profile<'static> = jacquard::common::from_data_owned(result.value) 97 - .map_err(|err| { 98 - log::error!("parse profile record: {err}"); 99 - XrpcErrorResponse::internal_server_error() 100 - })?; 101 - 102 - let avatar = profile_record 103 - .avatar 104 - .map(|blob_ref| { 105 - UriValue::new_owned(format!( 106 - "{pds_url}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}", 107 - cid = blob_ref.blob().cid() 108 - )) 109 - }) 110 - .transpose() 111 - .map_err(|err| { 112 - log::error!("avatar uri: {err}"); 113 - XrpcErrorResponse::internal_server_error() 114 - })?; 115 - 116 - let banner = profile_record 117 - .banner 118 - .map(|blob_ref| { 119 - UriValue::new_owned(format!( 120 - "{pds_url}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}", 121 - cid = blob_ref.blob().cid() 122 - )) 123 - }) 124 - .transpose() 125 - .map_err(|err| { 126 - log::error!("banner uri: {err}"); 127 - XrpcErrorResponse::internal_server_error() 128 - })?; 129 - 130 - let followers_count = fetch_followers_count(state, &did).await; 43 + let ctx = resolve_author_context(state, actor).await?; 44 + let followers_count = fetch_followers_count(state, &ctx.did).await; 131 45 132 46 Ok(ProfileViewDetailed { 133 47 associated: None, 134 - avatar, 135 - banner, 136 - created_at: profile_record.created_at, 48 + avatar: ctx.avatar_uri, 49 + banner: ctx.banner_uri, 50 + created_at: ctx.profile.created_at, 137 51 debug: None, 138 - description: profile_record.description, 139 - did, 140 - display_name: profile_record.display_name, 52 + description: ctx.profile.description, 53 + did: ctx.did, 54 + display_name: ctx.profile.display_name, 141 55 followers_count, 142 56 follows_count: Some(67), 143 - handle, 57 + handle: ctx.handle, 144 58 indexed_at: Some(Datetime::now()), 145 59 joined_via_starter_pack: None, 146 60 labels: None, 147 - pinned_post: profile_record.pinned_post, 61 + pinned_post: ctx.profile.pinned_post, 148 62 posts_count: None, 149 - pronouns: profile_record.pronouns, 63 + pronouns: ctx.profile.pronouns, 150 64 status: None, 151 65 verification: None, 152 66 viewer: None, 153 - website: profile_record.website, 67 + website: ctx.profile.website, 154 68 extra_data: Default::default(), 155 69 }) 156 70 }
+121
src/server/xrpc/app_bsky_feed.rs
··· 1 + use crate::server::xrpc::{XrpcErrorResponse, resolve_author_context}; 2 + use crate::state::AppState; 3 + use axum::{Json, Router, extract::State}; 4 + use jacquard::{ 5 + prelude::{HttpClient, XrpcClient}, 6 + types::{datetime::Datetime, ident::AtIdentifier, string::Nsid}, 7 + }; 8 + use jacquard_api::app_bsky::actor::ProfileViewBasic; 9 + use jacquard_api::app_bsky::feed::get_author_feed::{GetAuthorFeedOutput, GetAuthorFeedRequest}; 10 + use jacquard_api::app_bsky::feed::{FeedViewPost, PostView}; 11 + use jacquard_api::com_atproto::repo::list_records::{ListRecords, ListRecordsResponse}; 12 + use jacquard_axum::{ExtractXrpc, IntoRouter, service_auth::ExtractOptionalServiceAuth}; 13 + use jacquard_common::xrpc; 14 + use log::{debug, info}; 15 + 16 + pub async fn get_author_feed( 17 + State(state): State<AppState>, 18 + ExtractOptionalServiceAuth(_auth): ExtractOptionalServiceAuth, 19 + ExtractXrpc(req): ExtractXrpc<GetAuthorFeedRequest>, 20 + ) -> Result<Json<GetAuthorFeedOutput<'static>>, XrpcErrorResponse> { 21 + info!( 22 + "get_author_feed actor={} limit={:?}", 23 + req.actor.as_ref(), 24 + req.limit 25 + ); 26 + debug!( 27 + "get_author_feed filter={:?} include_pins={:?} cursor={:?}", 28 + req.filter, req.include_pins, req.cursor 29 + ); 30 + 31 + let ctx = resolve_author_context(&state, req.actor).await?; 32 + 33 + let author = ProfileViewBasic { 34 + associated: None, 35 + avatar: ctx.avatar_uri.clone(), 36 + created_at: ctx.profile.created_at, 37 + debug: None, 38 + did: ctx.did.clone(), 39 + display_name: ctx.profile.display_name.clone(), 40 + handle: ctx.handle.clone(), 41 + labels: None, 42 + pronouns: ctx.profile.pronouns.clone(), 43 + status: None, 44 + verification: None, 45 + viewer: None, 46 + extra_data: Default::default(), 47 + }; 48 + 49 + let post_collection = 50 + Nsid::new_static("app.bsky.feed.post").expect("app.bsky.feed.post is a valid nsid"); 51 + let list_req = ListRecords::new() 52 + .repo(AtIdentifier::Did(ctx.did.clone())) 53 + .collection(post_collection) 54 + .maybe_limit(req.limit) 55 + .maybe_cursor(req.cursor) 56 + .build(); 57 + 58 + let opts = state.agent.opts().await; 59 + let http_request = xrpc::build_http_request(&ctx.pds_url, &list_req, &opts).map_err(|err| { 60 + log::error!("build listRecords request: {err}"); 61 + XrpcErrorResponse::internal_server_error() 62 + })?; 63 + let http_response = state.agent.send_http(http_request).await.map_err(|err| { 64 + log::error!("send listRecords: {err}"); 65 + XrpcErrorResponse::internal_server_error() 66 + })?; 67 + let response: xrpc::Response<ListRecordsResponse> = xrpc::process_response(http_response) 68 + .map_err(|err| { 69 + log::error!("process listRecords response: {err}"); 70 + XrpcErrorResponse::internal_server_error() 71 + })?; 72 + let output = response.into_output().map_err(|err| { 73 + log::error!("parse listRecords output: {err}"); 74 + XrpcErrorResponse::internal_server_error() 75 + })?; 76 + 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(); 109 + 110 + Ok(Json(GetAuthorFeedOutput { 111 + cursor: output.cursor, 112 + feed, 113 + extra_data: Default::default(), 114 + })) 115 + } 116 + 117 + pub fn routes() -> Router<AppState> { 118 + Router::<AppState>::new().merge(GetAuthorFeedRequest::into_router::<_, AppState, _>( 119 + get_author_feed, 120 + )) 121 + }
+122
src/server/xrpc/mod.rs
··· 1 + use std::str::FromStr; 2 + 1 3 use axum::{ 2 4 Json, 3 5 http::StatusCode, 4 6 response::{IntoResponse, Response}, 5 7 }; 8 + use jacquard::{ 9 + IntoStatic, 10 + client::AgentSessionExt, 11 + deps::fluent_uri::Uri, 12 + prelude::IdentityResolver, 13 + types::{aturi::AtUri, did::Did, ident::AtIdentifier, string::Handle, uri::UriValue}, 14 + }; 15 + use jacquard_api::app_bsky::actor::profile::Profile; 16 + 17 + use crate::state::AppState; 6 18 7 19 pub mod app_bsky_actor; 20 + pub mod app_bsky_feed; 8 21 9 22 pub struct XrpcErrorResponse { 10 23 error: XrpcError, ··· 55 68 (self.status, Json(self.error)).into_response() 56 69 } 57 70 } 71 + 72 + //Author things 73 + 74 + pub(crate) struct AuthorContext { 75 + pub did: Did<'static>, 76 + pub handle: Handle<'static>, 77 + pub pds_url: Uri<String>, 78 + pub profile: Profile<'static>, 79 + pub avatar_uri: Option<UriValue<'static>>, 80 + pub banner_uri: Option<UriValue<'static>>, 81 + } 82 + 83 + pub(crate) async fn resolve_author_context( 84 + state: &AppState, 85 + actor: AtIdentifier<'_>, 86 + ) -> Result<AuthorContext, XrpcErrorResponse> { 87 + let did = match actor { 88 + AtIdentifier::Did(did) => did.into_static(), 89 + AtIdentifier::Handle(handle) => { 90 + state 91 + .resolver 92 + .resolve_handle(&handle) 93 + .await 94 + .map_err(|err| { 95 + log::error!("Error resolving the handle to a did for: {handle}"); 96 + log::error!("{err}"); 97 + XrpcErrorResponse::internal_server_error() 98 + })? 99 + } 100 + }; 101 + 102 + let requested_did_doc = state.resolver.resolve_did_doc(&did).await.map_err(|err| { 103 + log::error!("{err}"); 104 + XrpcErrorResponse::internal_server_error() 105 + })?; 106 + 107 + let did_doc = requested_did_doc.parse().map_err(|err| { 108 + log::error!("{err}"); 109 + XrpcErrorResponse::internal_server_error() 110 + })?; 111 + 112 + let handle = did_doc.handles().get(0).cloned().ok_or_else(|| { 113 + log::error!("No handle found in DID document"); 114 + XrpcErrorResponse::internal_server_error() 115 + })?; 116 + 117 + let pds_url = did_doc.pds_endpoint().ok_or_else(|| { 118 + log::error!("No PDS URL found in DID document"); 119 + XrpcErrorResponse::internal_server_error() 120 + })?; 121 + 122 + let at_uri = AtUri::from_str(format!("at://{did}/app.bsky.actor.profile/self").as_str()) 123 + .map_err(|err| { 124 + log::error!("{err}"); 125 + XrpcErrorResponse::internal_server_error() 126 + })?; 127 + let result = state 128 + .agent 129 + .fetch_record_slingshot(&at_uri) 130 + .await 131 + .map_err(|err| { 132 + log::error!("{err}"); 133 + XrpcErrorResponse::internal_server_error() 134 + })?; 135 + let profile: Profile<'static> = 136 + jacquard::common::from_data_owned(result.value).map_err(|err| { 137 + log::error!("parse profile record: {err}"); 138 + XrpcErrorResponse::internal_server_error() 139 + })?; 140 + 141 + let avatar_uri = profile 142 + .avatar 143 + .as_ref() 144 + .map(|blob_ref| { 145 + UriValue::new_owned(format!( 146 + "{pds_url}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}", 147 + cid = blob_ref.blob().cid() 148 + )) 149 + }) 150 + .transpose() 151 + .map_err(|err| { 152 + log::error!("avatar uri: {err}"); 153 + XrpcErrorResponse::internal_server_error() 154 + })?; 155 + 156 + let banner_uri = profile 157 + .banner 158 + .as_ref() 159 + .map(|blob_ref| { 160 + UriValue::new_owned(format!( 161 + "{pds_url}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}", 162 + cid = blob_ref.blob().cid() 163 + )) 164 + }) 165 + .transpose() 166 + .map_err(|err| { 167 + log::error!("banner uri: {err}"); 168 + XrpcErrorResponse::internal_server_error() 169 + })?; 170 + 171 + Ok(AuthorContext { 172 + did, 173 + handle, 174 + pds_url, 175 + profile, 176 + avatar_uri, 177 + banner_uri, 178 + }) 179 + }