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

Configure Feed

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

constellation for followers

+437 -7
+2
Cargo.lock
··· 410 410 "jacquard-api", 411 411 "jacquard-axum", 412 412 "jacquard-common", 413 + "jacquard-derive", 413 414 "jacquard-identity", 414 415 "log", 415 416 "postcard", 416 417 "reqwest", 418 + "rustversion", 417 419 "serde", 418 420 "serde_json", 419 421 "thiserror 2.0.18",
+2
Cargo.toml
··· 18 18 jacquard-axum = { path = "../jacquard/crates/jacquard-axum" } 19 19 # jacquard-common = "0.11.0" 20 20 jacquard-common = { path = "../jacquard/crates/jacquard-common", features = ["websocket", "streaming", "reqwest-client"] } 21 + jacquard-derive = { path = "../jacquard/crates/jacquard-derive" } 21 22 # jacquard-identity = { version = "0.11.0", features = ["cache", "dns"] } 22 23 jacquard-identity = { path = "../jacquard/crates/jacquard-identity", features = ["cache", "dns"] } 23 24 log = "0.4.29" 24 25 postcard = { version = "1", features = ["alloc"] } 25 26 reqwest = { version = "0.12.23", features = ["stream", "json"] } 27 + rustversion = "1.0" 26 28 serde = { version = "1.0.228", features = ["derive"] } 27 29 serde_json = "1.0.149" 28 30 thiserror = "2"
+3
README.md
··· 4 4 5 5 # Work done currently 6 6 - `app.bsky.actor.getProfile` - Just started, and let me tell you. It got hands 7 + - Should be pulling in everything from the profile lexicon 8 + - Pulls in followers since it's easy from constellation 9 + - Other things like labels, etc are not done yet 7 10 - `app.bsky.actor.getPreferences` - Works, but not as expected. It has it's own internal preferences. Not the ones from your PDS. best to just use a social app that clears the atproto-proxy to get from your own PDS 8 11 - `app.bsky.actor.putPreferences` - [lol](https://github.com/bluesky-social/atproto/issues/4193). Best to just use a bsky-social app fork like https://blacksky.community for running this AppView
+62
lexicons/blue/microcosm/links/getBacklinkDids.json
··· 1 + { 2 + "defs": { 3 + "main": { 4 + "description": "a list of distinct dids with a specific records linking to a target at a specified path", 5 + "output": { 6 + "encoding": "application/json", 7 + "schema": { 8 + "properties": { 9 + "cursor": { 10 + "description": "pagination cursor", 11 + "type": "string" 12 + }, 13 + "linking_dids": { 14 + "items": { 15 + "format": "did", 16 + "type": "string" 17 + }, 18 + "type": "array" 19 + }, 20 + "total": { 21 + "description": "total number of matching links", 22 + "type": "integer" 23 + } 24 + }, 25 + "required": [ 26 + "total", 27 + "linking_dids" 28 + ], 29 + "type": "object" 30 + } 31 + }, 32 + "parameters": { 33 + "properties": { 34 + "limit": { 35 + "default": 16, 36 + "description": "number of results to return", 37 + "maximum": 100, 38 + "minimum": 1, 39 + "type": "integer" 40 + }, 41 + "source": { 42 + "description": "collection and path specification (e.g., 'app.bsky.feed.like:subject.uri')", 43 + "type": "string" 44 + }, 45 + "subject": { 46 + "description": "the target being linked to (at-uri, did, or uri)", 47 + "format": "uri", 48 + "type": "string" 49 + } 50 + }, 51 + "required": [ 52 + "subject", 53 + "source" 54 + ], 55 + "type": "params" 56 + }, 57 + "type": "query" 58 + } 59 + }, 60 + "id": "blue.microcosm.links.getBacklinkDids", 61 + "lexicon": 1 62 + }
+61
src/constellation.rs
··· 1 + use anyhow::Context; 2 + use jacquard::IntoStatic; 3 + use jacquard::common::xrpc::XrpcEndpoint; 4 + use reqwest::Client; 5 + 6 + use crate::lexicons::blue_microcosm::links::get_backlink_dids::{ 7 + GetBacklinkDidsOutput, GetBacklinkDidsRequest, 8 + }; 9 + 10 + pub async fn get_backlink_dids( 11 + client: &Client, 12 + host: &str, 13 + subject: &str, 14 + source: &str, 15 + limit: Option<i64>, 16 + cursor: Option<&str>, 17 + ) -> anyhow::Result<GetBacklinkDidsOutput<'static>> { 18 + let url = format!( 19 + "{}{}", 20 + host.trim_end_matches('/'), 21 + GetBacklinkDidsRequest::PATH, 22 + ); 23 + 24 + let mut query: Vec<(&str, String)> = Vec::with_capacity(4); 25 + query.push(("subject", subject.to_string())); 26 + query.push(("source", source.to_string())); 27 + if let Some(limit) = limit { 28 + query.push(("limit", limit.to_string())); 29 + } 30 + if let Some(cursor) = cursor { 31 + query.push(("cursor", cursor.to_string())); 32 + } 33 + 34 + let resp = client 35 + .get(&url) 36 + .header(reqwest::header::CONTENT_TYPE, "application/json") 37 + .header(reqwest::header::ACCEPT, "application/json") 38 + .query(&query) 39 + .send() 40 + .await 41 + .context("constellation getBacklinkDids request")?; 42 + 43 + let status = resp.status(); 44 + let bytes = resp 45 + .bytes() 46 + .await 47 + .context("constellation getBacklinkDids read")?; 48 + 49 + if !status.is_success() { 50 + anyhow::bail!( 51 + "constellation getBacklinkDids: status={} body={}", 52 + status, 53 + String::from_utf8_lossy(&bytes) 54 + ); 55 + } 56 + 57 + let parsed: GetBacklinkDidsOutput<'_> = 58 + serde_json::from_slice(&bytes).context("constellation getBacklinkDids parse")?; 59 + 60 + Ok(parsed.into_static()) 61 + }
+6
src/lexicons/blue_microcosm.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + pub mod links;
+6
src/lexicons/blue_microcosm/links.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + pub mod get_backlink_dids;
+43
src/lexicons/builder_types.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + /// Marker type indicating a builder field has been set 7 + pub struct Set<T>(pub T); 8 + impl<T> Set<T> { 9 + /// Extract the inner value 10 + #[inline] 11 + pub fn into_inner(self) -> T { 12 + self.0 13 + } 14 + } 15 + 16 + /// Marker type indicating a builder field has not been set 17 + pub struct Unset; 18 + /// Trait indicating a builder field is set (has a value) 19 + #[rustversion::attr( 20 + since(1.78.0), 21 + diagnostic::on_unimplemented( 22 + message = "the field `{Self}` was not set, but this method requires it to be set", 23 + label = "the field `{Self}` was not set" 24 + ) 25 + )] 26 + pub trait IsSet: private::Sealed {} 27 + /// Trait indicating a builder field is unset (no value yet) 28 + #[rustversion::attr( 29 + since(1.78.0), 30 + diagnostic::on_unimplemented( 31 + message = "the field `{Self}` was already set, but this method requires it to be unset", 32 + label = "the field `{Self}` was already set" 33 + ) 34 + )] 35 + pub trait IsUnset: private::Sealed {} 36 + impl<T> IsSet for Set<T> {} 37 + impl IsUnset for Unset {} 38 + mod private { 39 + /// Sealed trait to prevent external implementations 40 + pub trait Sealed {} 41 + impl<T> Sealed for super::Set<T> {} 42 + impl Sealed for super::Unset {} 43 + }
+2
src/lexicons/mod.rs
··· 1 + pub mod blue_microcosm; 2 + pub mod builder_types;
+10
src/main.rs
··· 1 + extern crate alloc; 2 + 1 3 use std::env; 2 4 use std::net::SocketAddr; 3 5 use std::path::Path; ··· 17 19 use crate::error::{Error, Result}; 18 20 use crate::state::AppState; 19 21 22 + mod constellation; 20 23 mod error; 24 + mod lexicons; 21 25 mod server; 22 26 mod state; 23 27 mod storage; 24 28 mod sync; 29 + 30 + pub use crate::lexicons::builder_types; 25 31 26 32 static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 27 33 ··· 66 72 let data_dir = env::var("CERES_DATA_DIRECTORY").unwrap_or_else(|_| ".ceres_data".to_string()); 67 73 let db = storage::open(Path::new(&data_dir))?; 68 74 75 + let constellation_host = env::var("CONSTELLATION_HOST") 76 + .unwrap_or_else(|_| "https://constellation.microcosm.blue".to_string()); 77 + 69 78 let state = AppState { 70 79 service_auth, 71 80 reqwest_client, 72 81 agent, 73 82 resolver, 74 83 forwarded_app_view: env::var("FORWARDED_APP_VIEW").ok(), 84 + constellation_host, 75 85 db: db.clone(), 76 86 }; 77 87
+26 -7
src/server/xrpc/app_bsky_actor.rs
··· 1 - use std::str::FromStr; 2 - use std::time::SystemTime; 3 - 1 + use crate::constellation; 2 + use crate::server::xrpc::XrpcErrorResponse; 4 3 use crate::state::AppState; 5 4 use crate::storage; 6 - use crate::{server::xrpc::XrpcErrorResponse, sync::backfill::BackfillJob}; 7 - use axum::Form; 8 5 use axum::{Json, Router, extract::State}; 9 6 use jacquard::client::AgentSessionExt; 10 7 use jacquard::types::aturi::AtUri; 11 8 use jacquard::{ 12 9 IntoStatic, 13 10 prelude::IdentityResolver, 14 - types::{datetime::Datetime, ident::AtIdentifier, uri::UriValue}, 11 + types::{datetime::Datetime, did::Did, ident::AtIdentifier, uri::UriValue}, 15 12 }; 16 13 use jacquard_api::app_bsky::actor::get_profiles::{GetProfilesOutput, GetProfilesRequest}; 17 14 use jacquard_api::app_bsky::actor::{ ··· 23 20 }; 24 21 use jacquard_axum::{ExtractXrpc, IntoRouter, service_auth::ExtractOptionalServiceAuth}; 25 22 use log::info; 23 + use std::str::FromStr; 24 + 25 + async fn fetch_followers_count(state: &AppState, did: &Did<'_>) -> Option<i64> { 26 + match constellation::get_backlink_dids( 27 + &state.reqwest_client, 28 + &state.constellation_host, 29 + did.as_ref(), 30 + "app.bsky.graph.follow:subject", 31 + None, 32 + None, 33 + ) 34 + .await 35 + { 36 + Ok(out) => Some(out.total), 37 + Err(err) => { 38 + log::warn!("constellation followers fetch failed: {err:#}"); 39 + None 40 + } 41 + } 42 + } 26 43 27 44 async fn resolve_profile_view( 28 45 state: &AppState, ··· 110 127 XrpcErrorResponse::internal_server_error() 111 128 })?; 112 129 130 + let followers_count = fetch_followers_count(state, &did).await; 131 + 113 132 Ok(ProfileViewDetailed { 114 133 associated: None, 115 134 avatar, ··· 119 138 description: profile_record.description, 120 139 did, 121 140 display_name: profile_record.display_name, 122 - followers_count: Some(67), 141 + followers_count, 123 142 follows_count: Some(67), 124 143 handle, 125 144 indexed_at: Some(Datetime::now()),
+1
src/state.rs
··· 16 16 pub agent: Arc<AppAgent>, 17 17 pub resolver: JacquardResolver, 18 18 pub forwarded_app_view: Option<String>, 19 + pub constellation_host: String, 19 20 pub db: DbRef, 20 21 } 21 22